From a1b3cd40647e8f7768b6db0bc64179e60f5d5937 Mon Sep 17 00:00:00 2001 From: Mircea Danila Dumitrescu Date: Mon, 2 Oct 2017 20:32:36 +0000 Subject: namespace should be lowercased in kubernetes. This is also true for the scenario where the namespace is generated from the project group-name. --- app/models/project_services/kubernetes_service.rb | 12 +++++++++++- changelogs/unreleased/mr-14642.yml | 6 ++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/mr-14642.yml diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 8ba07173c74..45a544e3674 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -153,7 +153,17 @@ class KubernetesService < DeploymentService end def default_namespace - "#{project.path}-#{project.id}" if project.present? + return unless project + + # 1. lowercase + # 2. replace non kubernetes characters with dash + # 3. trim dash from the beginning and end + + slugified = "#{project.path}-#{project.id}" + slugified.downcase! + slugified.gsub!(/[^a-z0-9]/, '-') + slugified.gsub!(/^-+|-+$/, '') + slugified end def build_kubeclient!(api_path: 'api', api_version: 'v1') diff --git a/changelogs/unreleased/mr-14642.yml b/changelogs/unreleased/mr-14642.yml new file mode 100644 index 00000000000..048cc79e323 --- /dev/null +++ b/changelogs/unreleased/mr-14642.yml @@ -0,0 +1,6 @@ +--- +title: Auto Devops kubernetes default namespace is now correctly built out of gitlab + project group-name +merge_request: 14642 +author: Mircea Danila Dumitrescu +type: fixed -- cgit v1.2.1 From b0c2772a900bd4390d0ead7192e1bda3acd01bab Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 12 Oct 2017 11:31:29 -0500 Subject: convert Autosave into pure es module and remove global export --- app/assets/javascripts/autosave.js | 31 +++++++++------------- app/assets/javascripts/issuable_form.js | 2 +- app/assets/javascripts/notes.js | 4 +-- .../notes/components/issue_comment_form.vue | 3 +-- app/assets/javascripts/notes/mixins/autosave.js | 3 +-- 5 files changed, 18 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index 4d2d4db7c0e..73bdab4ecb7 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -1,8 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */ +/* eslint-disable no-param-reassign, prefer-template, no-var, no-void, consistent-return */ + import AccessorUtilities from './lib/utils/accessor'; -window.Autosave = (function() { - function Autosave(field, key, resource) { +export default class Autosave { + constructor(field, key, resource) { this.field = field; this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); this.resource = resource; @@ -12,14 +13,10 @@ window.Autosave = (function() { this.key = 'autosave/' + key; this.field.data('autosave', this); this.restore(); - this.field.on('input', (function(_this) { - return function() { - return _this.save(); - }; - })(this)); + this.field.on('input', () => this.save()); } - Autosave.prototype.restore = function() { + restore() { var text; if (!this.isLocalStorageAvailable) return; @@ -40,9 +37,9 @@ window.Autosave = (function() { field.dispatchEvent(event); } } - }; + } - Autosave.prototype.save = function() { + save() { var text; text = this.field.val(); @@ -51,15 +48,13 @@ window.Autosave = (function() { } return this.reset(); - }; + } - Autosave.prototype.reset = function() { + reset() { if (!this.isLocalStorageAvailable) return; return window.localStorage.removeItem(this.key); - }; - - return Autosave; -})(); + } +} -export default window.Autosave; +window.Autosave = Autosave; diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 470c39c6f76..10f853066ca 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,9 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */ /* global GitLab */ -/* global Autosave */ /* global dateFormat */ import Pikaday from 'pikaday'; +import Autosave from './autosave'; import UsersSelect from './users_select'; import GfmAutoComplete from './gfm_auto_complete'; import ZenMode from './zen_mode'; diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 790f78d2e11..a09f938a281 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -5,7 +5,7 @@ default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, class-methods-use-this */ -/* global Autosave */ + /* global ResolveService */ /* global mrRefreshWidgetUrl */ @@ -21,7 +21,7 @@ import Flash from './flash'; import CommentTypeToggle from './comment_type_toggle'; import GLForm from './gl_form'; import loadAwardsHandler from './awards_handler'; -import './autosave'; +import Autosave from './autosave'; import './dropzone_input'; import TaskList from './task_list'; import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 2ce52e4538a..ad384a1cc36 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,10 +1,9 @@ @@ -73,8 +91,13 @@ export default { :img-alt="imgAlt" :css-classes="imgCssClasses" :size="imgSize" - :tooltip-text="tooltipText" + :tooltip-text="avatarTooltipText" + :tooltip-placement="tooltipPlacement" + /> + >{{username}} diff --git a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js index 52e450e9ba5..ce75df6fc7b 100644 --- a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js @@ -11,6 +11,7 @@ describe('User Avatar Link Component', function () { imgCssClasses: 'myextraavatarclass', tooltipText: 'tooltip text', tooltipPlacement: 'bottom', + username: 'username', }; const UserAvatarLinkComponent = Vue.extend(UserAvatarLink); @@ -47,4 +48,42 @@ describe('User Avatar Link Component', function () { expect(this.userAvatarLink[key]).toBeDefined(); }); }); + + describe('no username', function () { + beforeEach(function (done) { + this.userAvatarLink.username = ''; + + Vue.nextTick(done); + }); + + it('should not render as a child element', function () { + expect(this.userAvatarLink.$el.querySelector('span')).toBeNull(); + }); + + it('should render avatar image tooltip', function () { + expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(this.propsData.tooltipText); + }); + }); + + describe('username', function () { + it('should not render avatar image tooltip', function () { + expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(''); + }); + + it('should render as a child element', function () { + expect(this.userAvatarLink.$el.querySelector('span')).toBeDefined(); + }); + + it('should render username prop in ', function () { + expect(this.userAvatarLink.$el.querySelector('span').innerText.trim()).toEqual(this.propsData.username); + }); + + it('should render text tooltip for ', function () { + expect(this.userAvatarLink.$el.querySelector('span').dataset.originalTitle).toEqual(this.propsData.tooltipText); + }); + + it('should render text tooltip placement for ', function () { + expect(this.userAvatarLink.$el.querySelector('span').getAttribute('tooltip-placement')).toEqual(this.propsData.tooltipPlacement); + }); + }); }); -- cgit v1.2.1 From 1ab8aeeefd2ee826485a0be9d1c862782eaba3d4 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 19 Oct 2017 10:36:20 +0100 Subject: Moves placeholders components into shared folder with documentation. Makes them easier to reuse in MR and Snippets comments --- .../notes/components/issue_discussion.vue | 4 +- .../notes/components/issue_notes_app.vue | 10 +-- .../notes/components/issue_placeholder_note.vue | 53 ---------------- .../components/issue_placeholder_system_note.vue | 21 ------- .../notes/components/issue_system_note.vue | 54 ---------------- .../components/notes/placeholder_note.vue | 70 +++++++++++++++++++++ .../components/notes/placeholder_system_note.vue | 29 +++++++++ .../vue_shared/components/notes/system_note.vue | 73 ++++++++++++++++++++++ .../unreleased/38178-fl-mr-notes-components.yml | 6 ++ .../components/issue_placeholder_note_spec.js | 39 ------------ .../issue_placeholder_system_note_spec.js | 24 ------- .../notes/components/issue_system_note_spec.js | 53 ---------------- .../components/notes/placeholder_note_spec.js | 39 ++++++++++++ .../notes/placeholder_system_note_spec.js | 25 ++++++++ .../components/notes/system_note_spec.js | 57 +++++++++++++++++ 15 files changed, 306 insertions(+), 251 deletions(-) delete mode 100644 app/assets/javascripts/notes/components/issue_placeholder_note.vue delete mode 100644 app/assets/javascripts/notes/components/issue_placeholder_system_note.vue delete mode 100644 app/assets/javascripts/notes/components/issue_system_note.vue create mode 100644 app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue create mode 100644 app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue create mode 100644 app/assets/javascripts/vue_shared/components/notes/system_note.vue create mode 100644 changelogs/unreleased/38178-fl-mr-notes-components.yml delete mode 100644 spec/javascripts/notes/components/issue_placeholder_note_spec.js delete mode 100644 spec/javascripts/notes/components/issue_placeholder_system_note_spec.js delete mode 100644 spec/javascripts/notes/components/issue_system_note_spec.js create mode 100644 spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js create mode 100644 spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js create mode 100644 spec/javascripts/vue_shared/components/notes/system_note_spec.js diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index baf43190d9e..0f13221b81e 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -9,8 +9,8 @@ import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import issueNoteEditedText from './issue_note_edited_text.vue'; import issueNoteForm from './issue_note_form.vue'; - import placeholderNote from './issue_placeholder_note.vue'; - import placeholderSystemNote from './issue_placeholder_system_note.vue'; + import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; + import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import autosave from '../mixins/autosave'; export default { diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index aecd1f957e5..5c9119644e3 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -5,10 +5,10 @@ import * as constants from '../constants'; import issueNote from './issue_note.vue'; import issueDiscussion from './issue_discussion.vue'; - import issueSystemNote from './issue_system_note.vue'; + import systemNote from '../../vue_shared/components/notes/system_note.vue'; import issueCommentForm from './issue_comment_form.vue'; - import placeholderNote from './issue_placeholder_note.vue'; - import placeholderSystemNote from './issue_placeholder_system_note.vue'; + import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; + import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { @@ -37,7 +37,7 @@ components: { issueNote, issueDiscussion, - issueSystemNote, + systemNote, issueCommentForm, loadingIcon, placeholderNote, @@ -68,7 +68,7 @@ } return placeholderNote; } else if (note.individual_note) { - return note.notes[0].system ? issueSystemNote : issueNote; + return note.notes[0].system ? systemNote : issueNote; } return issueDiscussion; diff --git a/app/assets/javascripts/notes/components/issue_placeholder_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_note.vue deleted file mode 100644 index 6921d91372f..00000000000 --- a/app/assets/javascripts/notes/components/issue_placeholder_note.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue deleted file mode 100644 index 80a8ef56a83..00000000000 --- a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue deleted file mode 100644 index 0cfb6522e77..00000000000 --- a/app/assets/javascripts/notes/components/issue_system_note.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue new file mode 100644 index 00000000000..e467ca56704 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -0,0 +1,70 @@ + + + diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue new file mode 100644 index 00000000000..d805fea8006 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue @@ -0,0 +1,29 @@ + + + diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue new file mode 100644 index 00000000000..98f8f32557d --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -0,0 +1,73 @@ + + + diff --git a/changelogs/unreleased/38178-fl-mr-notes-components.yml b/changelogs/unreleased/38178-fl-mr-notes-components.yml new file mode 100644 index 00000000000..244ccfb3071 --- /dev/null +++ b/changelogs/unreleased/38178-fl-mr-notes-components.yml @@ -0,0 +1,6 @@ +--- +title: Moves placeholders components into shared folder with documentation. Makes + them easier to reuse in MR and Snippets comments +merge_request: +author: +type: other diff --git a/spec/javascripts/notes/components/issue_placeholder_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_note_spec.js deleted file mode 100644 index 6e5275087f3..00000000000 --- a/spec/javascripts/notes/components/issue_placeholder_note_spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import Vue from 'vue'; -import issuePlaceholderNote from '~/notes/components/issue_placeholder_note.vue'; -import store from '~/notes/stores'; -import { userDataMock } from '../mock_data'; - -describe('issue placeholder system note component', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(issuePlaceholderNote); - store.dispatch('setUserData', userDataMock); - vm = new Component({ - store, - propsData: { note: { body: 'Foo' } }, - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('user information', () => { - it('should render user avatar with link', () => { - expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); - expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(userDataMock.avatar_url); - }); - }); - - describe('note content', () => { - it('should render note header information', () => { - expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual(userDataMock.path); - expect(vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim()).toEqual(`@${userDataMock.username}`); - }); - - it('should render note body', () => { - expect(vm.$el.querySelector('.note-text p').textContent.trim()).toEqual('Foo'); - }); - }); -}); diff --git a/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js deleted file mode 100644 index d508a49f710..00000000000 --- a/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js +++ /dev/null @@ -1,24 +0,0 @@ -import Vue from 'vue'; -import placeholderSystemNote from '~/notes/components/issue_placeholder_system_note.vue'; - -describe('issue placeholder system note component', () => { - let mountComponent; - beforeEach(() => { - const PlaceholderSystemNote = Vue.extend(placeholderSystemNote); - - mountComponent = props => new PlaceholderSystemNote({ - propsData: { - note: { - body: props, - }, - }, - }).$mount(); - }); - - it('should render system note placeholder with plain text', () => { - const vm = mountComponent('This is a placeholder'); - - expect(vm.$el.tagName).toEqual('LI'); - expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual('This is a placeholder'); - }); -}); diff --git a/spec/javascripts/notes/components/issue_system_note_spec.js b/spec/javascripts/notes/components/issue_system_note_spec.js deleted file mode 100644 index c317ce32716..00000000000 --- a/spec/javascripts/notes/components/issue_system_note_spec.js +++ /dev/null @@ -1,53 +0,0 @@ -import Vue from 'vue'; -import issueSystemNote from '~/notes/components/issue_system_note.vue'; -import store from '~/notes/stores'; - -describe('issue system note', () => { - let vm; - let props; - - beforeEach(() => { - props = { - note: { - id: 1424, - author: { - id: 1, - name: 'Root', - username: 'root', - state: 'active', - avatar_url: 'path', - path: '/root', - }, - note_html: '

closed

