diff options
-rw-r--r-- | app/assets/javascripts/notes.js | 8 | ||||
-rw-r--r-- | app/assets/javascripts/repo/components/new_branch_form.vue | 115 | ||||
-rw-r--r-- | app/assets/javascripts/repo/components/repo.vue | 32 | ||||
-rw-r--r-- | app/assets/javascripts/repo/index.js | 22 | ||||
-rw-r--r-- | app/assets/javascripts/repo/services/repo_service.js | 10 | ||||
-rw-r--r-- | app/assets/javascripts/repo/stores/repo_store.js | 1 | ||||
-rw-r--r-- | app/views/peek/views/_gitaly.html.haml | 7 | ||||
-rw-r--r-- | app/views/projects/tree/_tree_header.html.haml | 2 | ||||
-rw-r--r-- | app/views/shared/_ref_switcher.html.haml | 23 | ||||
-rw-r--r-- | changelogs/unreleased/zj-peek-gitaly.yml | 5 | ||||
-rw-r--r-- | config/initializers/peek.rb | 1 | ||||
-rw-r--r-- | lib/gitlab/gitaly_client.rb | 9 | ||||
-rw-r--r-- | lib/peek/views/gitaly.rb | 34 | ||||
-rw-r--r-- | spec/features/projects/ref_switcher_spec.rb | 35 | ||||
-rw-r--r-- | spec/javascripts/notes_spec.js | 10 | ||||
-rw-r--r-- | spec/javascripts/repo/components/new_branch_form_spec.js | 122 | ||||
-rw-r--r-- | spec/javascripts/repo/components/repo_spec.js | 96 |
17 files changed, 520 insertions, 12 deletions
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 9c008da1a5d..5a6868be444 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1280,10 +1280,12 @@ export default class Notes { * Get data from Form attributes to use for saving/submitting comment. */ getFormData($form) { + const content = $form.find('.js-note-text').val(); return { formData: $form.serialize(), - formContent: _.escape($form.find('.js-note-text').val()), + formContent: _.escape(content), formAction: $form.attr('action'), + formContentOriginal: content, }; } @@ -1415,7 +1417,7 @@ export default class Notes { const isMainForm = $form.hasClass('js-main-target-form'); const isDiscussionForm = $form.hasClass('js-discussion-note-form'); const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button'); - const { formData, formContent, formAction } = this.getFormData($form); + const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form); let noteUniqueId; let systemNoteUniqueId; let hasQuickActions = false; @@ -1574,7 +1576,7 @@ export default class Notes { $form = $notesContainer.parent().find('form'); } - $form.find('.js-note-text').val(formContent); + $form.find('.js-note-text').val(formContentOriginal); this.reenableTargetFormSubmitButton(e); this.addNoteError($form); }); diff --git a/app/assets/javascripts/repo/components/new_branch_form.vue b/app/assets/javascripts/repo/components/new_branch_form.vue new file mode 100644 index 00000000000..eac43e692b0 --- /dev/null +++ b/app/assets/javascripts/repo/components/new_branch_form.vue @@ -0,0 +1,115 @@ +<script> + import flash, { hideFlash } from '../../flash'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import eventHub from '../event_hub'; + + export default { + components: { + loadingIcon, + }, + props: { + currentBranch: { + type: String, + required: true, + }, + }, + data() { + return { + branchName: '', + loading: false, + }; + }, + computed: { + btnDisabled() { + return this.loading || this.branchName === ''; + }, + }, + methods: { + toggleDropdown() { + this.$dropdown.dropdown('toggle'); + }, + submitNewBranch() { + // need to query as the element is appended outside of Vue + const flashEl = this.$refs.flashContainer.querySelector('.flash-alert'); + + this.loading = true; + + if (flashEl) { + hideFlash(flashEl, false); + } + + eventHub.$emit('createNewBranch', this.branchName); + }, + showErrorMessage(message) { + this.loading = false; + flash(message, 'alert', this.$el); + }, + createdNewBranch(newBranchName) { + this.loading = false; + this.branchName = ''; + + if (this.dropdownText) { + this.dropdownText.textContent = newBranchName; + } + }, + }, + created() { + // Dropdown is outside of Vue instance & is controlled by Bootstrap + this.$dropdown = $('.git-revision-dropdown'); + + // text element is outside Vue app + this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text'); + + eventHub.$on('createNewBranchSuccess', this.createdNewBranch); + eventHub.$on('createNewBranchError', this.showErrorMessage); + eventHub.$on('toggleNewBranchDropdown', this.toggleDropdown); + }, + destroyed() { + eventHub.$off('createNewBranchSuccess', this.createdNewBranch); + eventHub.$off('toggleNewBranchDropdown', this.toggleDropdown); + eventHub.$off('createNewBranchError', this.showErrorMessage); + }, + }; +</script> + +<template> + <div> + <div + class="flash-container" + ref="flashContainer" + > + </div> + <p> + Create from: + <code>{{ currentBranch }}</code> + </p> + <input + class="form-control js-new-branch-name" + type="text" + placeholder="Name new branch" + v-model="branchName" + @keyup.enter.stop.prevent="submitNewBranch" + /> + <div class="prepend-top-default clearfix"> + <button + type="button" + class="btn btn-primary pull-left" + :disabled="btnDisabled" + @click.stop.prevent="submitNewBranch" + > + <loading-icon + v-if="loading" + :inline="true" + /> + <span>Create</span> + </button> + <button + type="button" + class="btn btn-default pull-right" + @click.stop.prevent="toggleDropdown" + > + Cancel + </button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue index 6ab98a33d15..788976a9804 100644 --- a/app/assets/javascripts/repo/components/repo.vue +++ b/app/assets/javascripts/repo/components/repo.vue @@ -8,7 +8,9 @@ import RepoMixin from '../mixins/repo_mixin'; import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; import Store from '../stores/repo_store'; import Helper from '../helpers/repo_helper'; +import Service from '../services/repo_service'; import MonacoLoaderHelper from '../helpers/monaco_loader_helper'; +import eventHub from '../event_hub'; export default { data() { @@ -24,12 +26,19 @@ export default { PopupDialog, RepoPreview, }, - + created() { + eventHub.$on('createNewBranch', this.createNewBranch); + }, mounted() { Helper.getContent().catch(Helper.loadingError); }, - + destroyed() { + eventHub.$off('createNewBranch', this.createNewBranch); + }, methods: { + getCurrentLocation() { + return location.href; + }, toggleDialogOpen(toggle) { this.dialog.open = toggle; }, @@ -42,8 +51,25 @@ export default { Helper.removeAllTmpFiles('openedFiles'); Helper.removeAllTmpFiles('files'); }, - toggleBlobView: Store.toggleBlobView, + createNewBranch(branch) { + Service.createBranch({ + branch, + ref: Store.currentBranch, + }).then((res) => { + const newBranchName = res.data.name; + const newUrl = this.getCurrentLocation().replace(Store.currentBranch, newBranchName); + + Store.currentBranch = newBranchName; + + history.pushState({ key: Helper.key }, '', newUrl); + + eventHub.$emit('createNewBranchSuccess', newBranchName); + eventHub.$emit('toggleNewBranchDropdown'); + }).catch((err) => { + eventHub.$emit('createNewBranchError', err.response.data.message); + }); + }, }, }; </script> diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js index 3586b5fea67..72fc5a70648 100644 --- a/app/assets/javascripts/repo/index.js +++ b/app/assets/javascripts/repo/index.js @@ -5,6 +5,7 @@ import Service from './services/repo_service'; import Store from './stores/repo_store'; import Repo from './components/repo.vue'; import RepoEditButton from './components/repo_edit_button.vue'; +import newBranchForm from './components/new_branch_form.vue'; import newDropdown from './components/new_dropdown/index.vue'; import Translate from '../vue_shared/translate'; @@ -76,6 +77,26 @@ function initNewDropdown(el) { }); } +function initNewBranchForm() { + const el = document.querySelector('.js-new-branch-dropdown'); + + if (!el) return null; + + return new Vue({ + el, + components: { + newBranchForm, + }, + render(createElement) { + return createElement('new-branch-form', { + props: { + currentBranch: Store.currentBranch, + }, + }); + }, + }); +} + function initRepoBundle() { const repo = document.getElementById('repo'); const editButton = document.querySelector('.editable-mode'); @@ -88,6 +109,7 @@ function initRepoBundle() { initRepo(repo); initRepoEditButton(editButton); + initNewBranchForm(); initNewDropdown(newDropdownHolder); } diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js index de8a2ecaa1d..c9fa5cc8bf8 100644 --- a/app/assets/javascripts/repo/services/repo_service.js +++ b/app/assets/javascripts/repo/services/repo_service.js @@ -1,8 +1,11 @@ import axios from 'axios'; +import csrf from '../../lib/utils/csrf'; import Store from '../stores/repo_store'; import Api from '../../api'; import Helper from '../helpers/repo_helper'; +axios.defaults.headers.common[csrf.headerKey] = csrf.token; + const RepoService = { url: '', options: { @@ -10,6 +13,7 @@ const RepoService = { format: 'json', }, }, + createBranchPath: '/api/:version/projects/:id/repository/branches', richExtensionRegExp: /md/, getRaw(file) { @@ -79,6 +83,12 @@ const RepoService = { .then(this.commitFlash); }, + createBranch(payload) { + const url = Api.buildUrl(this.createBranchPath) + .replace(':id', Store.projectId); + return axios.post(url, payload); + }, + commitFlash(data) { if (data.short_id && data.stats) { window.Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice'); diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js index 1214419f553..38df1e3e0d2 100644 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ b/app/assets/javascripts/repo/stores/repo_store.js @@ -13,6 +13,7 @@ const RepoStore = { projectId: '', projectName: '', projectUrl: '', + branchUrl: '', blobRaw: '', currentBlobView: 'repo-preview', openedFiles: [], diff --git a/app/views/peek/views/_gitaly.html.haml b/app/views/peek/views/_gitaly.html.haml new file mode 100644 index 00000000000..a7d040d6821 --- /dev/null +++ b/app/views/peek/views/_gitaly.html.haml @@ -0,0 +1,7 @@ +- local_assigns.fetch(:view) + +%strong + %span{ data: { defer_to: "#{view.defer_key}-duration" } } ... + \/ + %span{ data: { defer_to: "#{view.defer_key}-calls" } } ... + Gitaly diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index df58e257a4e..7ea19e6c828 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -1,6 +1,6 @@ .tree-ref-container .tree-ref-holder - = render 'shared/ref_switcher', destination: 'tree', path: @path + = render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true - if show_new_repo? .js-new-dropdown diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 7ad743b3b81..6d7c9633913 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -1,3 +1,4 @@ +- show_new_branch_form = show_new_repo? && show_create && can?(current_user, :push_code, @project) - dropdown_toggle_text = @ref || @project.default_branch = form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do = hidden_field_tag :destination, destination @@ -7,8 +8,20 @@ = hidden_field_tag key, value, id: nil .dropdown = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" } - .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } - = dropdown_title _("Switch branch/tag") - = dropdown_filter _("Search branches and tags") - = dropdown_content - = dropdown_loading + .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } + .dropdown-page-one + = dropdown_title _("Switch branch/tag") + = dropdown_filter _("Search branches and tags") + = dropdown_content + = dropdown_loading + - if show_new_branch_form + = dropdown_footer do + %ul.dropdown-footer-list + %li + %a.dropdown-toggle-page{ href: "#" } + Create new branch + - if show_new_branch_form + .dropdown-page-two + = dropdown_title("Create new branch", options: { back: true }) + = dropdown_content do + .js-new-branch-dropdown diff --git a/changelogs/unreleased/zj-peek-gitaly.yml b/changelogs/unreleased/zj-peek-gitaly.yml new file mode 100644 index 00000000000..bd2f2a07540 --- /dev/null +++ b/changelogs/unreleased/zj-peek-gitaly.yml @@ -0,0 +1,5 @@ +--- +title: Add Gitaly metrics to the performance bar +merge_request: +author: +type: other diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb index a54d53cbbe2..1cff355346c 100644 --- a/config/initializers/peek.rb +++ b/config/initializers/peek.rb @@ -16,6 +16,7 @@ Peek.into Peek::Views::Redis Peek.into Peek::Views::Sidekiq Peek.into Peek::Views::Rblineprof Peek.into Peek::Views::GC +Peek.into Peek::Views::Gitaly # rubocop:disable Style/ClassAndModuleCamelCase class PEEK_DB_CLIENT diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 6c1ae19ff11..6868be26758 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -33,6 +33,12 @@ module Gitlab MUTEX = Mutex.new private_constant :MUTEX + class << self + attr_accessor :query_time + end + + self.query_time = 0 + def self.stub(name, storage) MUTEX.synchronize do @stubs ||= {} @@ -83,11 +89,14 @@ module Gitlab # end # def self.call(storage, service, rpc, request) + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) enforce_gitaly_request_limits(:call) kwargs = request_kwargs(storage) kwargs = yield(kwargs) if block_given? stub(service, storage).__send__(rpc, request, kwargs) # rubocop:disable GitlabSecurity/PublicSend + ensure + self.query_time += Process.clock_gettime(Process::CLOCK_MONOTONIC) - start end def self.request_kwargs(storage) diff --git a/lib/peek/views/gitaly.rb b/lib/peek/views/gitaly.rb new file mode 100644 index 00000000000..d519d8e86fa --- /dev/null +++ b/lib/peek/views/gitaly.rb @@ -0,0 +1,34 @@ +module Peek + module Views + class Gitaly < View + def duration + ::Gitlab::GitalyClient.query_time + end + + def calls + ::Gitlab::GitalyClient.get_request_count + end + + def results + { duration: formatted_duration, calls: calls } + end + + private + + def formatted_duration + ms = duration * 1000 + if ms >= 1000 + "%.2fms" % ms + else + "%.0fms" % ms + end + end + + def setup_subscribers + subscribe 'start_processing.action_controller' do + ::Gitlab::GitalyClient.query_time = 0 + end + end + end + end +end diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb index f8695403857..50c0bfd580d 100644 --- a/spec/features/projects/ref_switcher_spec.rb +++ b/spec/features/projects/ref_switcher_spec.rb @@ -6,6 +6,7 @@ feature 'Ref switcher', :js do before do project.team << [user, :master] + page.driver.set_cookie('new_repo', 'true') sign_in(user) visit project_tree_path(project, 'master') end @@ -40,4 +41,38 @@ feature 'Ref switcher', :js do expect(page).to have_title "'test'" end + + context "create branch" do + let(:input) { find('.js-new-branch-name') } + + before do + click_button 'master' + wait_for_requests + + page.within '.project-refs-form' do + find(".dropdown-footer-list a").click + end + end + + it "shows error message for the invalid branch name" do + input.set 'foo bar' + click_button('Create') + wait_for_requests + expect(page).to have_content 'Branch name is invalid' + end + + it "should create new branch properly" do + input.set 'new-branch-name' + click_button('Create') + wait_for_requests + expect(find('.js-project-refs-dropdown')).to have_content 'new-branch-name' + end + + it "should create new branch by Enter key" do + input.set 'new-branch-name-2' + input.native.send_keys :enter + wait_for_requests + expect(find('.js-project-refs-dropdown')).to have_content 'new-branch-name-2' + end + end end diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 66c52611614..4546b88e44d 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -103,6 +103,16 @@ import '~/notes'; $('.js-comment-button').click(); expect(this.autoSizeSpy).toHaveBeenTriggered(); }); + + it('should not place escaped text in the comment box in case of error', function() { + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + $(textarea).text('A comment with `markup`.'); + + deferred.reject(); + $('.js-comment-button').click(); + expect($(textarea).val()).toEqual('A comment with `markup`.'); + }); }); describe('updateNote', () => { diff --git a/spec/javascripts/repo/components/new_branch_form_spec.js b/spec/javascripts/repo/components/new_branch_form_spec.js new file mode 100644 index 00000000000..c9c5ce096fc --- /dev/null +++ b/spec/javascripts/repo/components/new_branch_form_spec.js @@ -0,0 +1,122 @@ +import Vue from 'vue'; +import newBranchForm from '~/repo/components/new_branch_form.vue'; +import eventHub from '~/repo/event_hub'; +import RepoStore from '~/repo/stores/repo_store'; +import createComponent from '../../helpers/vue_mount_component_helper'; + +describe('Multi-file editor new branch form', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(newBranchForm); + + RepoStore.currentBranch = 'master'; + + vm = createComponent(Component, { + currentBranch: RepoStore.currentBranch, + }); + }); + + afterEach(() => { + vm.$destroy(); + + RepoStore.currentBranch = ''; + }); + + describe('template', () => { + it('renders submit as disabled', () => { + expect(vm.$el.querySelector('.btn').getAttribute('disabled')).toBe('disabled'); + }); + + it('enables the submit button when branch is not empty', (done) => { + vm.branchName = 'testing'; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn').getAttribute('disabled')).toBeNull(); + + done(); + }); + }); + + it('displays current branch creating from', (done) => { + Vue.nextTick(() => { + expect(vm.$el.querySelector('p').textContent.replace(/\s+/g, ' ').trim()).toBe('Create from: master'); + + done(); + }); + }); + }); + + describe('submitNewBranch', () => { + it('sets to loading', () => { + vm.submitNewBranch(); + + expect(vm.loading).toBeTruthy(); + }); + + it('hides current flash element', (done) => { + vm.$refs.flashContainer.innerHTML = '<div class="flash-alert"></div>'; + + vm.submitNewBranch(); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.flash-alert')).toBeNull(); + + done(); + }); + }); + + it('emits an event with branchName', () => { + spyOn(eventHub, '$emit'); + + vm.branchName = 'testing'; + + vm.submitNewBranch(); + + expect(eventHub.$emit).toHaveBeenCalledWith('createNewBranch', 'testing'); + }); + }); + + describe('showErrorMessage', () => { + it('sets loading to false', () => { + vm.loading = true; + + vm.showErrorMessage(); + + expect(vm.loading).toBeFalsy(); + }); + + it('creates flash element', () => { + vm.showErrorMessage('error message'); + + expect(vm.$el.querySelector('.flash-alert')).not.toBeNull(); + expect(vm.$el.querySelector('.flash-alert').textContent.trim()).toBe('error message'); + }); + }); + + describe('createdNewBranch', () => { + it('set loading to false', () => { + vm.loading = true; + + vm.createdNewBranch(); + + expect(vm.loading).toBeFalsy(); + }); + + it('resets branch name', () => { + vm.branchName = 'testing'; + + vm.createdNewBranch(); + + expect(vm.branchName).toBe(''); + }); + + it('sets the dropdown toggle text', () => { + vm.dropdownText = document.createElement('span'); + + vm.createdNewBranch('branch name'); + + expect(vm.dropdownText.textContent).toBe('branch name'); + }); + }); +}); diff --git a/spec/javascripts/repo/components/repo_spec.js b/spec/javascripts/repo/components/repo_spec.js new file mode 100644 index 00000000000..3558a155728 --- /dev/null +++ b/spec/javascripts/repo/components/repo_spec.js @@ -0,0 +1,96 @@ +import Vue from 'vue'; +import repo from '~/repo/components/repo.vue'; +import RepoStore from '~/repo/stores/repo_store'; +import Service from '~/repo/services/repo_service'; +import eventHub from '~/repo/event_hub'; +import createComponent from '../../helpers/vue_mount_component_helper'; + +describe('repo component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(repo); + + RepoStore.currentBranch = 'master'; + + vm = createComponent(Component); + }); + + afterEach(() => { + vm.$destroy(); + + RepoStore.currentBranch = ''; + }); + + describe('createNewBranch', () => { + beforeEach(() => { + spyOn(history, 'pushState'); + }); + + describe('success', () => { + beforeEach(() => { + spyOn(Service, 'createBranch').and.returnValue(Promise.resolve({ + data: { + name: 'test', + }, + })); + }); + + it('calls createBranch with branchName', () => { + eventHub.$emit('createNewBranch', 'test'); + + expect(Service.createBranch).toHaveBeenCalledWith({ + branch: 'test', + ref: RepoStore.currentBranch, + }); + }); + + it('pushes new history state', (done) => { + RepoStore.currentBranch = 'master'; + + spyOn(vm, 'getCurrentLocation').and.returnValue('http://test.com/master'); + + eventHub.$emit('createNewBranch', 'test'); + + setTimeout(() => { + expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'http://test.com/test'); + done(); + }); + }); + + it('updates stores currentBranch', (done) => { + eventHub.$emit('createNewBranch', 'test'); + + setTimeout(() => { + expect(RepoStore.currentBranch).toBe('test'); + + done(); + }); + }); + }); + + describe('failure', () => { + beforeEach(() => { + spyOn(Service, 'createBranch').and.returnValue(Promise.reject({ + response: { + data: { + message: 'test', + }, + }, + })); + }); + + it('emits createNewBranchError event', (done) => { + spyOn(eventHub, '$emit').and.callThrough(); + + eventHub.$emit('createNewBranch', 'test'); + + setTimeout(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('createNewBranchError', 'test'); + + done(); + }); + }); + }); + }); +}); |