', - system_note_icon_name: 'icon_status_closed', - created_at: '2017-08-02T10:51:58.559Z', - }, - }; - - store.dispatch('setTargetNoteHash', `note_${props.note.id}`); - - const Component = Vue.extend(issueSystemNote); - vm = new Component({ - store, - propsData: props, - }).$mount(); - }); - - it('should render a list item with correct id', () => { - expect(vm.$el.getAttribute('id')).toEqual(`note_${props.note.id}`); - }); - - it('should render target class is note is target note', () => { - expect(vm.$el.classList).toContain('target'); - }); - - it('should render svg icon', () => { - expect(vm.$el.querySelector('.timeline-icon svg')).toBeDefined(); - }); - - it('should render note header component', () => { - expect( - vm.$el.querySelector('.system-note-message').innerHTML, - ).toEqual(props.note.note_html); - }); -}); diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js new file mode 100644 index 00000000000..ba8ab0b2cd7 --- /dev/null +++ b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'; +import store from '~/notes/stores'; +import { userDataMock } from '../../../notes/mock_data'; + +describe('issue placeholder system note component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(issuePlaceholderNote); + store.dispatch('setUserData', userDataMock); + vm = new Component({ + store, + propsData: { note: { body: 'Foo' } }, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('user information', () => { + it('should render user avatar with link', () => { + expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); + expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(userDataMock.avatar_url); + }); + }); + + describe('note content', () => { + it('should render note header information', () => { + expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual(userDataMock.path); + expect(vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim()).toEqual(`@${userDataMock.username}`); + }); + + it('should render note body', () => { + expect(vm.$el.querySelector('.note-text p').textContent.trim()).toEqual('Foo'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js new file mode 100644 index 00000000000..7b8e6c330c2 --- /dev/null +++ b/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js @@ -0,0 +1,25 @@ +import Vue from 'vue'; +import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('placeholder system note component', () => { + let PlaceholderSystemNote; + let vm; + + beforeEach(() => { + PlaceholderSystemNote = Vue.extend(placeholderSystemNote); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render system note placeholder with plain text', () => { + vm = mountComponent(PlaceholderSystemNote, { + note: { body: 'This is a placeholder' }, + }); + + expect(vm.$el.tagName).toEqual('LI'); + expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual('This is a placeholder'); + }); +}); diff --git a/spec/javascripts/vue_shared/components/notes/system_note_spec.js b/spec/javascripts/vue_shared/components/notes/system_note_spec.js new file mode 100644 index 00000000000..36aaf0a6c2e --- /dev/null +++ b/spec/javascripts/vue_shared/components/notes/system_note_spec.js @@ -0,0 +1,57 @@ +import Vue from 'vue'; +import issueSystemNote from '~/vue_shared/components/notes/system_note.vue'; +import store from '~/notes/stores'; + +describe('issue system note', () => { + let vm; + let props; + + beforeEach(() => { + props = { + note: { + id: 1424, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: 'path', + path: '/root', + }, + note_html: '

closed

', + system_note_icon_name: 'icon_status_closed', + created_at: '2017-08-02T10:51:58.559Z', + }, + }; + + store.dispatch('setTargetNoteHash', `note_${props.note.id}`); + + const Component = Vue.extend(issueSystemNote); + vm = new Component({ + store, + propsData: props, + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render a list item with correct id', () => { + expect(vm.$el.getAttribute('id')).toEqual(`note_${props.note.id}`); + }); + + it('should render target class is note is target note', () => { + expect(vm.$el.classList).toContain('target'); + }); + + it('should render svg icon', () => { + expect(vm.$el.querySelector('.timeline-icon svg')).toBeDefined(); + }); + + it('should render note header component', () => { + expect( + vm.$el.querySelector('.system-note-message').innerHTML, + ).toEqual(props.note.note_html); + }); +}); -- cgit v1.2.1 From 1da6c9c06cd39792d4eadcdfa4f17fa4b895272f Mon Sep 17 00:00:00 2001 From: Herman van Rink Date: Fri, 20 Oct 2017 08:58:41 +0200 Subject: Suggest to rename the remote for existing repositories --- app/views/projects/empty.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index c9956183e12..af564b93dc3 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -72,6 +72,7 @@ %pre.light-well :preserve cd existing_repo + git remote rename origin old-origin git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git push -u origin --all git push -u origin --tags -- cgit v1.2.1 From c44dff9984d4ee055a40b9c3354888193b3d5f57 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Fri, 20 Oct 2017 13:28:30 +0100 Subject: Remove page-specific GLForm init and add support_autocomplete: false local to groups/milestones/_form --- app/assets/javascripts/dispatcher.js | 1 - app/views/groups/milestones/_form.html.haml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 2885923aeda..eb576672d25 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -234,7 +234,6 @@ import DueDateSelectors from './due_date_select'; case 'groups:milestones:update': new ZenMode(); new DueDateSelectors(); - new GLForm($('.milestone-form'), true); break; case 'projects:compare:show': new gl.Diff(); diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml index cc879e5a308..a1be0d3220a 100644 --- a/app/views/groups/milestones/_form.html.haml +++ b/app/views/groups/milestones/_form.html.haml @@ -11,7 +11,7 @@ = f.label :description, "Description", class: "control-label" .col-sm-10 = render layout: 'projects/md_preview', locals: { url: group_preview_markdown_path } do - = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...', supports_autocomplete: false .clearfix .error-alert -- cgit v1.2.1 From 6d04f3789cc16f7211fb3d465956bbd84c9430b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Thu, 19 Oct 2017 15:57:20 -0300 Subject: Avoid calling underlying methods on non-existing repos This saves us Rugged/gRPC invocations --- app/models/repository.rb | 9 +++++++-- spec/models/repository_spec.rb | 34 ++++++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 4324ea46aac..8a1b81b5337 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1031,6 +1031,10 @@ class Repository if instance_variable_defined?(ivar) instance_variable_get(ivar) else + # If the repository doesn't exist and a fallback was specified we return + # that value inmediately. This saves us Rugged/gRPC invocations. + return fallback unless fallback.nil? || exists? + begin value = if memoize_only @@ -1040,8 +1044,9 @@ class Repository end instance_variable_set(ivar, value) rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository - # if e.g. HEAD or the entire repository doesn't exist we want to - # gracefully handle this and not cache anything. + # Even if the above `#exists?` check passes these errors might still + # occur (for example because of a non-existing HEAD). We want to + # gracefully handle this and not cache anything fallback end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 39d188f18af..874368ada67 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -2110,19 +2110,41 @@ describe Repository do end describe '#cache_method_output', :use_clean_rails_memory_store_caching do + let(:fallback) { 10 } + context 'with a non-existing repository' do - let(:value) do - repository.cache_method_output(:cats, fallback: 10) do - raise Rugged::ReferenceError + let(:project) { create(:project) } # No repository + + subject do + repository.cache_method_output(:cats, fallback: fallback) do + repository.cats_call_stub end end - it 'returns a fallback value' do - expect(value).to eq(10) + it 'returns the fallback value' do + expect(subject).to eq(fallback) + end + + it 'avoids calling the original method' do + expect(repository).not_to receive(:cats_call_stub) + + subject + end + end + + context 'with a method throwing a non-existing-repository error' do + subject do + repository.cache_method_output(:cats, fallback: fallback) do + raise Gitlab::Git::Repository::NoRepository + end + end + + it 'returns the fallback value' do + expect(subject).to eq(fallback) end it 'does not cache the data' do - value + subject expect(repository.instance_variable_defined?(:@cats)).to eq(false) expect(repository.send(:cache).exist?(:cats)).to eq(false) -- cgit v1.2.1 From c8d29d17aef6ac4fd0620dc0d69df5ef454fd102 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Fri, 20 Oct 2017 17:11:31 +0100 Subject: Added group milestones form spec --- spec/features/groups/milestone_spec.rb | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb index 12aa54a3da1..1b41b3842c8 100644 --- a/spec/features/groups/milestone_spec.rb +++ b/spec/features/groups/milestone_spec.rb @@ -19,9 +19,9 @@ feature 'Group milestones', :js do end it 'renders description preview' do - form = find('.gfm-form') + description = find('.note-textarea') - form.fill_in(:milestone_description, with: '') + description.native.send_keys('') click_link('Preview') @@ -31,7 +31,7 @@ feature 'Group milestones', :js do click_link('Write') - form.fill_in(:milestone_description, with: ':+1: Nice') + description.native.send_keys(':+1: Nice') click_link('Preview') @@ -51,6 +51,13 @@ feature 'Group milestones', :js do expect(find('.start_date')).to have_content(Date.today.at_beginning_of_month.strftime('%b %-d, %Y')) end + + it 'description input does not support autocomplete' do + description = find('.note-textarea') + description.native.send_keys('!') + + expect(page).not_to have_selector('.atwho-view') + end end context 'milestones list' do -- cgit v1.2.1 From 24077144fc6dd9e08ad357ed6b9e72b9ed500fed Mon Sep 17 00:00:00 2001 From: Herman van Rink Date: Fri, 20 Oct 2017 19:42:01 +0200 Subject: Add changelog entry --- changelogs/unreleased/14970-suggest-rename-remote.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/14970-suggest-rename-remote.yml diff --git a/changelogs/unreleased/14970-suggest-rename-remote.yml b/changelogs/unreleased/14970-suggest-rename-remote.yml new file mode 100644 index 00000000000..68a77eb446d --- /dev/null +++ b/changelogs/unreleased/14970-suggest-rename-remote.yml @@ -0,0 +1,5 @@ +--- +title: Suggest to rename the remote for existing repository instructions +merge_request: 14970 +author: helmo42 +type: added -- cgit v1.2.1 From 70d9401acd546246feef1296f79415c581a31fda Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Fri, 20 Oct 2017 15:57:24 -0700 Subject: Replace all instances of new-sidebar with contextual-sidebar --- app/assets/stylesheets/framework.scss | 2 +- .../stylesheets/framework/contextual-sidebar.scss | 506 ++++++++++++++++++++ app/assets/stylesheets/framework/new-sidebar.scss | 510 --------------------- app/assets/stylesheets/pages/boards.scss | 2 +- app/helpers/nav_helper.rb | 2 +- 5 files changed, 509 insertions(+), 513 deletions(-) create mode 100644 app/assets/stylesheets/framework/contextual-sidebar.scss delete mode 100644 app/assets/stylesheets/framework/new-sidebar.scss diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index aa61ddc6a2c..1aab985d9e3 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -36,7 +36,7 @@ @import "framework/secondary-navigation-elements"; @import "framework/selects"; @import "framework/sidebar"; -@import "framework/new-sidebar"; +@import "framework/contextual-sidebar"; @import "framework/tables"; @import "framework/notes"; @import "framework/tabs"; diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss new file mode 100644 index 00000000000..ff667a1f927 --- /dev/null +++ b/app/assets/stylesheets/framework/contextual-sidebar.scss @@ -0,0 +1,506 @@ +$active-background: rgba(0, 0, 0, .04); +$active-hover-background: $active-background; +$active-hover-color: $gl-text-color; +$inactive-badge-background: rgba(0, 0, 0, .08); +$hover-background: rgba(0, 0, 0, .06); +$hover-color: $gl-text-color; +$inactive-color: $gl-text-color-secondary; +$contextual-sidebar-width: 220px; +$contextual-sidebar-collapsed-width: 50px; + +.page-with-contextual-sidebar { + @media (min-width: $screen-md-min) { + padding-left: $contextual-sidebar-collapsed-width; + } + + @media (min-width: $screen-lg-min) { + padding-left: $contextual-sidebar-width; + } + + // Override position: absolute + .right-sidebar { + position: fixed; + height: calc(100% - #{$header-height}); + } + + .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header { + padding: 10px 0 15px; + } +} + +.page-with-icon-sidebar { + @media (min-width: $screen-sm-min) { + padding-left: $contextual-sidebar-collapsed-width; + } +} + +.context-header { + position: relative; + margin-right: 2px; + + a { + font-weight: $gl-font-weight-bold; + display: flex; + align-items: center; + padding: 10px 16px 10px 10px; + color: $gl-text-color; + } + + &:hover, + a:hover { + background-color: $hover-background; + color: $hover-color; + + .settings-avatar { + svg { + fill: $hover-color; + } + } + } + + .avatar-container { + flex: 0 0 40px; + background-color: $white-light; + } + + .sidebar-context-title { + overflow: hidden; + text-overflow: ellipsis; + } +} + +.settings-avatar { + background-color: $white-light; + + svg { + fill: $gl-text-color-secondary; + margin: auto; + } +} + +.nav-sidebar { + position: fixed; + z-index: 400; + width: $contextual-sidebar-width; + transition: left $sidebar-transition-duration; + top: $header-height; + bottom: 0; + left: 0; + background-color: $gray-light; + box-shadow: inset -2px 0 0 $border-color; + transform: translate3d(0, 0, 0); + + &:not(.sidebar-icons-only) { + @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { + box-shadow: inset -2px 0 0 $border-color, + 2px 1px 3px $dropdown-shadow-color; + } + } + + &.sidebar-icons-only { + width: auto; + min-width: $contextual-sidebar-collapsed-width; + + .nav-sidebar-inner-scroll { + overflow-x: hidden; + } + + .badge:not(.fly-out-badge), + .sidebar-context-title, + .nav-item-name { + display: none; + } + + .sidebar-top-level-items > li > a { + min-height: 44px; + } + + .fly-out-top-item { + display: block; + } + + .avatar-container { + margin-right: 0; + } + } + + &.nav-sidebar-expanded { + left: 0; + } + + a { + transition: none; + text-decoration: none; + } + + ul { + padding-left: 0; + list-style: none; + } + + li { + white-space: nowrap; + + a { + display: flex; + align-items: center; + padding: 12px 16px; + color: $inactive-color; + } + + svg { + fill: $inactive-color; + } + } + + .nav-item-name { + flex: 1; + } + + li.active { + > a { + font-weight: $gl-font-weight-bold; + } + } + + @media (max-width: $screen-xs-max) { + left: (-$contextual-sidebar-width); + } + + .nav-icon-container { + display: flex; + margin-right: 8px; + } + + .fly-out-top-item { + display: none; + } + + svg { + height: 16px; + width: 16px; + } +} + +.nav-sidebar-inner-scroll { + height: 100%; + width: 100%; + overflow: auto; + + @media (min-width: $screen-sm-min) { + overflow: hidden; + } +} + +.with-performance-bar .nav-sidebar { + top: $header-height + $performance-bar-height; +} + +.sidebar-sub-level-items { + display: none; + padding-bottom: 8px; + + > li { + a { + padding: 8px 16px 8px 40px; + + &:hover, + &:focus { + background: $active-hover-background; + color: $active-hover-color; + } + } + + &.active { + a { + &, + &:hover, + &:focus { + background: $active-background; + } + } + } + } +} + +.sidebar-top-level-items { + margin-bottom: 60px; + + > li { + > a { + @media (min-width: $screen-sm-min) { + margin-right: 2px; + } + + &:hover { + color: $gl-text-color; + + svg { + fill: $gl-text-color; + } + } + } + + &.is-showing-fly-out { + > a { + margin-right: 2px; + } + + .sidebar-sub-level-items { + @media (min-width: $screen-sm-min) { + position: fixed; + top: 0; + left: 0; + min-width: 150px; + margin-top: -1px; + padding: 4px 1px; + background-color: $white-light; + box-shadow: 2px 1px 3px $dropdown-shadow-color; + border: 1px solid $gray-darker; + border-left: 0; + border-radius: 0 3px 3px 0; + + &::before { + content: ""; + position: absolute; + top: -30px; + bottom: -30px; + left: -10px; + right: -30px; + z-index: -1; + } + + &.is-above { + margin-top: 1px; + } + + .divider { + height: 1px; + margin: 4px -1px; + padding: 0; + background-color: $dropdown-divider-color; + } + + > .active { + box-shadow: none; + + > a { + background-color: transparent; + } + } + + a { + padding: 8px 16px; + color: $gl-text-color; + + &:hover, + &:focus { + background-color: $gray-darker; + } + } + } + } + } + + .badge { + background-color: $inactive-badge-background; + color: $inactive-color; + } + + &.active { + background: $active-background; + + > a { + margin-left: 4px; + padding-left: 12px; + } + + .badge { + font-weight: $gl-font-weight-bold; + } + + .sidebar-sub-level-items:not(.is-fly-out-only) { + display: block; + } + } + + &.active > a:hover, + &.is-over > a { + background-color: $hover-background; + } + } +} + + +// Collapsed nav + +.toggle-sidebar-button, +.close-nav-button { + width: $contextual-sidebar-width - 2px; + position: fixed; + bottom: 0; + padding: 16px; + background-color: $gray-light; + border: 0; + border-top: 2px solid $border-color; + color: $gl-text-color-secondary; + display: flex; + align-items: center; + + svg { + fill: $gl-text-color-secondary; + margin-right: 8px; + } + + .icon-angle-double-right { + display: none; + } + + &:hover { + background-color: $border-color; + color: $gl-text-color; + + svg { + fill: $gl-text-color; + } + } +} + +.toggle-sidebar-button { + @media (max-width: $screen-xs-max) { + display: none; + } +} + + +.sidebar-icons-only { + .context-header { + height: 61px; + + a { + padding: 10px 4px; + } + } + + li a { + padding: 12px 15px; + } + + .sidebar-top-level-items > li { + &.active a { + padding-left: 12px; + } + + .sidebar-sub-level-items { + &:not(.flyout-list) { + display: none; + } + } + } + + .nav-icon-container { + margin-right: 0; + } + + .toggle-sidebar-button { + width: $contextual-sidebar-collapsed-width - 2px; + padding: 16px; + + .collapse-text, + .icon-angle-double-left { + display: none; + } + + .icon-angle-double-right { + display: block; + margin: 0; + } + } +} + +.fly-out-top-item { + > a { + display: flex; + } + + .fly-out-badge { + margin-left: 8px; + } +} + +.fly-out-top-item-name { + flex: 1; +} + +// Mobile nav + +.close-nav-button { + display: none; +} + +.toggle-mobile-nav { + display: none; + background-color: transparent; + border: 0; + padding: 6px 16px; + margin: 0 0 0 -15px; + height: 46px; + + i { + font-size: 20px; + color: $gl-text-color-secondary; + } + + @media (max-width: $screen-xs-max) { + display: flex; + align-items: center; + + i { + font-size: 18px; + } + } + + @media (max-width: $screen-xs-max) { + + .breadcrumbs-links { + padding-left: $gl-padding; + border-left: 1px solid $gl-text-color-quaternary; + } + } +} + +@media (max-width: $screen-xs-max) { + .close-nav-button { + display: flex; + } +} + +.mobile-overlay { + display: none; + + &.mobile-nav-open { + display: block; + position: fixed; + background-color: $black-transparent; + height: 100%; + width: 100%; + z-index: 300; + } +} + + +// Make issue boards full-height now that sub-nav is gone + +.boards-list { + height: calc(100vh - #{$header-height}); + + @media (min-width: $screen-sm-min) { + height: 475px; // Needed for PhantomJS + // scss-lint:disable DuplicateProperty + height: calc(100vh - 180px); + // scss-lint:enable DuplicateProperty + } +} + +.with-performance-bar .boards-list { + height: calc(100vh - #{$header-height} - #{$performance-bar-height}); +} diff --git a/app/assets/stylesheets/framework/new-sidebar.scss b/app/assets/stylesheets/framework/new-sidebar.scss deleted file mode 100644 index 7a309f2c8a1..00000000000 --- a/app/assets/stylesheets/framework/new-sidebar.scss +++ /dev/null @@ -1,510 +0,0 @@ -@import "framework/variables"; -@import 'framework/tw_bootstrap_variables'; -@import "bootstrap/variables"; - -$active-background: rgba(0, 0, 0, .04); -$active-hover-background: $active-background; -$active-hover-color: $gl-text-color; -$inactive-badge-background: rgba(0, 0, 0, .08); -$hover-background: rgba(0, 0, 0, .06); -$hover-color: $gl-text-color; -$inactive-color: $gl-text-color-secondary; -$new-sidebar-width: 220px; -$new-sidebar-collapsed-width: 50px; - -.page-with-new-sidebar { - @media (min-width: $screen-md-min) { - padding-left: $new-sidebar-collapsed-width; - } - - @media (min-width: $screen-lg-min) { - padding-left: $new-sidebar-width; - } - - // Override position: absolute - .right-sidebar { - position: fixed; - height: calc(100% - #{$header-height}); - } - - .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header { - padding: 10px 0 15px; - } -} - -.page-with-icon-sidebar { - @media (min-width: $screen-sm-min) { - padding-left: $new-sidebar-collapsed-width; - } -} - -.context-header { - position: relative; - margin-right: 2px; - - a { - font-weight: $gl-font-weight-bold; - display: flex; - align-items: center; - padding: 10px 16px 10px 10px; - color: $gl-text-color; - } - - &:hover, - a:hover { - background-color: $hover-background; - color: $hover-color; - - .settings-avatar { - svg { - fill: $hover-color; - } - } - } - - .avatar-container { - flex: 0 0 40px; - background-color: $white-light; - } - - .sidebar-context-title { - overflow: hidden; - text-overflow: ellipsis; - } -} - -.settings-avatar { - background-color: $white-light; - - svg { - fill: $gl-text-color-secondary; - margin: auto; - } -} - -.nav-sidebar { - position: fixed; - z-index: 400; - width: $new-sidebar-width; - transition: left $sidebar-transition-duration; - top: $header-height; - bottom: 0; - left: 0; - background-color: $gray-light; - box-shadow: inset -2px 0 0 $border-color; - transform: translate3d(0, 0, 0); - - &:not(.sidebar-icons-only) { - @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { - box-shadow: inset -2px 0 0 $border-color, - 2px 1px 3px $dropdown-shadow-color; - } - } - - &.sidebar-icons-only { - width: auto; - min-width: $new-sidebar-collapsed-width; - - .nav-sidebar-inner-scroll { - overflow-x: hidden; - } - - .badge:not(.fly-out-badge), - .sidebar-context-title, - .nav-item-name { - display: none; - } - - .sidebar-top-level-items > li > a { - min-height: 44px; - } - - .fly-out-top-item { - display: block; - } - - .avatar-container { - margin-right: 0; - } - } - - &.nav-sidebar-expanded { - left: 0; - } - - a { - transition: none; - text-decoration: none; - } - - ul { - padding-left: 0; - list-style: none; - } - - li { - white-space: nowrap; - - a { - display: flex; - align-items: center; - padding: 12px 16px; - color: $inactive-color; - } - - svg { - fill: $inactive-color; - } - } - - .nav-item-name { - flex: 1; - } - - li.active { - > a { - font-weight: $gl-font-weight-bold; - } - } - - @media (max-width: $screen-xs-max) { - left: (-$new-sidebar-width); - } - - .nav-icon-container { - display: flex; - margin-right: 8px; - } - - .fly-out-top-item { - display: none; - } - - svg { - height: 16px; - width: 16px; - } -} - -.nav-sidebar-inner-scroll { - height: 100%; - width: 100%; - overflow: auto; - - @media (min-width: $screen-sm-min) { - overflow: hidden; - } -} - -.with-performance-bar .nav-sidebar { - top: $header-height + $performance-bar-height; -} - -.sidebar-sub-level-items { - display: none; - padding-bottom: 8px; - - > li { - a { - padding: 8px 16px 8px 40px; - - &:hover, - &:focus { - background: $active-hover-background; - color: $active-hover-color; - } - } - - &.active { - a { - &, - &:hover, - &:focus { - background: $active-background; - } - } - } - } -} - -.sidebar-top-level-items { - margin-bottom: 60px; - - > li { - > a { - @media (min-width: $screen-sm-min) { - margin-right: 2px; - } - - &:hover { - color: $gl-text-color; - - svg { - fill: $gl-text-color; - } - } - } - - &.is-showing-fly-out { - > a { - margin-right: 2px; - } - - .sidebar-sub-level-items { - @media (min-width: $screen-sm-min) { - position: fixed; - top: 0; - left: 0; - min-width: 150px; - margin-top: -1px; - padding: 4px 1px; - background-color: $white-light; - box-shadow: 2px 1px 3px $dropdown-shadow-color; - border: 1px solid $gray-darker; - border-left: 0; - border-radius: 0 3px 3px 0; - - &::before { - content: ""; - position: absolute; - top: -30px; - bottom: -30px; - left: -10px; - right: -30px; - z-index: -1; - } - - &.is-above { - margin-top: 1px; - } - - .divider { - height: 1px; - margin: 4px -1px; - padding: 0; - background-color: $dropdown-divider-color; - } - - > .active { - box-shadow: none; - - > a { - background-color: transparent; - } - } - - a { - padding: 8px 16px; - color: $gl-text-color; - - &:hover, - &:focus { - background-color: $gray-darker; - } - } - } - } - } - - .badge { - background-color: $inactive-badge-background; - color: $inactive-color; - } - - &.active { - background: $active-background; - - > a { - margin-left: 4px; - padding-left: 12px; - } - - .badge { - font-weight: $gl-font-weight-bold; - } - - .sidebar-sub-level-items:not(.is-fly-out-only) { - display: block; - } - } - - &.active > a:hover, - &.is-over > a { - background-color: $hover-background; - } - } -} - - -// Collapsed nav - -.toggle-sidebar-button, -.close-nav-button { - width: $new-sidebar-width - 2px; - position: fixed; - bottom: 0; - padding: 16px; - background-color: $gray-light; - border: 0; - border-top: 2px solid $border-color; - color: $gl-text-color-secondary; - display: flex; - align-items: center; - - svg { - fill: $gl-text-color-secondary; - margin-right: 8px; - } - - .icon-angle-double-right { - display: none; - } - - &:hover { - background-color: $border-color; - color: $gl-text-color; - - svg { - fill: $gl-text-color; - } - } -} - -.toggle-sidebar-button { - @media (max-width: $screen-xs-max) { - display: none; - } -} - - -.sidebar-icons-only { - .context-header { - height: 61px; - - a { - padding: 10px 4px; - } - } - - li a { - padding: 12px 15px; - } - - .sidebar-top-level-items > li { - &.active a { - padding-left: 12px; - } - - .sidebar-sub-level-items { - &:not(.flyout-list) { - display: none; - } - } - } - - .nav-icon-container { - margin-right: 0; - } - - .toggle-sidebar-button { - width: $new-sidebar-collapsed-width - 2px; - padding: 16px; - - .collapse-text, - .icon-angle-double-left { - display: none; - } - - .icon-angle-double-right { - display: block; - margin: 0; - } - } -} - -.fly-out-top-item { - > a { - display: flex; - } - - .fly-out-badge { - margin-left: 8px; - } -} - -.fly-out-top-item-name { - flex: 1; -} - -// Mobile nav - -.close-nav-button { - display: none; -} - -.toggle-mobile-nav { - display: none; - background-color: transparent; - border: 0; - padding: 6px 16px; - margin: 0 0 0 -15px; - height: 46px; - - i { - font-size: 20px; - color: $gl-text-color-secondary; - } - - @media (max-width: $screen-xs-max) { - display: flex; - align-items: center; - - i { - font-size: 18px; - } - } - - @media (max-width: $screen-xs-max) { - + .breadcrumbs-links { - padding-left: $gl-padding; - border-left: 1px solid $gl-text-color-quaternary; - } - } -} - -@media (max-width: $screen-xs-max) { - .close-nav-button { - display: flex; - } -} - -.mobile-overlay { - display: none; - - &.mobile-nav-open { - display: block; - position: fixed; - background-color: $black-transparent; - height: 100%; - width: 100%; - z-index: 300; - } -} - - -// Make issue boards full-height now that sub-nav is gone - -.boards-list { - height: calc(100vh - #{$header-height}); - - @media (min-width: $screen-sm-min) { - height: 475px; // Needed for PhantomJS - // scss-lint:disable DuplicateProperty - height: calc(100vh - 180px); - // scss-lint:enable DuplicateProperty - } -} - -.with-performance-bar .boards-list { - height: calc(100vh - #{$header-height} - #{$performance-bar-height}); -} diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index ca61f7a30c3..91296b354a7 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -414,7 +414,7 @@ margin: 5px; } -.page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar { +.page-with-contextual-sidebar.page-with-sidebar .issue-boards-sidebar { .issuable-sidebar-header { position: relative; } diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index a23a43c9f43..5a74511afa7 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -1,7 +1,7 @@ module NavHelper def page_with_sidebar_class class_name = page_gutter_class - class_name << 'page-with-new-sidebar' if defined?(@left_sidebar) && @left_sidebar + class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar class_name -- cgit v1.2.1 From 33380f811e85b71f95a18e4a200b0cf09d452928 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Fri, 20 Oct 2017 16:07:48 -0700 Subject: Remove variables from sidebar CSS --- .../stylesheets/framework/contextual-sidebar.scss | 32 ++++++++-------------- app/assets/stylesheets/framework/variables.scss | 10 ++++++- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss index ff667a1f927..fa5d3833f3e 100644 --- a/app/assets/stylesheets/framework/contextual-sidebar.scss +++ b/app/assets/stylesheets/framework/contextual-sidebar.scss @@ -1,13 +1,3 @@ -$active-background: rgba(0, 0, 0, .04); -$active-hover-background: $active-background; -$active-hover-color: $gl-text-color; -$inactive-badge-background: rgba(0, 0, 0, .08); -$hover-background: rgba(0, 0, 0, .06); -$hover-color: $gl-text-color; -$inactive-color: $gl-text-color-secondary; -$contextual-sidebar-width: 220px; -$contextual-sidebar-collapsed-width: 50px; - .page-with-contextual-sidebar { @media (min-width: $screen-md-min) { padding-left: $contextual-sidebar-collapsed-width; @@ -48,12 +38,12 @@ $contextual-sidebar-collapsed-width: 50px; &:hover, a:hover { - background-color: $hover-background; - color: $hover-color; + background-color: $link-hover-background; + color: $gl-text-color; .settings-avatar { svg { - fill: $hover-color; + fill: $gl-text-color; } } } @@ -145,11 +135,11 @@ $contextual-sidebar-collapsed-width: 50px; display: flex; align-items: center; padding: 12px 16px; - color: $inactive-color; + color: $gl-text-color-secondary; } svg { - fill: $inactive-color; + fill: $gl-text-color-secondary; } } @@ -206,8 +196,8 @@ $contextual-sidebar-collapsed-width: 50px; &:hover, &:focus { - background: $active-hover-background; - color: $active-hover-color; + background: $link-active-background; + color: $gl-text-color; } } @@ -216,7 +206,7 @@ $contextual-sidebar-collapsed-width: 50px; &, &:hover, &:focus { - background: $active-background; + background: $link-active-background; } } } @@ -304,11 +294,11 @@ $contextual-sidebar-collapsed-width: 50px; .badge { background-color: $inactive-badge-background; - color: $inactive-color; + color: $gl-text-color-secondary; } &.active { - background: $active-background; + background: $link-active-background; > a { margin-left: 4px; @@ -326,7 +316,7 @@ $contextual-sidebar-collapsed-width: 50px; &.active > a:hover, &.is-over > a { - background-color: $hover-background; + background-color: $link-hover-background; } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d5ca23ff870..8ab48e4844f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -9,6 +9,8 @@ $sidebar-transition-duration: .15s; $sidebar-breakpoint: 1024px; $default-transition-duration: .15s; $right-sidebar-transition-duration: .3s; +$contextual-sidebar-width: 220px; +$contextual-sidebar-collapsed-width: 50px; /* * Color schema @@ -358,6 +360,13 @@ $dropdown-item-hover-bg: $gray-darker; $filtered-search-term-shadow-color: rgba(0, 0, 0, 0.09); $dropdown-hover-color: $blue-400; +/* +* Contextual Sidebar +*/ +$link-active-background: rgba(0, 0, 0, .04); +$link-hover-background: rgba(0, 0, 0, .06); +$inactive-badge-background: rgba(0, 0, 0, .08); + /* * Buttons */ @@ -404,7 +413,6 @@ $note-targe3-inside: #ffffd3; $note-line2-border: #ddd; $note-icon-gutter-width: 55px; - /* * Zen */ -- cgit v1.2.1 From 58af2647dc6a5bd7805e703a32fe45190d62c7d3 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Fri, 20 Oct 2017 16:14:55 -0700 Subject: Rename new_sidebar in JS --- app/assets/javascripts/contextual_sidebar.js | 81 ++++++++++++++++++++++++++++ app/assets/javascripts/layout_nav.js | 6 +-- app/assets/javascripts/new_sidebar.js | 81 ---------------------------- 3 files changed, 84 insertions(+), 84 deletions(-) create mode 100644 app/assets/javascripts/contextual_sidebar.js delete mode 100644 app/assets/javascripts/new_sidebar.js diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js new file mode 100644 index 00000000000..46b68ebe158 --- /dev/null +++ b/app/assets/javascripts/contextual_sidebar.js @@ -0,0 +1,81 @@ +import Cookies from 'js-cookie'; +import _ from 'underscore'; +import bp from './breakpoints'; + +export default class ContextualSidebar { + constructor() { + this.initDomElements(); + this.render(); + } + + initDomElements() { + this.$page = $('.page-with-sidebar'); + this.$sidebar = $('.nav-sidebar'); + this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar); + this.$overlay = $('.mobile-overlay'); + this.$openSidebar = $('.toggle-mobile-nav'); + this.$closeSidebar = $('.close-nav-button'); + this.$sidebarToggle = $('.js-toggle-sidebar'); + } + + bindEvents() { + document.addEventListener('click', (e) => { + if (!e.target.closest('.nav-sidebar') && (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')) { + this.toggleCollapsedSidebar(true); + } + }); + this.$openSidebar.on('click', () => this.toggleSidebarNav(true)); + this.$closeSidebar.on('click', () => this.toggleSidebarNav(false)); + this.$overlay.on('click', () => this.toggleSidebarNav(false)); + this.$sidebarToggle.on('click', () => { + const value = !this.$sidebar.hasClass('sidebar-icons-only'); + this.toggleCollapsedSidebar(value); + }); + + $(window).on('resize', () => _.debounce(this.render(), 100)); + } + + static setCollapsedCookie(value) { + if (bp.getBreakpointSize() !== 'lg') { + return; + } + Cookies.set('sidebar_collapsed', value, { expires: 365 * 10 }); + } + + toggleSidebarNav(show) { + this.$sidebar.toggleClass('nav-sidebar-expanded', show); + this.$overlay.toggleClass('mobile-nav-open', show); + this.$sidebar.removeClass('sidebar-icons-only'); + } + + toggleCollapsedSidebar(collapsed) { + const breakpoint = bp.getBreakpointSize(); + + if (this.$sidebar.length) { + this.$sidebar.toggleClass('sidebar-icons-only', collapsed); + this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); + } + ContextualSidebar.setCollapsedCookie(collapsed); + + this.toggleSidebarOverflow(); + } + + toggleSidebarOverflow() { + if (this.$innerScroll.prop('scrollHeight') > this.$innerScroll.prop('offsetHeight')) { + this.$innerScroll.css('overflow-y', 'scroll'); + } else { + this.$innerScroll.css('overflow-y', ''); + } + } + + render() { + const breakpoint = bp.getBreakpointSize(); + + if (breakpoint === 'sm' || breakpoint === 'md') { + this.toggleCollapsedSidebar(true); + } else if (breakpoint === 'lg') { + const collapse = Cookies.get('sidebar_collapsed') === 'true'; + this.toggleCollapsedSidebar(collapse); + } + } +} diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index d064a2c0024..a6f82b247e2 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */ import _ from 'underscore'; import Cookies from 'js-cookie'; -import NewNavSidebar from './new_sidebar'; +import ContextualSidebar from './contextual_sidebar'; import initFlyOutNav from './fly_out_nav'; (function() { @@ -51,8 +51,8 @@ import initFlyOutNav from './fly_out_nav'; }); $(() => { - const newNavSidebar = new NewNavSidebar(); - newNavSidebar.bindEvents(); + const contextualSidebar = new ContextualSidebar(); + contextualSidebar.bindEvents(); initFlyOutNav(); }); diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js deleted file mode 100644 index 997550b37fb..00000000000 --- a/app/assets/javascripts/new_sidebar.js +++ /dev/null @@ -1,81 +0,0 @@ -import Cookies from 'js-cookie'; -import _ from 'underscore'; -import bp from './breakpoints'; - -export default class NewNavSidebar { - constructor() { - this.initDomElements(); - this.render(); - } - - initDomElements() { - this.$page = $('.page-with-sidebar'); - this.$sidebar = $('.nav-sidebar'); - this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar); - this.$overlay = $('.mobile-overlay'); - this.$openSidebar = $('.toggle-mobile-nav'); - this.$closeSidebar = $('.close-nav-button'); - this.$sidebarToggle = $('.js-toggle-sidebar'); - } - - bindEvents() { - document.addEventListener('click', (e) => { - if (!e.target.closest('.nav-sidebar') && (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')) { - this.toggleCollapsedSidebar(true); - } - }); - this.$openSidebar.on('click', () => this.toggleSidebarNav(true)); - this.$closeSidebar.on('click', () => this.toggleSidebarNav(false)); - this.$overlay.on('click', () => this.toggleSidebarNav(false)); - this.$sidebarToggle.on('click', () => { - const value = !this.$sidebar.hasClass('sidebar-icons-only'); - this.toggleCollapsedSidebar(value); - }); - - $(window).on('resize', () => _.debounce(this.render(), 100)); - } - - static setCollapsedCookie(value) { - if (bp.getBreakpointSize() !== 'lg') { - return; - } - Cookies.set('sidebar_collapsed', value, { expires: 365 * 10 }); - } - - toggleSidebarNav(show) { - this.$sidebar.toggleClass('nav-sidebar-expanded', show); - this.$overlay.toggleClass('mobile-nav-open', show); - this.$sidebar.removeClass('sidebar-icons-only'); - } - - toggleCollapsedSidebar(collapsed) { - const breakpoint = bp.getBreakpointSize(); - - if (this.$sidebar.length) { - this.$sidebar.toggleClass('sidebar-icons-only', collapsed); - this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); - } - NewNavSidebar.setCollapsedCookie(collapsed); - - this.toggleSidebarOverflow(); - } - - toggleSidebarOverflow() { - if (this.$innerScroll.prop('scrollHeight') > this.$innerScroll.prop('offsetHeight')) { - this.$innerScroll.css('overflow-y', 'scroll'); - } else { - this.$innerScroll.css('overflow-y', ''); - } - } - - render() { - const breakpoint = bp.getBreakpointSize(); - - if (breakpoint === 'sm' || breakpoint === 'md') { - this.toggleCollapsedSidebar(true); - } else if (breakpoint === 'lg') { - const collapse = Cookies.get('sidebar_collapsed') === 'true'; - this.toggleCollapsedSidebar(collapse); - } - } -} -- cgit v1.2.1 From 188e860804285f6d54140df54d8fa62a15f99dc9 Mon Sep 17 00:00:00 2001 From: Guilherme Vieira Date: Fri, 20 Oct 2017 21:49:18 -0200 Subject: Hides pipeline duration in commit box when it is zero (nil) --- app/views/projects/commit/_commit_box.html.haml | 5 +++-- changelogs/unreleased/hide-pipeline-zero-duration.yml | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/hide-pipeline-zero-duration.yml diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 09bcd187e59..ff17372fdd9 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -77,5 +77,6 @@ #{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) } .mr-widget-pipeline-graph = render 'shared/mini_pipeline_graph', pipeline: last_pipeline, klass: 'js-commit-pipeline-graph' - in - = time_interval_in_words last_pipeline.duration + - if last_pipeline.duration + in + = time_interval_in_words last_pipeline.duration diff --git a/changelogs/unreleased/hide-pipeline-zero-duration.yml b/changelogs/unreleased/hide-pipeline-zero-duration.yml new file mode 100644 index 00000000000..5d7a0983537 --- /dev/null +++ b/changelogs/unreleased/hide-pipeline-zero-duration.yml @@ -0,0 +1,5 @@ +--- +title: Hides pipeline duration in commit box when it is zero (nil) +merge_request: 14979 +author: gvieira37 +type: fixed -- cgit v1.2.1 From 8bb17358b535fd288edb46bee389acf481a5e2f9 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sat, 21 Oct 2017 20:52:17 +0300 Subject: Update docs on creating MRs --- doc/gitlab-basics/add-merge-request.md | 23 +++++++++------------ doc/gitlab-basics/img/merge_request_new.png | Bin 2234 -> 0 bytes .../img/merge_request_select_branch.png | Bin 20332 -> 16668 bytes doc/gitlab-basics/img/project_navbar.png | Bin 3259 -> 0 bytes 4 files changed, 10 insertions(+), 13 deletions(-) delete mode 100644 doc/gitlab-basics/img/merge_request_new.png delete mode 100644 doc/gitlab-basics/img/project_navbar.png diff --git a/doc/gitlab-basics/add-merge-request.md b/doc/gitlab-basics/add-merge-request.md index bf01fe51dc3..5cc014419ad 100644 --- a/doc/gitlab-basics/add-merge-request.md +++ b/doc/gitlab-basics/add-merge-request.md @@ -3,31 +3,28 @@ Merge requests are useful to integrate separate changes that you've made to a project, on different branches. This is a brief guide on how to create a merge request. For more information, check the -[merge requests documentation](../user/project/merge_requests.md). +[merge requests documentation](../user/project/merge_requests/index.md). --- 1. Before you start, you should have already [created a branch](create-branch.md) and [pushed your changes](basic-git-commands.md) to GitLab. - -1. You can then go to the project where you'd like to merge your changes and - click on the **Merge requests** tab. - - ![Merge requests](img/project_navbar.png) - +1. Go to the project where you'd like to merge your changes and click on the + **Merge requests** tab. 1. Click on **New merge request** on the right side of the screen. - - ![New Merge Request](img/merge_request_new.png) - -1. Select a source branch and click on the **Compare branches and continue** button. +1. From there on, you have the option to select the source branch and the target + branch you'd like to compare to. The default target project is the upstream + repository, but you can choose to compare across any of its forks. ![Select a branch](img/merge_request_select_branch.png) +1. When ready, click on the **Compare branches and continue** button. 1. At a minimum, add a title and a description to your merge request. Optionally, select a user to review your merge request and to accept or close it. You may also select a milestone and labels. ![New merge request page](img/merge_request_page.png) -1. When ready, click on the **Submit merge request** button. Your merge request - will be ready to be approved and published. +1. When ready, click on the **Submit merge request** button. + +Your merge request will be ready to be approved and merged. diff --git a/doc/gitlab-basics/img/merge_request_new.png b/doc/gitlab-basics/img/merge_request_new.png deleted file mode 100644 index 6fcd7bebada..00000000000 Binary files a/doc/gitlab-basics/img/merge_request_new.png and /dev/null differ diff --git a/doc/gitlab-basics/img/merge_request_select_branch.png b/doc/gitlab-basics/img/merge_request_select_branch.png index 9f6b93943a9..57ea0e65f34 100644 Binary files a/doc/gitlab-basics/img/merge_request_select_branch.png and b/doc/gitlab-basics/img/merge_request_select_branch.png differ diff --git a/doc/gitlab-basics/img/project_navbar.png b/doc/gitlab-basics/img/project_navbar.png deleted file mode 100644 index be6f38ede32..00000000000 Binary files a/doc/gitlab-basics/img/project_navbar.png and /dev/null differ -- cgit v1.2.1 From 064eb397941ee0dbc1c478c62987505511c8272a Mon Sep 17 00:00:00 2001 From: Travis Miller Date: Tue, 29 Aug 2017 23:19:31 -0500 Subject: Add Changelog --- changelogs/unreleased/23000-pages-api.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/23000-pages-api.yml diff --git a/changelogs/unreleased/23000-pages-api.yml b/changelogs/unreleased/23000-pages-api.yml new file mode 100644 index 00000000000..9f6fa13dd07 --- /dev/null +++ b/changelogs/unreleased/23000-pages-api.yml @@ -0,0 +1,5 @@ +--- +title: Add API endpoints for Pages Domains +merge_request: 13917 +author: Travis Miller +type: added -- cgit v1.2.1 From 66a8f3d4330b27624569e61680f150fdc026a347 Mon Sep 17 00:00:00 2001 From: Travis Miller Date: Fri, 20 Oct 2017 01:33:52 -0500 Subject: Rename conflicting private method in PagesDomain model --- app/models/pages_domain.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 5d798247863..2e824cda525 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -16,9 +16,9 @@ class PagesDomain < ActiveRecord::Base key: Gitlab::Application.secrets.db_key_base, algorithm: 'aes-256-cbc' - after_create :update - after_save :update - after_destroy :update + after_create :update_daemon + after_save :update_daemon + after_destroy :update_daemon def to_param domain @@ -80,7 +80,7 @@ class PagesDomain < ActiveRecord::Base private - def update + def update_daemon ::Projects::UpdatePagesConfigurationService.new(project).execute end -- cgit v1.2.1 From 6e5f63acce8768637a774e82adc5add755b7c0ac Mon Sep 17 00:00:00 2001 From: Travis Miller Date: Mon, 21 Aug 2017 18:56:47 -0500 Subject: Add pages domains API documentation --- doc/api/README.md | 1 + doc/api/pages_domains.md | 170 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 doc/api/pages_domains.md diff --git a/doc/api/README.md b/doc/api/README.md index de0fe79b3d6..2f6bfd4e2ac 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -37,6 +37,7 @@ following locations: - [Notes](notes.md) (comments) - [Notification settings](notification_settings.md) - [Open source license templates](templates/licenses.md) +- [Pages Domains](pages_domains.md) - [Pipelines](pipelines.md) - [Pipeline Triggers](pipeline_triggers.md) - [Pipeline Schedules](pipeline_schedules.md) diff --git a/doc/api/pages_domains.md b/doc/api/pages_domains.md new file mode 100644 index 00000000000..51962595e33 --- /dev/null +++ b/doc/api/pages_domains.md @@ -0,0 +1,170 @@ +# Pages domains API + +Endpoints for connecting custom domain(s) and TLS certificates in [GitLab Pages](https://about.gitlab.com/features/pages/). + +The GitLab Pages feature must be enabled to use these endpoints. Find out more about [administering](../administration/pages/index.md) and [using](../user/project/pages/index.md) the feature. + +## List pages domains + +Get a list of project pages domains. The user must have permissions to view pages domains. + +```http +GET /projects/:id/pages/domains +``` + +| Attribute | Type | Required | Description | +| --------- | -------------- | -------- | ---------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains +``` + +```json +[ + { + "domain": "www.domain.example", + "url": "http://www.domain.example" + }, + { + "domain": "ssl.domain.example", + "url": "https://ssl.domain.example", + "certificate": { + "subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate", + "expired": false, + "certificate": "-----BEGIN CERTIFICATE-----\n … \n-----END CERTIFICATE-----", + "certificate_text": "Certificate:\n … \n" + } + } +] +``` + +## Single pages domain + +Get a single project pages domain. The user must have permissions to view pages domains. + +```http +GET /projects/:id/pages/domains/:domain +``` + +| Attribute | Type | Required | Description | +| --------- | -------------- | -------- | ---------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `domain` | string | yes | The domain | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains/www.domain.example +``` + +```json +{ + "domain": "www.domain.example", + "url": "http://www.domain.example" +} +``` + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example +``` + +```json +{ + "domain": "ssl.domain.example", + "url": "https://ssl.domain.example", + "certificate": { + "subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate", + "expired": false, + "certificate": "-----BEGIN CERTIFICATE-----\n … \n-----END CERTIFICATE-----", + "certificate_text": "Certificate:\n … \n" + } +} +``` + +## Create new pages domain + +Creates a new pages domain. The user must have permissions to create new pages domains. + +```http +POST /projects/:id/pages/domains +``` + +| Attribute | Type | Required | Description | +| ------------- | -------------- | -------- | ---------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `domain` | string | yes | The domain | +| `certificate` | file/string | no | The certificate in PEM format with intermediates following in most specific to least specific order.| +| `key` | file/string | no | The certificate key in PEM format. | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="domain=ssl.domain.example" --form="certificate=@/path/to/cert.pem" --form="key=@/path/to/key.pem" https://gitlab.example.com/api/v4/projects/5/pages/domains +``` + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="domain=ssl.domain.example" --form="certificate=$CERT_PEM" --form="key=$KEY_PEM" https://gitlab.example.com/api/v4/projects/5/pages/domains +``` + +```json +{ + "domain": "ssl.domain.example", + "url": "https://ssl.domain.example", + "certificate": { + "subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate", + "expired": false, + "certificate": "-----BEGIN CERTIFICATE-----\n … \n-----END CERTIFICATE-----", + "certificate_text": "Certificate:\n … \n" + } +} +``` + +## Update pages domain + +Updates an existing project pages domain. The user must have permissions to change an existing pages domains. + +```http +PUT /projects/:id/pages/domains/:domain +``` + +| Attribute | Type | Required | Description | +| ------------- | -------------- | -------- | ---------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `domain` | string | yes | The domain | +| `certificate` | file/string | no | The certificate in PEM format with intermediates following in most specific to least specific order.| +| `key` | file/string | no | The certificate key in PEM format. | + +```bash +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="certificate=@/path/to/cert.pem" --form="key=@/path/to/key.pem" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example +``` + +```bash +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form="certificate=$CERT_PEM" --form="key=$KEY_PEM" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example +``` + +```json +{ + "domain": "ssl.domain.example", + "url": "https://ssl.domain.example", + "certificate": { + "subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate", + "expired": false, + "certificate": "-----BEGIN CERTIFICATE-----\n … \n-----END CERTIFICATE-----", + "certificate_text": "Certificate:\n … \n" + } +} +``` + +## Delete pages domain + +Deletes an existing project pages domain. + +```http +DELETE /projects/:id/pages/domains/:domain +``` + +| Attribute | Type | Required | Description | +| --------- | -------------- | -------- | ---------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `domain` | string | yes | The domain | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example +``` -- cgit v1.2.1 From de4d1bacc6ec0e5d6d8b3aad7617cd7880185044 Mon Sep 17 00:00:00 2001 From: Travis Miller Date: Mon, 21 Aug 2017 18:57:20 -0500 Subject: Add pages domains API schema --- .../api/schemas/public_api/v4/pages_domains.json | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 spec/fixtures/api/schemas/public_api/v4/pages_domains.json diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domains.json b/spec/fixtures/api/schemas/public_api/v4/pages_domains.json new file mode 100644 index 00000000000..0de1d0f1228 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/pages_domains.json @@ -0,0 +1,23 @@ +{ + "type": "array", + "items": { + "type": "object", + "properties": { + "domain": { "type": "string" }, + "url": { "type": "uri" }, + "certificate": { + "type": "object", + "properties": { + "subject": { "type": "string" }, + "expired": { "type": "boolean" }, + "certificate": { "type": "string" }, + "certificate_text": { "type": "string" } + }, + "required": ["subject", "expired"], + "additionalProperties": false + } + }, + "required": ["domain", "url"], + "additionalProperties": false + } +} -- cgit v1.2.1 From aca58784bd51679a3406a1d5bf9562b1ae16def7 Mon Sep 17 00:00:00 2001 From: Travis Miller Date: Mon, 21 Aug 2017 18:57:55 -0500 Subject: Add pages domains API tests --- spec/requests/api/pages_domains_spec.rb | 440 ++++++++++++++++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 spec/requests/api/pages_domains_spec.rb diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb new file mode 100644 index 00000000000..d13b3a958c9 --- /dev/null +++ b/spec/requests/api/pages_domains_spec.rb @@ -0,0 +1,440 @@ +require 'rails_helper' + +describe API::PagesDomains do + set(:project) { create(:project) } + set(:user) { create(:user) } + + set(:pages_domain) { create(:pages_domain, domain: 'www.domain.test', project: project) } + set(:pages_domain_secure) { create(:pages_domain, :with_certificate, :with_key, domain: 'ssl.domain.test', project: project) } + set(:pages_domain_expired) { create(:pages_domain, :with_expired_certificate, :with_key, domain: 'expired.domain.test', project: project) } + + let(:pages_domain_params) { build(:pages_domain, domain: 'www.other-domain.test').slice(:domain) } + let(:pages_domain_secure_params) { build(:pages_domain, :with_certificate, :with_key, domain: 'ssl.other-domain.test', project: project).slice(:domain, :certificate, :key) } + let(:pages_domain_secure_key_missmatch_params) {build(:pages_domain, :with_trusted_chain, :with_key, project: project).slice(:domain, :certificate, :key) } + let(:pages_domain_secure_missing_chain_params) {build(:pages_domain, :with_missing_chain, project: project).slice(:certificate) } + + let(:route) { "/projects/#{project.id}/pages/domains" } + let(:route_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain.domain}" } + let(:route_secure_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_secure.domain}" } + let(:route_expired_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_expired.domain}" } + let(:route_vacant_domain) { "/projects/#{project.id}/pages/domains/www.vacant-domain.test" } + + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + end + + describe 'GET /projects/:project_id/pages/domains' do + shared_examples_for 'get pages domains' do + it 'returns paginated pages domains' do + get api(route, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(3) + expect(json_response.map { |pages_domain| pages_domain['domain'] }).to include(pages_domain.domain) + expect(json_response.last).to have_key('domain') + end + end + + context 'when pages is disabled' do + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(false) + project.add_master(user) + end + + it_behaves_like '404 response' do + let(:request) { get api(route, user) } + end + end + + context 'when user is a master' do + before do + project.add_master(user) + end + + it_behaves_like 'get pages domains' + end + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it_behaves_like '403 response' do + let(:request) { get api(route, user) } + end + end + + context 'when user is a reporter' do + before do + project.add_reporter(user) + end + + it_behaves_like '403 response' do + let(:request) { get api(route, user) } + end + end + + context 'when user is a guest' do + before do + project.add_guest(user) + end + + it_behaves_like '403 response' do + let(:request) { get api(route, user) } + end + end + + context 'when user is not a member' do + it_behaves_like '404 response' do + let(:request) { get api(route, user) } + end + end + end + + describe 'GET /projects/:project_id/pages/domains/:domain' do + shared_examples_for 'get pages domain' do + it 'returns pages domain' do + get api(route_domain, user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['domain']).to eq(pages_domain.domain) + expect(json_response['url']).to eq(pages_domain.url) + expect(json_response['certificate']).to be_nil + end + + it 'returns pages domain with a certificate' do + get api(route_secure_domain, user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['domain']).to eq(pages_domain_secure.domain) + expect(json_response['url']).to eq(pages_domain_secure.url) + expect(json_response['certificate']['subject']).to eq(pages_domain_secure.subject) + expect(json_response['certificate']['expired']).to be false + end + + it 'returns pages domain with an expired certificate' do + get api(route_expired_domain, user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['certificate']['expired']).to be true + end + end + + context 'when domain is vacant' do + before do + project.add_master(user) + end + + it_behaves_like '404 response' do + let(:request) { get api(route_vacant_domain, user) } + end + end + + context 'when user is a master' do + before do + project.add_master(user) + end + + it_behaves_like 'get pages domain' + end + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it_behaves_like '403 response' do + let(:request) { get api(route, user) } + end + end + + context 'when user is a reporter' do + before do + project.add_reporter(user) + end + + it_behaves_like '403 response' do + let(:request) { get api(route, user) } + end + end + + context 'when user is a guest' do + before do + project.add_guest(user) + end + + it_behaves_like '403 response' do + let(:request) { get api(route, user) } + end + end + + context 'when user is not a member' do + it_behaves_like '404 response' do + let(:request) { get api(route, user) } + end + end + end + + describe 'POST /projects/:project_id/pages/domains' do + let(:params) { pages_domain_params.slice(:domain) } + let(:params_secure) { pages_domain_secure_params.slice(:domain, :certificate, :key) } + + shared_examples_for 'post pages domains' do + it 'creates a new pages domain' do + post api(route, user), params + pages_domain = PagesDomain.find_by(domain: json_response['domain']) + + expect(response).to have_gitlab_http_status(201) + expect(pages_domain.domain).to eq(params[:domain]) + expect(pages_domain.certificate).to be_nil + expect(pages_domain.key).to be_nil + end + + it 'creates a new secure pages domain' do + post api(route, user), params_secure + pages_domain = PagesDomain.find_by(domain: json_response['domain']) + + expect(response).to have_gitlab_http_status(201) + expect(pages_domain.domain).to eq(params_secure[:domain]) + expect(pages_domain.certificate).to eq(params_secure[:certificate]) + expect(pages_domain.key).to eq(params_secure[:key]) + end + + it 'fails to create pages domain without key' do + post api(route, user), pages_domain_secure_params.slice(:domain, :certificate) + + expect(response).to have_gitlab_http_status(400) + end + + it 'fails to create pages domain with key missmatch' do + post api(route, user), pages_domain_secure_key_missmatch_params.slice(:domain, :certificate, :key) + + expect(response).to have_gitlab_http_status(400) + end + end + + context 'when user is a master' do + before do + project.add_master(user) + end + + it_behaves_like 'post pages domains' + end + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it_behaves_like '403 response' do + let(:request) { post api(route, user), params } + end + end + + context 'when user is a reporter' do + before do + project.add_reporter(user) + end + + it_behaves_like '403 response' do + let(:request) { post api(route, user), params } + end + end + + context 'when user is a guest' do + before do + project.add_guest(user) + end + + it_behaves_like '403 response' do + let(:request) { post api(route, user), params } + end + end + + context 'when user is not a member' do + it_behaves_like '404 response' do + let(:request) { post api(route, user), params } + end + end + end + + describe 'PUT /projects/:project_id/pages/domains/:domain' do + let(:params_secure) { pages_domain_secure_params.slice(:certificate, :key) } + let(:params_secure_nokey) { pages_domain_secure_params.slice(:certificate) } + + shared_examples_for 'put pages domain' do + it 'updates pages domain removing certificate' do + put api(route_secure_domain, user) + pages_domain_secure.reload + + expect(response).to have_gitlab_http_status(200) + expect(pages_domain_secure.certificate).to be_nil + expect(pages_domain_secure.key).to be_nil + end + + it 'updates pages domain adding certificate' do + put api(route_domain, user), params_secure + pages_domain.reload + + expect(response).to have_gitlab_http_status(200) + expect(pages_domain.certificate).to eq(params_secure[:certificate]) + expect(pages_domain.key).to eq(params_secure[:key]) + end + + it 'updates pages domain with expired certificate' do + put api(route_expired_domain, user), params_secure + pages_domain_expired.reload + + expect(response).to have_gitlab_http_status(200) + expect(pages_domain_expired.certificate).to eq(params_secure[:certificate]) + expect(pages_domain_expired.key).to eq(params_secure[:key]) + end + + it 'updates pages domain with expired certificate not updating key' do + put api(route_secure_domain, user), params_secure_nokey + pages_domain_secure.reload + + expect(response).to have_gitlab_http_status(200) + expect(pages_domain_secure.certificate).to eq(params_secure_nokey[:certificate]) + end + + it 'fails to update pages domain adding certificate without key' do + put api(route_domain, user), params_secure_nokey + + expect(response).to have_gitlab_http_status(400) + end + + it 'fails to update pages domain adding certificate with missing chain' do + put api(route_domain, user), pages_domain_secure_missing_chain_params.slice(:certificate) + + expect(response).to have_gitlab_http_status(400) + end + + it 'fails to update pages domain with key missmatch' do + put api(route_secure_domain, user), pages_domain_secure_key_missmatch_params.slice(:certificate, :key) + + expect(response).to have_gitlab_http_status(400) + end + end + + context 'when domain is vacant' do + before do + project.add_master(user) + end + + it_behaves_like '404 response' do + let(:request) { put api(route_vacant_domain, user) } + end + end + + context 'when user is a master' do + before do + project.add_master(user) + end + + it_behaves_like 'put pages domain' + end + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it_behaves_like '403 response' do + let(:request) { put api(route_domain, user) } + end + end + + context 'when user is a reporter' do + before do + project.add_reporter(user) + end + + it_behaves_like '403 response' do + let(:request) { put api(route_domain, user) } + end + end + + context 'when user is a guest' do + before do + project.add_guest(user) + end + + it_behaves_like '403 response' do + let(:request) { put api(route_domain, user) } + end + end + + context 'when user is not a member' do + it_behaves_like '404 response' do + let(:request) { put api(route_domain, user) } + end + end + end + + describe 'DELETE /projects/:project_id/pages/domains/:domain' do + shared_examples_for 'delete pages domain' do + it 'deletes a pages domain' do + delete api(route_domain, user) + + expect(response).to have_gitlab_http_status(204) + end + end + + context 'when domain is vacant' do + before do + project.add_master(user) + end + + it_behaves_like '404 response' do + let(:request) { delete api(route_vacant_domain, user) } + end + end + + context 'when user is a master' do + before do + project.add_master(user) + end + + it_behaves_like 'delete pages domain' + end + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it_behaves_like '403 response' do + let(:request) { delete api(route_domain, user) } + end + end + + context 'when user is a reporter' do + before do + project.add_reporter(user) + end + + it_behaves_like '403 response' do + let(:request) { delete api(route_domain, user) } + end + end + + context 'when user is a guest' do + before do + project.add_guest(user) + end + + it_behaves_like '403 response' do + let(:request) { delete api(route_domain, user) } + end + end + + context 'when user is not a member' do + it_behaves_like '404 response' do + let(:request) { delete api(route_domain, user) } + end + end + end +end -- cgit v1.2.1 From bcccf6c1619ecd56bbcc515c4a8f057ba34d0ab9 Mon Sep 17 00:00:00 2001 From: Travis Miller Date: Mon, 21 Aug 2017 18:58:50 -0500 Subject: Add pages domains API entities --- lib/api/entities.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 5f0bad14839..efe874b2e6b 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1043,5 +1043,22 @@ module API expose :key expose :value end + + class PagesDomainCertificate < Grape::Entity + expose :subject + expose :expired?, as: :expired + expose :certificate + expose :certificate_text + end + + class PagesDomain < Grape::Entity + expose :domain + expose :url + expose :certificate, + if: ->(pages_domain, _) { pages_domain.certificate? }, + using: PagesDomainCertificate do |pages_domain| + pages_domain + end + end end end -- cgit v1.2.1 From 8d1ab256bfc9dc0af5aefbb86b1a4b96c4d7c12d Mon Sep 17 00:00:00 2001 From: Travis Miller Date: Mon, 21 Aug 2017 18:59:54 -0500 Subject: Add pages domains API implementation --- lib/api/api.rb | 1 + lib/api/helpers.rb | 4 ++ lib/api/pages_domains.rb | 117 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 lib/api/pages_domains.rb diff --git a/lib/api/api.rb b/lib/api/api.rb index 99fcc59ba04..7db18e25a5f 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -131,6 +131,7 @@ module API mount ::API::Namespaces mount ::API::Notes mount ::API::NotificationSettings + mount ::API::PagesDomains mount ::API::Pipelines mount ::API::PipelineSchedules mount ::API::ProjectHooks diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 2b316b58ed9..7a2ec865860 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -184,6 +184,10 @@ module API end end + def require_pages_enabled! + not_found! unless user_project.pages_available? + end + def can?(object, action, subject = :global) Ability.allowed?(object, action, subject) end diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb new file mode 100644 index 00000000000..259f3f34068 --- /dev/null +++ b/lib/api/pages_domains.rb @@ -0,0 +1,117 @@ +module API + class PagesDomains < Grape::API + include PaginationParams + + before do + authenticate! + require_pages_enabled! + end + + after_validation do + normalize_params_file_to_string + end + + helpers do + def find_pages_domain! + user_project.pages_domains.find_by(domain: params[:domain]) || not_found!('PagesDomain') + end + + def pages_domain + @pages_domain ||= find_pages_domain! + end + + def normalize_params_file_to_string + params.each do |k, v| + if v.is_a?(Hash) && v.key?(:tempfile) + params[k] = v[:tempfile].to_a.join('') + end + end + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: %r{[^/]+} } do + desc 'Get all pages domains' do + success Entities::PagesDomain + end + params do + use :pagination + end + get ":id/pages/domains" do + authorize! :read_pages, user_project + + present paginate(user_project.pages_domains.order(:domain)), with: Entities::PagesDomain + end + + desc 'Get a single pages domain' do + success Entities::PagesDomain + end + params do + requires :domain, type: String, desc: 'The domain' + end + get ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do + authorize! :read_pages, user_project + + present pages_domain, with: Entities::PagesDomain + end + + desc 'Create a new pages domain' do + success Entities::PagesDomain + end + params do + requires :domain, type: String, desc: 'The domain' + optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate' + optional :key, allow_blank: false, types: [File, String], desc: 'The key' + all_or_none_of :certificate, :key + end + post ":id/pages/domains" do + authorize! :update_pages, user_project + + pages_domain_params = declared(params, include_parent_namespaces: false) + pages_domain = user_project.pages_domains.create(pages_domain_params) + + if pages_domain.persisted? + present pages_domain, with: Entities::PagesDomain + else + render_validation_error!(pages_domain) + end + end + + desc 'Updates a pages domain' + params do + requires :domain, type: String, desc: 'The domain' + optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate' + optional :key, allow_blank: false, types: [File, String], desc: 'The key' + end + put ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do + authorize! :update_pages, user_project + + pages_domain_params = declared(params, include_parent_namespaces: false) + + # Remove empty private key if certificate is not empty. + if pages_domain_params[:certificate] && !pages_domain_params[:key] + pages_domain_params.delete(:key) + end + + if pages_domain.update(pages_domain_params) + present pages_domain, with: Entities::PagesDomain + else + render_validation_error!(pages_domain) + end + end + + desc 'Delete a pages domain' + params do + requires :domain, type: String, desc: 'The domain' + end + delete ":id/pages/domains/:domain", requirements: { domain: %r{[^/]+} } do + authorize! :update_pages, user_project + + status 204 + pages_domain.destroy + end + end + end +end -- cgit v1.2.1 From d43d1012d7bef2020ce281d35546bed73b9268f9 Mon Sep 17 00:00:00 2001 From: James Ramsay Date: Sun, 22 Oct 2017 11:45:20 +0300 Subject: Remove superfluous namespace from cluster settings --- app/views/projects/clusters/show.html.haml | 4 +- locale/gitlab.pot | 75 ++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index ff76abc3553..b127e06030e 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -47,7 +47,7 @@ - if can?(current_user, :update_cluster, @cluster) .form-group - = field.submit s_('ClusterIntegration|Save'), class: 'btn btn-success' + = field.submit _('Save'), class: 'btn btn-success' %section.settings#js-cluster-details .settings-header @@ -68,7 +68,7 @@ %section.settings#js-cluster-advanced-settings .settings-header - %h4= s_('ClusterIntegration|Advanced settings') + %h4= _('Advanced settings') %button.btn.js-settings-toggle = expanded ? 'Collapse' : 'Expand' %p= s_('ClusterIntegration|Manage Cluster integration on your GitLab project') diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1f356a231b0..08f6212d997 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-10-10 17:50+0200\n" -"PO-Revision-Date: 2017-10-10 17:50+0200\n" +"POT-Creation-Date: 2017-10-22 16:40+0300\n" +"PO-Revision-Date: 2017-10-22 16:40+0300\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" @@ -106,6 +106,12 @@ msgstr "" msgid "Add new directory" msgstr "" +msgid "AdminHealthPageLink|health page" +msgstr "" + +msgid "Advanced settings" +msgstr "" + msgid "All" msgstr "" @@ -127,6 +133,9 @@ msgstr "" msgid "Are you sure you want to discard your changes?" msgstr "" +msgid "Are you sure you want to leave this group?" +msgstr "" + msgid "Are you sure you want to reset registration token?" msgstr "" @@ -160,18 +169,21 @@ msgstr "" msgid "AutoDevOps|Auto DevOps (Beta)" msgstr "" -msgid "AutoDevOps|Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration." -msgstr "" - msgid "AutoDevOps|Auto DevOps documentation" msgstr "" msgid "AutoDevOps|Enable in settings" msgstr "" +msgid "AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration." +msgstr "" + msgid "AutoDevOps|Learn more in the %{link_to_documentation}" msgstr "" +msgid "AutoDevOps|You can activate %{link_to_settings} for this project." +msgstr "" + msgid "Branch" msgid_plural "Branches" msgstr[0] "" @@ -180,6 +192,9 @@ msgstr[1] "" msgid "Branch %{branch_name} was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" msgstr "" +msgid "Branch has changed" +msgstr "" + msgid "BranchSwitcherPlaceholder|Search branches" msgstr "" @@ -375,6 +390,9 @@ msgstr "" msgid "CiStatus|running" msgstr "" +msgid "CircuitBreakerApiLink|circuitbreaker api" +msgstr "" + msgid "Clone repository" msgstr "" @@ -384,6 +402,9 @@ msgstr "" msgid "ClusterIntegration|A %{link_to_container_project} must have been created under this account" msgstr "" +msgid "ClusterIntegration|Cluster details" +msgstr "" + msgid "ClusterIntegration|Cluster integration" msgstr "" @@ -435,6 +456,9 @@ msgstr "" msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters" msgstr "" +msgid "ClusterIntegration|Manage Cluster integration on your GitLab project" +msgstr "" + msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}" msgstr "" @@ -459,7 +483,7 @@ msgstr "" msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your project." msgstr "" -msgid "ClusterIntegration|Save" +msgid "ClusterIntegration|See and edit the details for your cluster" msgstr "" msgid "ClusterIntegration|See machine types" @@ -614,6 +638,9 @@ msgstr "" msgid "Create merge request" msgstr "" +msgid "Create new branch" +msgstr "" + msgid "Create new..." msgstr "" @@ -1021,6 +1048,9 @@ msgstr "" msgid "Login" msgstr "" +msgid "Maximum git storage failures" +msgstr "" + msgid "Median" msgstr "" @@ -1347,6 +1377,9 @@ msgstr "" msgid "Profiles|your account" msgstr "" +msgid "Project '%{project_name}' is in the process of being deleted." +msgstr "" + msgid "Project '%{project_name}' queued for deletion." msgstr "" @@ -1356,9 +1389,6 @@ msgstr "" msgid "Project '%{project_name}' was successfully updated." msgstr "" -msgid "Project '%{project_name}' will be deleted." -msgstr "" - msgid "Project access must be granted explicitly to each user." msgstr "" @@ -1494,6 +1524,9 @@ msgstr "" msgid "SSH Keys" msgstr "" +msgid "Save" +msgstr "" + msgid "Save changes" msgstr "" @@ -1512,6 +1545,15 @@ msgstr "" msgid "Search branches and tags" msgstr "" +msgid "Seconds before reseting failure information" +msgstr "" + +msgid "Seconds to wait after a storage failure" +msgstr "" + +msgid "Seconds to wait for a storage access attempt" +msgstr "" + msgid "Select Archive Format" msgstr "" @@ -1714,6 +1756,9 @@ msgstr "" msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage." msgstr "" +msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}." +msgstr "" + msgid "The phase of the development lifecycle." msgstr "" @@ -1744,6 +1789,12 @@ msgstr "" msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running." msgstr "" +msgid "The time in seconds GitLab will keep failure information. When no failures occur during this time, information about the mount is reset." +msgstr "" + +msgid "The time in seconds GitLab will try to access storage. After this time a timeout error will be raised." +msgstr "" + msgid "The time taken by each data entry gathered by that stage." msgstr "" @@ -1753,6 +1804,9 @@ msgstr "" msgid "There are problems accessing Git storage: " msgstr "" +msgid "This branch has changed since you started editing. Would you like to create a new branch?" +msgstr "" + msgid "This is a confidential issue." msgstr "" @@ -1976,6 +2030,9 @@ msgstr "" msgid "We don't have enough data to show this stage." msgstr "" +msgid "When access to a storage fails. GitLab will prevent access to the storage for the time specified here. This allows the filesystem to recover. Repositories on failing shards are temporarly unavailable" +msgstr "" + msgid "Wiki" msgstr "" -- cgit v1.2.1 From 3bff85a4f659438edbbc486a0b3c32ff589ab299 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 23 Oct 2017 03:57:51 +0300 Subject: Fix the writing of invalid environment refs Environment names that contained a space would cause an error in GitLab 10.1 because a new guard clause was introduced in Repository#write_ref to prevent such references from existing. Use the slug instead to ensure that the name is valid. Closes #39182 --- app/models/environment.rb | 2 +- changelogs/unreleased/sh-fix-environment-write-ref.yml | 5 +++++ spec/models/environment_spec.rb | 10 ++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/sh-fix-environment-write-ref.yml diff --git a/app/models/environment.rb b/app/models/environment.rb index b6868ccbe8f..e613d21add6 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -110,7 +110,7 @@ class Environment < ActiveRecord::Base end def ref_path - "refs/#{Repository::REF_ENVIRONMENTS}/#{Shellwords.shellescape(name)}" + "refs/#{Repository::REF_ENVIRONMENTS}/#{generate_slug}" end def formatted_external_url diff --git a/changelogs/unreleased/sh-fix-environment-write-ref.yml b/changelogs/unreleased/sh-fix-environment-write-ref.yml new file mode 100644 index 00000000000..8f291843ebe --- /dev/null +++ b/changelogs/unreleased/sh-fix-environment-write-ref.yml @@ -0,0 +1,5 @@ +--- +title: Fix the writing of invalid environment refs +merge_request: +author: +type: fixed diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 25e5d155894..e1be23541e8 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -575,6 +575,16 @@ describe Environment do end end + describe '#ref_path' do + subject(:environment) do + create(:environment, name: 'staging / review-1') + end + + it 'returns a path that uses the slug and does not have spaces' do + expect(environment.ref_path).to start_with('refs/environments/staging-review-1-') + end + end + describe '#external_url_for' do let(:source_path) { 'source/file.html' } let(:sha) { RepoHelpers.sample_commit.id } -- cgit v1.2.1 From 1cf35c3d1d274a24bf7b6283bf5d43ca0ffe8a10 Mon Sep 17 00:00:00 2001 From: George Andrinopoulos Date: Sun, 22 Oct 2017 17:50:58 +0300 Subject: Add case insensitive branches search --- app/finders/branches_finder.rb | 2 +- changelogs/unreleased/35199-case-insensitive-branches-search.yml | 5 +++++ spec/finders/branches_finder_spec.rb | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/35199-case-insensitive-branches-search.yml diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb index 533076585c0..852eac3647d 100644 --- a/app/finders/branches_finder.rb +++ b/app/finders/branches_finder.rb @@ -23,7 +23,7 @@ class BranchesFinder def filter_by_name(branches) if search - branches.select { |branch| branch.name.include?(search) } + branches.select { |branch| branch.name.upcase.include?(search.upcase) } else branches end diff --git a/changelogs/unreleased/35199-case-insensitive-branches-search.yml b/changelogs/unreleased/35199-case-insensitive-branches-search.yml new file mode 100644 index 00000000000..da2729e9e55 --- /dev/null +++ b/changelogs/unreleased/35199-case-insensitive-branches-search.yml @@ -0,0 +1,5 @@ +--- +title: Case insensitive search for branches +merge_request: 14995 +author: George Andrinopoulos +type: fixed diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb index 91f34973ba5..9e3f2c69606 100644 --- a/spec/finders/branches_finder_spec.rb +++ b/spec/finders/branches_finder_spec.rb @@ -46,6 +46,15 @@ describe BranchesFinder do expect(result.count).to eq(1) end + it 'filters branches by name ignoring letter case' do + branches_finder = described_class.new(repository, { search: 'FiX' }) + + result = branches_finder.execute + + expect(result.first.name).to eq('fix') + expect(result.count).to eq(1) + end + it 'does not find any branch with that name' do branches_finder = described_class.new(repository, { search: 'random' }) -- cgit v1.2.1 From 08033d3d24dca65350d0b3c6a1045a511219c9c3 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 17 Oct 2017 17:38:40 +0200 Subject: Add new circuitbreaker properties to application_settings --- changelogs/unreleased/bvl-circuitbreaker-backoff.yml | 6 ++++++ ...ew_circuitbreaker_settings_to_application_settings.rb | 16 ++++++++++++++++ db/schema.rb | 4 +++- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/bvl-circuitbreaker-backoff.yml create mode 100644 db/migrate/20171017145932_add_new_circuitbreaker_settings_to_application_settings.rb diff --git a/changelogs/unreleased/bvl-circuitbreaker-backoff.yml b/changelogs/unreleased/bvl-circuitbreaker-backoff.yml new file mode 100644 index 00000000000..5cb90e7c085 --- /dev/null +++ b/changelogs/unreleased/bvl-circuitbreaker-backoff.yml @@ -0,0 +1,6 @@ +--- +title: Make the circuitbreaker more robust by adding higher thresholds, and multiple + access attempts. +merge_request: 14933 +author: +type: fixed diff --git a/db/migrate/20171017145932_add_new_circuitbreaker_settings_to_application_settings.rb b/db/migrate/20171017145932_add_new_circuitbreaker_settings_to_application_settings.rb new file mode 100644 index 00000000000..07eb25c0b0f --- /dev/null +++ b/db/migrate/20171017145932_add_new_circuitbreaker_settings_to_application_settings.rb @@ -0,0 +1,16 @@ +class AddNewCircuitbreakerSettingsToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :application_settings, + :circuitbreaker_access_retries, + :integer, + default: 3 + add_column :application_settings, + :circuitbreaker_backoff_threshold, + :integer, + default: 80 + end +end diff --git a/db/schema.rb b/db/schema.rb index c2c04873d4d..530f08022be 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171012101043) do +ActiveRecord::Schema.define(version: 20171017145932) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -138,6 +138,8 @@ ActiveRecord::Schema.define(version: 20171012101043) do t.integer "circuitbreaker_failure_wait_time", default: 30 t.integer "circuitbreaker_failure_reset_time", default: 1800 t.integer "circuitbreaker_storage_timeout", default: 30 + t.integer "circuitbreaker_access_retries", default: 3 + t.integer "circuitbreaker_backoff_threshold", default: 80 end create_table "audit_events", force: :cascade do |t| -- cgit v1.2.1 From 1881d4f8ecbf52afd7bc732cd6c1296fafd38405 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 18 Oct 2017 14:57:35 +0200 Subject: Allow configuring new circuitbreaker settings from the UI and API --- app/helpers/application_settings_helper.rb | 11 ++++++++ app/models/application_setting.rb | 14 +++++++++- .../admin/application_settings/_form.html.haml | 30 ++++++++++++++------- doc/administration/img/circuitbreaker_config.png | Bin 213210 -> 335073 bytes doc/administration/repository_storage_paths.md | 5 ++++ doc/api/settings.md | 2 ++ spec/models/application_setting_spec.rb | 13 ++++++++- 7 files changed, 64 insertions(+), 11 deletions(-) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 1ee8911bb1a..cd1ecaadb85 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -120,6 +120,15 @@ module ApplicationSettingsHelper message.html_safe end + def circuitbreaker_access_retries_help_text + _('The number of attempts GitLab will make to access a storage.') + end + + def circuitbreaker_backoff_threshold_help_text + _("The number of failures after which GitLab will start temporarily "\ + "disabling access to a storage shard on a host") + end + def circuitbreaker_failure_wait_time_help_text _("When access to a storage fails. GitLab will prevent access to the "\ "storage for the time specified here. This allows the filesystem to "\ @@ -144,6 +153,8 @@ module ApplicationSettingsHelper :akismet_api_key, :akismet_enabled, :auto_devops_enabled, + :circuitbreaker_access_retries, + :circuitbreaker_backoff_threshold, :circuitbreaker_failure_count_threshold, :circuitbreaker_failure_reset_time, :circuitbreaker_failure_wait_time, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 4dda276bb41..f266e7db6da 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -153,13 +153,25 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { greater_than_or_equal_to: 0 } - validates :circuitbreaker_failure_count_threshold, + validates :circuitbreaker_backoff_threshold, + :circuitbreaker_failure_count_threshold, :circuitbreaker_failure_wait_time, :circuitbreaker_failure_reset_time, :circuitbreaker_storage_timeout, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :circuitbreaker_access_retries, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 1 } + + validates_each :circuitbreaker_backoff_threshold do |record, attr, value| + if value.to_i >= record.circuitbreaker_failure_count_threshold + record.errors.add(attr, _("The circuitbreaker backoff threshold should be "\ + "lower than the failure count threshold")) + end + end + SUPPORTED_KEY_TYPES.each do |type| validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 2b23af9212e..3a4d5ce0b5c 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -533,11 +533,23 @@ %fieldset %legend Git Storage Circuitbreaker settings .form-group - = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2' + = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2' .col-sm-10 - = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control' + = f.number_field :circuitbreaker_access_retries, class: 'form-control' .help-block - = circuitbreaker_failure_count_help_text + = circuitbreaker_access_retries_help_text + .form-group + = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :circuitbreaker_storage_timeout, class: 'form-control' + .help-block + = circuitbreaker_storage_timeout_help_text + .form-group + = f.label :circuitbreaker_backoff_threshold, _('Number of failures before backing off'), class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :circuitbreaker_backoff_threshold, class: 'form-control' + .help-block + = circuitbreaker_backoff_threshold_help_text .form-group = f.label :circuitbreaker_failure_wait_time, _('Seconds to wait after a storage failure'), class: 'control-label col-sm-2' .col-sm-10 @@ -545,17 +557,17 @@ .help-block = circuitbreaker_failure_wait_time_help_text .form-group - = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2' + = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2' .col-sm-10 - = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control' + = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control' .help-block - = circuitbreaker_failure_reset_time_help_text + = circuitbreaker_failure_count_help_text .form-group - = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2' + = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2' .col-sm-10 - = f.number_field :circuitbreaker_storage_timeout, class: 'form-control' + = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control' .help-block - = circuitbreaker_storage_timeout_help_text + = circuitbreaker_failure_reset_time_help_text %fieldset %legend Repository Checks diff --git a/doc/administration/img/circuitbreaker_config.png b/doc/administration/img/circuitbreaker_config.png index 9250d38297c..e811d173634 100644 Binary files a/doc/administration/img/circuitbreaker_config.png and b/doc/administration/img/circuitbreaker_config.png differ diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md index efcabd69822..2ee8d78b2f0 100644 --- a/doc/administration/repository_storage_paths.md +++ b/doc/administration/repository_storage_paths.md @@ -109,6 +109,11 @@ This can be configured from the admin interface: ![circuitbreaker configuration](img/circuitbreaker_config.png) +**Number of access attempts**: The number of attempts GitLab will make to access a +storage when probing a shard. + +**Number of failures before backing off**: The number of failures after which +GitLab will start temporarily disabling access to a storage shard on a host. **Maximum git storage failures:** The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in diff --git a/doc/api/settings.md b/doc/api/settings.md index 664f3ef7b77..4e24e4bbfc3 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -69,6 +69,8 @@ PUT /application/settings | `after_sign_up_text` | string | no | Text shown to the user after signing up | | `akismet_api_key` | string | no | API key for akismet spam protection | | `akismet_enabled` | boolean | no | Enable or disable akismet spam protection | +| `circuitbreaker_access_retries | integer | no | The number of attempts GitLab will make to access a storage. | +| `circuitbreaker_backoff_threshold | integer | no | The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host. | | `circuitbreaker_failure_count_threshold` | integer | no | The number of failures of after which GitLab will completely prevent access to the storage. | | `circuitbreaker_failure_reset_time` | integer | no | Time in seconds GitLab will keep storage failure information. When no failures occur during this time, the failure information is reset. | | `circuitbreaker_failure_wait_time` | integer | no | Time in seconds GitLab will block access to a failing storage to allow it to recover. | diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 30495fd4f5e..47b7150d36f 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -115,7 +115,8 @@ describe ApplicationSetting do end context 'circuitbreaker settings' do - [:circuitbreaker_failure_count_threshold, + [:circuitbreaker_backoff_threshold, + :circuitbreaker_failure_count_threshold, :circuitbreaker_failure_wait_time, :circuitbreaker_failure_reset_time, :circuitbreaker_storage_timeout].each do |field| @@ -125,6 +126,16 @@ describe ApplicationSetting do .is_greater_than_or_equal_to(0) end end + + it 'requires the `backoff_threshold` to be lower than the `failure_count_threshold`' do + setting.circuitbreaker_failure_count_threshold = 10 + setting.circuitbreaker_backoff_threshold = 15 + failure_message = "The circuitbreaker backoff threshold should be lower "\ + "than the failure count threshold" + + expect(setting).not_to be_valid + expect(setting.errors[:circuitbreaker_backoff_threshold]).to include(failure_message) + end end context 'repository storages' do -- cgit v1.2.1 From 430e7671397a1c022b88da31328a5a81409671b5 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 19 Oct 2017 08:32:55 +0200 Subject: Implement backoff for the circuitbreaker The circuitbreaker now has 2 failure modes: - Backing off: This will raise the `Gitlab::Git::Storage::Failing` exception. Access to the shard is blocked temporarily. - Circuit broken: This will raise the `Gitlab::Git::Storage::CircuitBroken` exception. Access to the shard will be blocked until the failures are reset. --- app/helpers/storage_health_helper.rb | 5 +- lib/gitlab/git/storage.rb | 1 + lib/gitlab/git/storage/circuit_breaker.rb | 28 ++- lib/gitlab/git/storage/circuit_breaker_settings.rb | 8 + lib/gitlab/git/storage/null_circuit_breaker.rb | 4 + .../lib/gitlab/git/storage/circuit_breaker_spec.rb | 259 ++++++++------------- .../git/storage/null_circuit_breaker_spec.rb | 13 +- 7 files changed, 128 insertions(+), 190 deletions(-) diff --git a/app/helpers/storage_health_helper.rb b/app/helpers/storage_health_helper.rb index 544c9efb845..4d2180f7eee 100644 --- a/app/helpers/storage_health_helper.rb +++ b/app/helpers/storage_health_helper.rb @@ -16,17 +16,16 @@ module StorageHealthHelper def message_for_circuit_breaker(circuit_breaker) maximum_failures = circuit_breaker.failure_count_threshold current_failures = circuit_breaker.failure_count - permanently_broken = circuit_breaker.circuit_broken? && current_failures >= maximum_failures translation_params = { number_of_failures: current_failures, maximum_failures: maximum_failures, number_of_seconds: circuit_breaker.failure_wait_time } - if permanently_broken + if circuit_breaker.circuit_broken? s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\ "retry automatically. Reset storage information when the problem is "\ "resolved.") % translation_params - elsif circuit_breaker.circuit_broken? + elsif circuit_breaker.backing_off? _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\ "block access for %{number_of_seconds} seconds.") % translation_params else diff --git a/lib/gitlab/git/storage.rb b/lib/gitlab/git/storage.rb index 08e6c29abad..99518c9b1e4 100644 --- a/lib/gitlab/git/storage.rb +++ b/lib/gitlab/git/storage.rb @@ -12,6 +12,7 @@ module Gitlab CircuitOpen = Class.new(Inaccessible) Misconfiguration = Class.new(Inaccessible) + Failing = Class.new(Inaccessible) REDIS_KEY_PREFIX = 'storage_accessible:'.freeze diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb index 0456ad9a1f3..2ce97ff41f9 100644 --- a/lib/gitlab/git/storage/circuit_breaker.rb +++ b/lib/gitlab/git/storage/circuit_breaker.rb @@ -64,12 +64,20 @@ module Gitlab def circuit_broken? return false if no_failures? + failure_count > failure_count_threshold + end + + def backing_off? + return false if no_failures? + recent_failure = last_failure > failure_wait_time.seconds.ago - too_many_failures = failure_count > failure_count_threshold + too_many_failures = failure_count > backoff_threshold - recent_failure || too_many_failures + recent_failure && too_many_failures end + private + def failure_info @failure_info ||= get_failure_info end @@ -94,7 +102,11 @@ module Gitlab def check_storage_accessible! if circuit_broken? - raise Gitlab::Git::Storage::CircuitOpen.new("Circuit for #{storage} is broken", failure_wait_time) + raise Gitlab::Git::Storage::CircuitOpen.new("Circuit for #{storage} is broken", failure_reset_time) + end + + if backing_off? + raise Gitlab::Git::Storage::Failing.new("Backing off access to #{storage}", failure_wait_time) end unless storage_available? @@ -131,12 +143,6 @@ module Gitlab end end - def cache_key - @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}" - end - - private - def get_failure_info last_failure, failure_count = Gitlab::Git::Storage.redis.with do |redis| redis.hmget(cache_key, :last_failure, :failure_count) @@ -146,6 +152,10 @@ module Gitlab FailureInfo.new(last_failure, failure_count.to_i) end + + def cache_key + @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}" + end end end end diff --git a/lib/gitlab/git/storage/circuit_breaker_settings.rb b/lib/gitlab/git/storage/circuit_breaker_settings.rb index d2313fe7c1b..257fe8cd8f0 100644 --- a/lib/gitlab/git/storage/circuit_breaker_settings.rb +++ b/lib/gitlab/git/storage/circuit_breaker_settings.rb @@ -18,6 +18,14 @@ module Gitlab application_settings.circuitbreaker_storage_timeout end + def access_retries + application_settings.circuitbreaker_access_retries + end + + def backoff_threshold + application_settings.circuitbreaker_backoff_threshold + end + private def application_settings diff --git a/lib/gitlab/git/storage/null_circuit_breaker.rb b/lib/gitlab/git/storage/null_circuit_breaker.rb index 60c6791a7e4..a12d52d295f 100644 --- a/lib/gitlab/git/storage/null_circuit_breaker.rb +++ b/lib/gitlab/git/storage/null_circuit_breaker.rb @@ -25,6 +25,10 @@ module Gitlab !!@error end + def backing_off? + false + end + def last_failure circuit_broken? ? Time.now : nil end diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb index c8d532df059..e3f221aa863 100644 --- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb +++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb @@ -79,7 +79,9 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: stub_application_setting(circuitbreaker_failure_count_threshold: 0, circuitbreaker_failure_wait_time: 1, circuitbreaker_failure_reset_time: 2, - circuitbreaker_storage_timeout: 3) + circuitbreaker_storage_timeout: 3, + circuitbreaker_access_retries: 4, + circuitbreaker_backoff_threshold: 5) end describe '#failure_count_threshold' do @@ -105,14 +107,43 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: expect(circuit_breaker.storage_timeout).to eq(3) end end + + describe '#access_retries' do + it 'reads the value from settings' do + expect(circuit_breaker.access_retries).to eq(4) + end + end + + describe '#backoff_threshold' do + it 'reads the value from settings' do + expect(circuit_breaker.backoff_threshold).to eq(5) + end + end end describe '#perform' do - it 'raises an exception with retry time when the circuit is open' do - allow(circuit_breaker).to receive(:circuit_broken?).and_return(true) + it 'raises the correct exception when the circuit is open' do + set_in_redis(:last_failure, 1.day.ago.to_f) + set_in_redis(:failure_count, 999) expect { |b| circuit_breaker.perform(&b) } - .to raise_error(Gitlab::Git::Storage::CircuitOpen) + .to raise_error do |exception| + expect(exception).to be_kind_of(Gitlab::Git::Storage::CircuitOpen) + expect(exception.retry_after).to eq(1800) + end + end + + it 'raises the correct exception when backing off' do + Timecop.freeze do + set_in_redis(:last_failure, 1.second.ago.to_f) + set_in_redis(:failure_count, 90) + + expect { |b| circuit_breaker.perform(&b) } + .to raise_error do |exception| + expect(exception).to be_kind_of(Gitlab::Git::Storage::Failing) + expect(exception.retry_after).to eq(30) + end + end end it 'yields the block' do @@ -122,6 +153,7 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: it 'checks if the storage is available' do expect(circuit_breaker).to receive(:check_storage_accessible!) + .and_call_original circuit_breaker.perform { 'hello world' } end @@ -137,201 +169,102 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: .to raise_error(Rugged::OSError) end - context 'with the feature disabled' do - it 'returns the block without checking accessibility' do - stub_feature_flags(git_storage_circuit_breaker: false) - - expect(circuit_breaker).not_to receive(:circuit_broken?) - - result = circuit_breaker.perform { 'hello' } - - expect(result).to eq('hello') - end - end - end - - describe '#circuit_broken?' do - it 'is working when there is no last failure' do - set_in_redis(:last_failure, nil) - set_in_redis(:failure_count, 0) - - expect(circuit_breaker.circuit_broken?).to be_falsey - end - - it 'is broken when there was a recent failure' do - Timecop.freeze do - set_in_redis(:last_failure, 1.second.ago.to_f) - set_in_redis(:failure_count, 1) - - expect(circuit_breaker.circuit_broken?).to be_truthy - end - end - - it 'is broken when there are too many failures' do - set_in_redis(:last_failure, 1.day.ago.to_f) - set_in_redis(:failure_count, 200) - - expect(circuit_breaker.circuit_broken?).to be_truthy - end - - context 'the `failure_wait_time` is set to 0' do - before do - stub_application_setting(circuitbreaker_failure_wait_time: 0) - end + it 'tracks that the storage was accessible' do + set_in_redis(:failure_count, 10) + set_in_redis(:last_failure, Time.now.to_f) - it 'is working even when there is a recent failure' do - Timecop.freeze do - set_in_redis(:last_failure, 0.seconds.ago.to_f) - set_in_redis(:failure_count, 1) + circuit_breaker.perform { '' } - expect(circuit_breaker.circuit_broken?).to be_falsey - end - end + expect(value_from_redis(:failure_count).to_i).to eq(0) + expect(value_from_redis(:last_failure)).to be_empty + expect(circuit_breaker.failure_count).to eq(0) + expect(circuit_breaker.last_failure).to be_nil end - end - describe "storage_available?" do - context 'the storage is available' do - it 'tracks that the storage was accessible an raises the error' do - expect(circuit_breaker).to receive(:track_storage_accessible) - - circuit_breaker.storage_available? - end + it 'only accessibility check once' do + expect(Gitlab::Git::Storage::ForkedStorageCheck) + .to receive(:storage_available?).once.and_call_original - it 'only performs the check once' do - expect(Gitlab::Git::Storage::ForkedStorageCheck) - .to receive(:storage_available?).once.and_call_original - - 2.times { circuit_breaker.storage_available? } - end + 2.times { circuit_breaker.perform { '' } } end - context 'storage is not available' do - let(:storage_name) { 'broken' } - - it 'tracks that the storage was inaccessible' do - expect(circuit_breaker).to receive(:track_storage_inaccessible) + context 'with the feature disabled' do + it 'returns the block without checking accessibility' do + stub_feature_flags(git_storage_circuit_breaker: false) - circuit_breaker.storage_available? - end - end - end + expect(circuit_breaker).not_to receive(:circuit_broken?) - describe '#check_storage_accessible!' do - it 'raises an exception with retry time when the circuit is open' do - allow(circuit_breaker).to receive(:circuit_broken?).and_return(true) + result = circuit_breaker.perform { 'hello' } - expect { circuit_breaker.check_storage_accessible! } - .to raise_error do |exception| - expect(exception).to be_kind_of(Gitlab::Git::Storage::CircuitOpen) - expect(exception.retry_after).to eq(30) + expect(result).to eq('hello') end end context 'the storage is not available' do let(:storage_name) { 'broken' } - it 'raises an error' do + it 'raises the correct exception' do expect(circuit_breaker).to receive(:track_storage_inaccessible) - expect { circuit_breaker.check_storage_accessible! } + expect { circuit_breaker.perform { '' } } .to raise_error do |exception| expect(exception).to be_kind_of(Gitlab::Git::Storage::Inaccessible) expect(exception.retry_after).to eq(30) end end - end - end - - describe '#track_storage_inaccessible' do - around do |example| - Timecop.freeze { example.run } - end - - it 'records the failure time in redis' do - circuit_breaker.track_storage_inaccessible - - failure_time = value_from_redis(:last_failure) - expect(Time.at(failure_time.to_i)).to be_within(1.second).of(Time.now) - end - - it 'sets the failure time on the breaker without reloading' do - circuit_breaker.track_storage_inaccessible - - expect(circuit_breaker).not_to receive(:get_failure_info) - expect(circuit_breaker.last_failure).to eq(Time.now) - end - - it 'increments the failure count in redis' do - set_in_redis(:failure_count, 10) - - circuit_breaker.track_storage_inaccessible - - expect(value_from_redis(:failure_count).to_i).to be(11) - end - - it 'increments the failure count on the breaker without reloading' do - set_in_redis(:failure_count, 10) - - circuit_breaker.track_storage_inaccessible + it 'tracks that the storage was inaccessible' do + Timecop.freeze do + expect { circuit_breaker.perform { '' } }.to raise_error(Gitlab::Git::Storage::Inaccessible) - expect(circuit_breaker).not_to receive(:get_failure_info) - expect(circuit_breaker.failure_count).to eq(11) + expect(value_from_redis(:failure_count).to_i).to eq(1) + expect(value_from_redis(:last_failure)).not_to be_empty + expect(circuit_breaker.failure_count).to eq(1) + expect(circuit_breaker.last_failure).to be_within(1.second).of(Time.now) + end + end end end - describe '#track_storage_accessible' do - it 'sets the failure count to zero in redis' do - set_in_redis(:failure_count, 10) - - circuit_breaker.track_storage_accessible - - expect(value_from_redis(:failure_count).to_i).to be(0) - end - - it 'sets the failure count to zero on the breaker without reloading' do - set_in_redis(:failure_count, 10) - - circuit_breaker.track_storage_accessible + describe '#circuit_broken?' do + it 'is working when there is no last failure' do + set_in_redis(:last_failure, nil) + set_in_redis(:failure_count, 0) - expect(circuit_breaker).not_to receive(:get_failure_info) - expect(circuit_breaker.failure_count).to eq(0) + expect(circuit_breaker.circuit_broken?).to be_falsey end - it 'removes the last failure time from redis' do - set_in_redis(:last_failure, Time.now.to_i) - - circuit_breaker.track_storage_accessible + it 'is broken when there are too many failures' do + set_in_redis(:last_failure, 1.day.ago.to_f) + set_in_redis(:failure_count, 200) - expect(circuit_breaker).not_to receive(:get_failure_info) - expect(circuit_breaker.last_failure).to be_nil + expect(circuit_breaker.circuit_broken?).to be_truthy end + end - it 'removes the last failure time from the breaker without reloading' do - set_in_redis(:last_failure, Time.now.to_i) - - circuit_breaker.track_storage_accessible + describe '#backing_off?' do + it 'is true when there was a recent failure' do + Timecop.freeze do + set_in_redis(:last_failure, 1.second.ago.to_f) + set_in_redis(:failure_count, 90) - expect(value_from_redis(:last_failure)).to be_empty + expect(circuit_breaker.backing_off?).to be_truthy + end end - it 'wont connect to redis when there are no failures' do - expect(Gitlab::Git::Storage.redis).to receive(:with).once - .and_call_original - expect(circuit_breaker).to receive(:track_storage_accessible) - .and_call_original - - circuit_breaker.track_storage_accessible - end - end + context 'the `failure_wait_time` is set to 0' do + before do + stub_application_setting(circuitbreaker_failure_wait_time: 0) + end - describe '#no_failures?' do - it 'is false when a failure was tracked' do - set_in_redis(:last_failure, Time.now.to_i) - set_in_redis(:failure_count, 1) + it 'is working even when there are failures' do + Timecop.freeze do + set_in_redis(:last_failure, 0.seconds.ago.to_f) + set_in_redis(:failure_count, 90) - expect(circuit_breaker.no_failures?).to be_falsey + expect(circuit_breaker.backing_off?).to be_falsey + end + end end end @@ -351,10 +284,4 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: expect(circuit_breaker.failure_count).to eq(7) end end - - describe '#cache_key' do - it 'includes storage and host' do - expect(circuit_breaker.cache_key).to eq(cache_key) - end - end end diff --git a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb index 7ee6d2f3709..5db37f55e03 100644 --- a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb +++ b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb @@ -65,17 +65,6 @@ describe Gitlab::Git::Storage::NullCircuitBreaker do ours = described_class.public_instance_methods theirs = Gitlab::Git::Storage::CircuitBreaker.public_instance_methods - # These methods are not part of the public API, but are public to allow the - # CircuitBreaker specs to operate. They should be made private over time. - exceptions = %i[ - cache_key - check_storage_accessible! - no_failures? - storage_available? - track_storage_accessible - track_storage_inaccessible - ] - - expect(theirs - ours).to contain_exactly(*exceptions) + expect(theirs - ours).to be_empty end end -- cgit v1.2.1 From 016e750308b6e927b53d8c050b3f3bb60e9ca4aa Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 20 Oct 2017 18:56:43 +0100 Subject: Fix performance of sticky.js Closes #39332 --- app/assets/javascripts/init_changes_dropdown.js | 2 +- app/assets/javascripts/lib/utils/sticky.js | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js index f785ed29e6c..e41f374b2e5 100644 --- a/app/assets/javascripts/init_changes_dropdown.js +++ b/app/assets/javascripts/init_changes_dropdown.js @@ -1,7 +1,7 @@ import stickyMonitor from './lib/utils/sticky'; export default () => { - stickyMonitor(document.querySelector('.js-diff-files-changed')); + stickyMonitor(document.querySelector('.js-diff-files-changed'), 76); $('.js-diff-stats-dropdown').glDropdown({ filterable: true, diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js index 64db42701ce..098afcfa1b4 100644 --- a/app/assets/javascripts/lib/utils/sticky.js +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -28,14 +28,10 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => { } }; -export default (el, insertPlaceholder = true) => { +export default (el, stickyTop, insertPlaceholder = true) => { if (!el) return; - const computedStyle = window.getComputedStyle(el); - - if (!/sticky/.test(computedStyle.position)) return; - - const stickyTop = parseInt(computedStyle.top, 10); + if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return; document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), { passive: true, -- cgit v1.2.1 From 35daaa36e0ee74e28422a3b088fd5b01249ea9b7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 23 Oct 2017 10:21:38 +0100 Subject: calculate the stickyTop instead of hard coding a variable --- app/assets/javascripts/dispatcher.js | 3 ++- app/assets/javascripts/init_changes_dropdown.js | 4 ++-- app/assets/javascripts/merge_request_tabs.js | 10 +++++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 5ca1708f1b3..44d6bd42f7f 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -239,7 +239,8 @@ import Diff from './diff'; break; case 'projects:compare:show': new Diff(); - initChangesDropdown(); + const paddingTop = 16; + initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop); break; case 'projects:branches:new': case 'projects:branches:create': diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js index e41f374b2e5..1bab7965c19 100644 --- a/app/assets/javascripts/init_changes_dropdown.js +++ b/app/assets/javascripts/init_changes_dropdown.js @@ -1,7 +1,7 @@ import stickyMonitor from './lib/utils/sticky'; -export default () => { - stickyMonitor(document.querySelector('.js-diff-files-changed'), 76); +export default (stickyTop) => { + stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop); $('.js-diff-stats-dropdown').glDropdown({ filterable: true, diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 789ccf48190..e64eb60231a 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -67,6 +67,9 @@ import Diff from './diff'; class MergeRequestTabs { constructor({ action, setUrl, stubLocation } = {}) { + const mergeRequestTabs = document.querySelector('.js-tabs-affix'); + const paddingTop = 16; + this.diffsLoaded = false; this.pipelinesLoaded = false; this.commitsLoaded = false; @@ -76,6 +79,11 @@ import Diff from './diff'; this.setCurrentAction = this.setCurrentAction.bind(this); this.tabShown = this.tabShown.bind(this); this.showTab = this.showTab.bind(this); + this.stickyTop = document.querySelector('.navbar-gitlab').offsetHeight - paddingTop; + + if (mergeRequestTabs) { + this.stickyTop += mergeRequestTabs.offsetHeight; + } if (stubLocation) { location = stubLocation; @@ -278,7 +286,7 @@ import Diff from './diff'; const $container = $('#diffs'); $container.html(data.html); - initChangesDropdown(); + initChangesDropdown(this.stickyTop); if (typeof gl.diffNotesCompileComponents !== 'undefined') { gl.diffNotesCompileComponents(); -- cgit v1.2.1 From caee9d5882050b3998f05edf7aff8bd3c36591c3 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 12 Oct 2017 10:55:11 +0100 Subject: Add new files & directories in the multi-file editor Closes #38614 --- .../repo/components/new_dropdown/index.vue | 70 +++++++++++++ .../repo/components/new_dropdown/modal.vue | 115 +++++++++++++++++++++ .../repo/components/repo_commit_section.vue | 2 +- .../javascripts/repo/components/repo_editor.vue | 43 ++++---- .../repo/components/repo_file_buttons.vue | 12 ++- .../javascripts/repo/components/repo_tab.vue | 4 +- app/assets/javascripts/repo/helpers/repo_helper.js | 4 +- app/assets/javascripts/repo/index.js | 15 +++ app/assets/javascripts/repo/mixins/repo_mixin.js | 2 +- app/assets/javascripts/repo/stores/repo_store.js | 7 +- .../vue_shared/components/popup_dialog.vue | 11 +- app/views/projects/tree/_tree_header.html.haml | 4 +- 12 files changed, 257 insertions(+), 32 deletions(-) create mode 100644 app/assets/javascripts/repo/components/new_dropdown/index.vue create mode 100644 app/assets/javascripts/repo/components/new_dropdown/modal.vue diff --git a/app/assets/javascripts/repo/components/new_dropdown/index.vue b/app/assets/javascripts/repo/components/new_dropdown/index.vue new file mode 100644 index 00000000000..652c8027ae3 --- /dev/null +++ b/app/assets/javascripts/repo/components/new_dropdown/index.vue @@ -0,0 +1,70 @@ + + + diff --git a/app/assets/javascripts/repo/components/new_dropdown/modal.vue b/app/assets/javascripts/repo/components/new_dropdown/modal.vue new file mode 100644 index 00000000000..95ecfb29dd3 --- /dev/null +++ b/app/assets/javascripts/repo/components/new_dropdown/modal.vue @@ -0,0 +1,115 @@ + + + diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index 185cd90ac06..e3003fbf477 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -49,7 +49,7 @@ export default { // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions const commitMessage = this.commitMessage; const actions = this.changedFiles.map(f => ({ - action: 'update', + action: f.tempFile ? 'create' : 'update', file_path: f.path, content: f.newContent, })); diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index 4639bee6d66..9f567d4e94d 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -16,28 +16,35 @@ const RepoEditor = { }, mounted() { - Service.getRaw(this.activeFile.raw_path) - .then((rawResponse) => { - Store.blobRaw = rawResponse.data; - Store.activeFile.plain = rawResponse.data; - - const monacoInstance = Helper.monaco.editor.create(this.$el, { - model: null, - readOnly: false, - contextmenu: true, - scrollBeyondLastLine: false, - }); + if (!this.activeFile.tempFile) { + Service.getRaw(this.activeFile.raw_path) + .then((rawResponse) => { + Store.blobRaw = rawResponse.data; + Store.activeFile.plain = rawResponse.data; + + this.createMonacoInstance(); + }) + .catch(Helper.loadingError); + } else { + this.createMonacoInstance(); + } + }, - Helper.monacoInstance = monacoInstance; + methods: { + createMonacoInstance() { + const monacoInstance = Helper.monaco.editor.create(this.$el, { + model: null, + readOnly: false, + contextmenu: true, + scrollBeyondLastLine: false, + }); - this.addMonacoEvents(); + Helper.monacoInstance = monacoInstance; - this.setupEditor(); - }) - .catch(Helper.loadingError); - }, + this.addMonacoEvents(); - methods: { + this.setupEditor(); + }, setupEditor() { this.showHide(); diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue index 03cd219e718..354be2545f4 100644 --- a/app/assets/javascripts/repo/components/repo_file_buttons.vue +++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue @@ -11,7 +11,12 @@ const RepoFileButtons = { mixins: [RepoMixin], computed: { - + showButtons() { + return this.activeFile.raw_path || + this.activeFile.blame_path || + this.activeFile.commits_path || + this.activeFile.permalink; + }, rawDownloadButtonLabel() { return this.binary ? 'Download' : 'Raw'; }, @@ -30,7 +35,10 @@ export default RepoFileButtons;