From 5bce197b617f2542430db7aecec321cf1619de72 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 4 May 2017 23:45:02 +0300 Subject: Serialize groups as json for Dashboard::GroupsController Signed-off-by: Dmitriy Zaporozhets --- app/controllers/dashboard/groups_controller.rb | 27 +++++++++++++++++++------- app/serializers/group_entity.rb | 16 +++++++++++++++ app/serializers/group_serializer.rb | 19 ++++++++++++++++++ app/views/dashboard/groups/_groups.html.haml | 5 +++-- app/views/dashboard/groups/index.html.haml | 2 +- 5 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 app/serializers/group_entity.rb create mode 100644 app/serializers/group_serializer.rb diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb index d03265e9f20..b339344dd40 100644 --- a/app/controllers/dashboard/groups_controller.rb +++ b/app/controllers/dashboard/groups_controller.rb @@ -1,16 +1,29 @@ class Dashboard::GroupsController < Dashboard::ApplicationController def index - @group_members = current_user.group_members.includes(source: :route).joins(:group) - @group_members = @group_members.merge(Group.search(params[:filter_groups])) if params[:filter_groups].present? - @group_members = @group_members.merge(Group.sort(@sort = params[:sort])) - @group_members = @group_members.page(params[:page]) + @groups = if params[:parent_id] + parent = Group.find(params[:parent_id]) + + if parent.users_with_parents.find_by(id: current_user) + Group.where(id: parent.children) + else + Group.none + end + else + Group.joins(:group_members).merge(current_user.group_members) + end + + @groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present? + @groups = @groups.includes(:route) + @groups = @groups.sort(@sort = params[:sort]) + @groups = @groups.page(params[:page]) respond_to do |format| format.html format.json do - render json: { - html: view_to_html_string("dashboard/groups/_groups", locals: { group_members: @group_members }) - } + render json: GroupSerializer + .new(current_user: @current_user) + .with_pagination(request, response) + .represent(@groups) end end end diff --git a/app/serializers/group_entity.rb b/app/serializers/group_entity.rb new file mode 100644 index 00000000000..33f1fbff31d --- /dev/null +++ b/app/serializers/group_entity.rb @@ -0,0 +1,16 @@ +class GroupEntity < Grape::Entity + include RequestAwareEntity + + expose :id, :name, :path, :description, :visibility + expose :avatar_url + expose :web_url + expose :full_name, :full_path + expose :parent_id + expose :created_at, :updated_at + + expose :permissions do + expose :group_access do |group, options| + group.group_members.find_by(user_id: request.current_user)&.access_level + end + end +end diff --git a/app/serializers/group_serializer.rb b/app/serializers/group_serializer.rb new file mode 100644 index 00000000000..26e8566828b --- /dev/null +++ b/app/serializers/group_serializer.rb @@ -0,0 +1,19 @@ +class GroupSerializer < BaseSerializer + entity GroupEntity + + def with_pagination(request, response) + tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) } + end + + def paginated? + @paginator.present? + end + + def represent(resource, opts = {}) + if paginated? + super(@paginator.paginate(resource), opts) + else + super(resource, opts) + end + end +end diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml index 6c3bf1a2b3b..d33ee450b29 100644 --- a/app/views/dashboard/groups/_groups.html.haml +++ b/app/views/dashboard/groups/_groups.html.haml @@ -1,6 +1,7 @@ .js-groups-list-holder %ul.content-list - - @group_members.each do |group_member| + - @groups.each do |group| + - group_member = group.group_members.find_by(user_id: current_user) = render 'shared/groups/group', group: group_member.group, group_member: group_member - = paginate @group_members, theme: 'gitlab' + = paginate @groups, theme: 'gitlab' diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 73ab2c95ff9..0d35d11fb63 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -2,7 +2,7 @@ - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' -- if @group_members.empty? +- if @groups.empty? = render 'empty_state' - else = render 'groups' -- cgit v1.2.1 From 1dc2b4693e4a58c94e556ae219ae6200044f95dc Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 4 May 2017 19:29:56 -0500 Subject: =?UTF-8?q?Add=20=E2=80=9Cgroups=E2=80=9D=20JS=20bundle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/assets/javascripts/groups/index.js | 3 +++ app/views/dashboard/groups/index.html.haml | 2 ++ config/webpack.config.js | 1 + 3 files changed, 6 insertions(+) create mode 100644 app/assets/javascripts/groups/index.js diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js new file mode 100644 index 00000000000..c0e083ef635 --- /dev/null +++ b/app/assets/javascripts/groups/index.js @@ -0,0 +1,3 @@ +$(() => { + // Groups bundle +}); \ No newline at end of file diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 0d35d11fb63..bf1013c685b 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -2,6 +2,8 @@ - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' += page_specific_javascript_bundle_tag('groups') + - if @groups.empty? = render 'empty_state' - else diff --git a/config/webpack.config.js b/config/webpack.config.js index 0ec9e48845e..966b1e2283e 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -32,6 +32,7 @@ var config = { filtered_search: './filtered_search/filtered_search_bundle.js', graphs: './graphs/graphs_bundle.js', group: './group.js', + groups: './groups/index.js', groups_list: './groups_list.js', issuable: './issuable/issuable_bundle.js', issue_show: './issue_show/index.js', -- cgit v1.2.1 From d67ab685350005b83a12988845b7fb87c613b472 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 4 May 2017 20:49:07 -0500 Subject: Set Groups Vue app for Dashboard page --- app/assets/javascripts/groups/index.js | 12 ++++++++++-- app/assets/javascripts/groups/stores/groups_store.js | 5 +++++ app/views/dashboard/groups/index.html.haml | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/groups/stores/groups_store.js diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index c0e083ef635..934e3b8e580 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -1,3 +1,11 @@ +import Vue from 'vue'; +import GroupsStore from './stores/groups_store'; + $(() => { - // Groups bundle -}); \ No newline at end of file + const groupsStore = new GroupsStore(); + + const GroupsApp = new Vue({ + el: document.querySelector('.js-groups-list-holder'), + data: groupsStore, + }); +}); diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js new file mode 100644 index 00000000000..f62f419ac1b --- /dev/null +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -0,0 +1,5 @@ +export default class GroupsStore { + constructor() { + this.groups = []; + } +} diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index bf1013c685b..af9f9b1b363 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -2,6 +2,7 @@ - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' += page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('groups') - if @groups.empty? -- cgit v1.2.1 From 265736052906fefc5ff57c3958158b1f563c2a9e Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 4 May 2017 21:34:56 -0500 Subject: Add GroupsService to fetch data from server --- app/assets/javascripts/groups/index.js | 17 ++++++++++++++++- .../javascripts/groups/services/groups_service.js | 14 ++++++++++++++ app/views/dashboard/groups/_groups.html.haml | 2 +- 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/groups/services/groups_service.js diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 934e3b8e580..f9fe3f7f341 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -1,11 +1,26 @@ +/* eslint-disable no-unused-vars */ + import Vue from 'vue'; import GroupsStore from './stores/groups_store'; +import GroupsService from './services/groups_service'; $(() => { + const appEl = document.querySelector('.js-groups-list-holder'); + const groupsStore = new GroupsStore(); + const groupsService = new GroupsService(appEl.dataset.endpoint); const GroupsApp = new Vue({ - el: document.querySelector('.js-groups-list-holder'), + el: appEl, data: groupsStore, + mounted() { + groupsService.getGroups() + .then((response) => { + this.groups = response.json(); + }) + .catch(() => { + // TODO: Handle error + }); + }, }); }); diff --git a/app/assets/javascripts/groups/services/groups_service.js b/app/assets/javascripts/groups/services/groups_service.js new file mode 100644 index 00000000000..4c5ce87f396 --- /dev/null +++ b/app/assets/javascripts/groups/services/groups_service.js @@ -0,0 +1,14 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +Vue.use(VueResource); + +export default class GroupsService { + constructor(endpoint) { + this.groups = Vue.resource(endpoint); + } + + getGroups() { + return this.groups.get(); + } +} diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml index d33ee450b29..9e19b6bc347 100644 --- a/app/views/dashboard/groups/_groups.html.haml +++ b/app/views/dashboard/groups/_groups.html.haml @@ -1,4 +1,4 @@ -.js-groups-list-holder +.js-groups-list-holder{ data: { endpoint: dashboard_groups_path(format: :json) } } %ul.content-list - @groups.each do |group| - group_member = group.group_members.find_by(user_id: current_user) -- cgit v1.2.1 From 5e0e3971b8c396c03375404c98874d9c18221668 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 5 May 2017 23:15:04 -0500 Subject: List groups with basic details - Adds Groups component - Adds GroupItem component --- .../javascripts/groups/components/group_item.vue | 21 ++++++++++ .../javascripts/groups/components/groups.vue | 45 ++++++++++++++++++++++ app/assets/javascripts/groups/index.js | 20 +++------- .../javascripts/groups/stores/groups_store.js | 11 +++++- app/views/dashboard/groups/_groups.html.haml | 8 +--- 5 files changed, 82 insertions(+), 23 deletions(-) create mode 100644 app/assets/javascripts/groups/components/group_item.vue create mode 100644 app/assets/javascripts/groups/components/groups.vue diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue new file mode 100644 index 00000000000..2db9672547a --- /dev/null +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -0,0 +1,21 @@ + + + diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue new file mode 100644 index 00000000000..341855b9915 --- /dev/null +++ b/app/assets/javascripts/groups/components/groups.vue @@ -0,0 +1,45 @@ + + + diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index f9fe3f7f341..883e9cb4187 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -1,26 +1,16 @@ /* eslint-disable no-unused-vars */ import Vue from 'vue'; -import GroupsStore from './stores/groups_store'; -import GroupsService from './services/groups_service'; +import GroupsComponent from './components/groups.vue' $(() => { - const appEl = document.querySelector('.js-groups-list-holder'); - - const groupsStore = new GroupsStore(); - const groupsService = new GroupsService(appEl.dataset.endpoint); + const appEl = document.querySelector('#dashboard-group-app'); const GroupsApp = new Vue({ el: appEl, - data: groupsStore, - mounted() { - groupsService.getGroups() - .then((response) => { - this.groups = response.json(); - }) - .catch(() => { - // TODO: Handle error - }); + components: { + 'groups-component': GroupsComponent }, + render: createElement => createElement('groups-component'), }); }); diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index f62f419ac1b..0e1820b7335 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -1,5 +1,14 @@ export default class GroupsStore { constructor() { - this.groups = []; + this.state = {}; + this.state.groups = []; + + return this; + } + + setGroups(groups) { + this.state.groups = groups; + + return groups; } } diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml index 9e19b6bc347..e7a5fa8ba32 100644 --- a/app/views/dashboard/groups/_groups.html.haml +++ b/app/views/dashboard/groups/_groups.html.haml @@ -1,7 +1 @@ -.js-groups-list-holder{ data: { endpoint: dashboard_groups_path(format: :json) } } - %ul.content-list - - @groups.each do |group| - - group_member = group.group_members.find_by(user_id: current_user) - = render 'shared/groups/group', group: group_member.group, group_member: group_member - - = paginate @groups, theme: 'gitlab' +.js-groups-list-holder#dashboard-group-app{ data: { endpoint: dashboard_groups_path(format: :json) } } -- cgit v1.2.1 From 4b488d72a263b56b9701dde818ca6b77b038ca89 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 8 May 2017 15:22:26 -0500 Subject: Prepare groups components for subgroups --- .../javascripts/groups/components/group_item.vue | 39 +++++++++++++++++----- .../javascripts/groups/components/groups.vue | 18 ++++++++-- app/assets/javascripts/groups/event_hub.js | 3 ++ app/assets/javascripts/groups/index.js | 4 +-- .../javascripts/groups/stores/groups_store.js | 19 ++++++++++- 5 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 app/assets/javascripts/groups/event_hub.js diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 2db9672547a..98f1d516cb2 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -1,21 +1,42 @@ diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 341855b9915..26ed0990ef5 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -2,6 +2,7 @@ import GroupsStore from '../stores/groups_store'; import GroupsService from '../services/groups_service'; import GroupItem from '../components/group_item.vue'; +import eventHub from '../event_hub'; export default { components: { @@ -14,7 +15,7 @@ export default { return { store, state: store.state, - } + }; }, created() { @@ -22,6 +23,8 @@ export default { this.service = new GroupsService(appEl.dataset.endpoint); this.fetchGroups(); + + eventHub.$on('toggleSubGroups', this.toggleSubGroups); }, methods: { @@ -34,12 +37,21 @@ export default { // TODO: Handler error }); }, - } + toggleSubGroups(group) { + GroupsStore.toggleSubGroups(group); + }, + }, }; diff --git a/app/assets/javascripts/groups/event_hub.js b/app/assets/javascripts/groups/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/groups/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 883e9cb4187..81385dfd981 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -1,7 +1,7 @@ /* eslint-disable no-unused-vars */ import Vue from 'vue'; -import GroupsComponent from './components/groups.vue' +import GroupsComponent from './components/groups.vue'; $(() => { const appEl = document.querySelector('#dashboard-group-app'); @@ -9,7 +9,7 @@ $(() => { const GroupsApp = new Vue({ el: appEl, components: { - 'groups-component': GroupsComponent + 'groups-component': GroupsComponent, }, render: createElement => createElement('groups-component'), }); diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index 0e1820b7335..2b74447c7f9 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -7,8 +7,25 @@ export default class GroupsStore { } setGroups(groups) { - this.state.groups = groups; + this.state.groups = this.decorateGroups(groups); return groups; } + + decorateGroups(rawGroups) { + this.groups = rawGroups.map(GroupsStore.decorateGroup); + return this.groups; + } + + static decorateGroup(rawGroup) { + const group = rawGroup; + group.isOpen = false; + return group; + } + + static toggleSubGroups(toggleGroup) { + const group = toggleGroup; + group.isOpen = !group.isOpen; + return group; + } } -- cgit v1.2.1 From 4c3753387b9bb46d0d257c90720c6e27a258c37a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 9 May 2017 18:10:19 -0500 Subject: Set tree structure for groups --- .../javascripts/groups/components/group_folder.vue | 18 +++++++++ .../javascripts/groups/components/group_item.vue | 43 +++++++++++----------- .../javascripts/groups/components/groups.vue | 19 +++------- app/assets/javascripts/groups/index.js | 9 +++-- 4 files changed, 50 insertions(+), 39 deletions(-) create mode 100644 app/assets/javascripts/groups/components/group_folder.vue diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue new file mode 100644 index 00000000000..5485da58ec5 --- /dev/null +++ b/app/assets/javascripts/groups/components/group_folder.vue @@ -0,0 +1,18 @@ + + + diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 98f1d516cb2..627eae1cd0d 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -10,6 +10,10 @@ export default { }, methods: { toggleSubGroups() { + if (this.group.subGroups && !this.group.subGroups.length) { + return; + } + eventHub.$emit('toggleSubGroups', this.group); }, }, @@ -17,26 +21,21 @@ export default { diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 26ed0990ef5..797fca9bd49 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -1,14 +1,9 @@ diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 81385dfd981..8a7c01161a1 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -2,15 +2,18 @@ import Vue from 'vue'; import GroupsComponent from './components/groups.vue'; +import GroupFolder from './components/group_folder.vue'; +import GroupItem from './components/group_item.vue'; $(() => { const appEl = document.querySelector('#dashboard-group-app'); + Vue.component('groups-component', GroupsComponent); + Vue.component('group-folder', GroupFolder); + Vue.component('group-item', GroupItem); + const GroupsApp = new Vue({ el: appEl, - components: { - 'groups-component': GroupsComponent, - }, render: createElement => createElement('groups-component'), }); }); -- cgit v1.2.1 From 3b6ff7fcaf4443b518770f97e437631197297980 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 10 May 2017 03:06:51 -0500 Subject: Add support to filter by name to Group list --- app/assets/javascripts/dispatcher.js | 6 ---- app/assets/javascripts/filterable_list.js | 5 +-- .../javascripts/groups/components/group_folder.vue | 2 -- .../javascripts/groups/components/groups.vue | 42 +++------------------- app/assets/javascripts/groups/index.js | 39 +++++++++++++++++++- app/assets/javascripts/groups_list.js | 10 ++---- app/views/dashboard/groups/_groups.html.haml | 4 ++- 7 files changed, 52 insertions(+), 56 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 7e469153106..e4c60ef1188 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -40,7 +40,6 @@ import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; import Group from './group'; import GroupName from './group_name'; -import GroupsList from './groups_list'; import ProjectsList from './projects_list'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; @@ -148,12 +147,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'admin:projects:index': new ProjectsList(); break; - case 'dashboard:groups:index': - new GroupsList(); - break; case 'explore:groups:index': - new GroupsList(); - const landingElement = document.querySelector('.js-explore-groups-landing'); if (!landingElement) break; const exploreGroupsLanding = new Landing( diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index aaaeb9bddb1..e6d6400ca86 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -4,7 +4,8 @@ */ export default class FilterableList { - constructor(form, filter, holder) { + constructor(form, filter, holder, store) { + this.store = store; this.filterForm = form; this.listFilterElement = filter; this.listHolderElement = holder; @@ -33,7 +34,7 @@ export default class FilterableList { $(this.listHolderElement).fadeTo(250, 1); }, success(data) { - this.listHolderElement.innerHTML = data.html; + this.store.setGroups(data); // Change url so if user reload a page - search results are saved return window.history.replaceState({ diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue index 5485da58ec5..75b20111ca1 100644 --- a/app/assets/javascripts/groups/components/group_folder.vue +++ b/app/assets/javascripts/groups/components/group_folder.vue @@ -1,6 +1,4 @@ diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 2ee7ec96dfe..45cc483b06e 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -2,7 +2,7 @@ export default { props: { groups: { - type: Array, + type: Object, required: true, }, }, diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index f8ba11caccc..be89955c101 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -1,6 +1,5 @@ import FilterableList from '~/filterable_list'; - export default class GroupFilterableList extends FilterableList { constructor(form, filter, holder, store) { super(form, filter, holder); diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index d136e64850f..5b94f7b762d 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -31,19 +31,24 @@ $(() => { }; }, methods: { - fetchGroups() { - service.getGroups() + fetchGroups(parentGroup) { + let parentId = null; + + if (parentGroup) { + parentId = parentGroup.id; + } + + service.getGroups(parentId) .then((response) => { - store.setGroups(response.json()); + store.setGroups(response.json(), parentGroup); }) .catch(() => { // TODO: Handler error }); }, - toggleSubGroups(group) { - GroupsStore.toggleSubGroups(group); - - this.fetchGroups(); + toggleSubGroups(parentGroup = null) { + GroupsStore.toggleSubGroups(parentGroup); + this.fetchGroups(parentGroup); }, }, created() { diff --git a/app/assets/javascripts/groups/services/groups_service.js b/app/assets/javascripts/groups/services/groups_service.js index 4c5ce87f396..d92e93d5fa9 100644 --- a/app/assets/javascripts/groups/services/groups_service.js +++ b/app/assets/javascripts/groups/services/groups_service.js @@ -8,7 +8,15 @@ export default class GroupsService { this.groups = Vue.resource(endpoint); } - getGroups() { - return this.groups.get(); + getGroups(parentId) { + let data = {}; + + if (parentId) { + data = { + parent_id: parentId, + }; + } + + return this.groups.get(data); } } diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index c0ef4f776e0..bf43441bd71 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -1,15 +1,48 @@ export default class GroupsStore { constructor() { this.state = {}; - this.state.groups = []; + this.state.groups = {}; return this; } - setGroups(groups) { - this.state.groups = this.decorateGroups(groups); + setGroups(rawGroups, parent = null) { + const parentGroup = parent; - return groups; + if (parentGroup) { + parentGroup.subGroups = this.buildTree(rawGroups); + } else { + this.state.groups = this.buildTree(rawGroups); + } + + return rawGroups; + } + + buildTree(rawGroups) { + const groups = this.decorateGroups(rawGroups); + const tree = {}; + const mappedGroups = {}; + + // Map groups to an object + for (let i = 0, len = groups.length; i < len; i += 1) { + const group = groups[i]; + mappedGroups[group.id] = group; + mappedGroups[group.id].subGroups = {}; + } + + Object.keys(mappedGroups).forEach((key) => { + const currentGroup = mappedGroups[key]; + // If the group is not at the root level, add it to its parent array of subGroups. + if (currentGroup.parentId) { + mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup; + mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups + } else { + // If the group is at the root level, add it to first level elements array. + tree[currentGroup.id] = currentGroup; + } + }); + + return tree; } decorateGroups(rawGroups) { @@ -19,12 +52,14 @@ export default class GroupsStore { static decorateGroup(rawGroup) { return { - fullName: rawGroup.name, + id: rawGroup.id, + fullName: rawGroup.full_name, description: rawGroup.description, webUrl: rawGroup.web_url, - parentId: rawGroup.parentId, - hasSubgroups: !!rawGroup.parent_id, + parentId: rawGroup.parent_id, + expandable: true, isOpen: false, + subGroups: {}, }; } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 72d73b89a2a..8c7e5591d79 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -111,3 +111,9 @@ height: 50px; } } + + +.list-group .list-group { + margin-top: 10px; + margin-bottom: 0; +} -- cgit v1.2.1 From d55a9d4c6426b3c611def994525f065c8d12b514 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 12 May 2017 03:43:39 -0500 Subject: Fix tree generation when filtering by name --- app/assets/javascripts/groups/stores/groups_store.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index bf43441bd71..1bfd8745423 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -33,7 +33,8 @@ export default class GroupsStore { Object.keys(mappedGroups).forEach((key) => { const currentGroup = mappedGroups[key]; // If the group is not at the root level, add it to its parent array of subGroups. - if (currentGroup.parentId) { + const parentGroup = mappedGroups[currentGroup['parentId']]; + if (currentGroup.parentId && parentGroup) { mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup; mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups } else { -- cgit v1.2.1 From fdba7449867ab49aed7cb39449ac41af4bc2d234 Mon Sep 17 00:00:00 2001 From: Alexander Randa Date: Fri, 21 Apr 2017 15:31:18 +0000 Subject: Fix long urls in the title of commit --- app/models/commit.rb | 27 ++++++++++-------------- changelogs/unreleased/12614-fix-long-message.yml | 4 ++++ spec/models/commit_spec.rb | 26 +++++++++++++++++++++-- 3 files changed, 39 insertions(+), 18 deletions(-) create mode 100644 changelogs/unreleased/12614-fix-long-message.yml diff --git a/app/models/commit.rb b/app/models/commit.rb index 8b8b3f00202..f21ef76fafc 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -114,16 +114,16 @@ class Commit # # Usually, the commit title is the first line of the commit message. # In case this first line is longer than 100 characters, it is cut off - # after 80 characters and ellipses (`&hellp;`) are appended. + # after 80 characters + `...` def title - full_title.length > 100 ? full_title[0..79] << "…" : full_title + return full_title if full_title.length < 100 + + full_title.truncate(81, separator: ' ', omission: '…') end # Returns the full commits title def full_title - return @full_title if @full_title - - @full_title = + @full_title ||= if safe_message.blank? no_commit_message else @@ -131,19 +131,14 @@ class Commit end end - # Returns the commits description - # - # cut off, ellipses (`&hellp;`) are prepended to the commit message. + # Returns full commit message if title is truncated (greater than 99 characters) + # otherwise returns commit message without first line def description - title_end = safe_message.index("\n") - @description ||= - if (!title_end && safe_message.length > 100) || (title_end && title_end > 100) - "…" << safe_message[80..-1] - else - safe_message.split("\n", 2)[1].try(:chomp) - end - end + return safe_message if full_title.length >= 100 + safe_message.split("\n", 2)[1].try(:chomp) + end + def description? description.present? end diff --git a/changelogs/unreleased/12614-fix-long-message.yml b/changelogs/unreleased/12614-fix-long-message.yml new file mode 100644 index 00000000000..94f8127c3c1 --- /dev/null +++ b/changelogs/unreleased/12614-fix-long-message.yml @@ -0,0 +1,4 @@ +--- +title: Fix long urls in the title of commit +merge_request: 10938 +author: Alexander Randa diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index ce31c8ed94c..3a76e207b0d 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -67,11 +67,11 @@ describe Commit, models: true do expect(commit.title).to eq("--no commit message") end - it "truncates a message without a newline at 80 characters" do + it 'truncates a message without a newline at natural break to 80 characters' do message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.' allow(commit).to receive(:safe_message).and_return(message) - expect(commit.title).to eq("#{message[0..79]}…") + expect(commit.title).to eq('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis…') end it "truncates a message with a newline before 80 characters at the newline" do @@ -113,6 +113,28 @@ eos end end + describe 'description' do + it 'returns description of commit message if title less than 100 characters' do + message = < Date: Mon, 15 May 2017 16:32:09 -0500 Subject: Stop event propagation to prevent multiple ajax calls --- app/assets/javascripts/groups/components/group_item.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 07fa648b237..3689804d576 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -18,7 +18,7 @@ export default { -- cgit v1.2.1 From 5bd52eaefb11e3568b3e78d21efd0f1dabf328b8 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 17:04:25 -0500 Subject: Use object destructuring when passing more than 3 params to follow style guide --- app/assets/javascripts/groups/groups_filterable_list.js | 2 +- app/assets/javascripts/groups/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index 9a8797caf36..d1347dc5a6e 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -1,7 +1,7 @@ import FilterableList from '~/filterable_list'; export default class GroupFilterableList extends FilterableList { - constructor(form, filter, holder, store) { + constructor({ form, filter, holder, store }) { super(form, filter, holder); this.store = store; diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 4d0f415bfc2..29dd6709421 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -78,7 +78,7 @@ $(() => { created() { let groupFilterList = null; - groupFilterList = new GroupFilterableList(form, filter, holder, store); + groupFilterList = new GroupFilterableList({ form, filter, holder, store }); groupFilterList.initSearch(); this.fetchGroups() -- cgit v1.2.1 From 24fa70589a82012aed5e324c7473cbc28073f86b Mon Sep 17 00:00:00 2001 From: Regis Date: Wed, 31 May 2017 16:46:32 -0600 Subject: Update CHANGELOG.md for 9.1.5 [ci skip] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ff4648620c..f114da997f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -219,6 +219,12 @@ entry. - Fix preemptive scroll bar on user activity calendar. - Pipeline chat notifications convert seconds to minutes and hours. +## 9.1.5 (2017-05-31) + +- Move uploads from 'public/uploads' to 'public/uploads/system'. +- Restrict API X-Frame-Options to same origin. +- Allow users autocomplete by author_id only for authenticated users. + ## 9.1.4 (2017-05-12) - Fix error on CI/CD Settings page related to invalid pipeline trigger. !10948 (dosuken123) -- cgit v1.2.1 From f4ebf930c629fa267884841c1790f6411f8a1de7 Mon Sep 17 00:00:00 2001 From: Regis Date: Wed, 31 May 2017 16:49:34 -0600 Subject: Update CHANGELOG.md for 9.0.8 [ci skip] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f114da997f0..b1fa9e7c562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -523,6 +523,12 @@ entry. - Only send chat notifications for the default branch. - Don't fill in the default kubernetes namespace. +## 9.0.8 (2017-05-31) + +- Move uploads from 'public/uploads' to 'public/uploads/system'. +- Restrict API X-Frame-Options to same origin. +- Allow users autocomplete by author_id only for authenticated users. + ## 9.0.7 (2017-05-05) - Enforce project features when searching blobs and wikis. -- cgit v1.2.1 From a161384b91e6f06f2afa41f9e0038cc3129f12b1 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 18:29:11 -0500 Subject: Declare store and service inside Vue app --- app/assets/javascripts/groups/index.js | 53 +++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 29dd6709421..26b172f3e94 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -1,5 +1,3 @@ -/* eslint-disable no-unused-vars */ - import Vue from 'vue'; import GroupFilterableList from './groups_filterable_list'; import GroupsComponent from './components/groups.vue'; @@ -9,25 +7,22 @@ import GroupsStore from './stores/groups_store'; import GroupsService from './services/groups_service'; import eventHub from './event_hub'; -$(() => { - const appEl = document.querySelector('#dashboard-group-app'); - const form = document.querySelector('form#group-filter-form'); - const filter = document.querySelector('.js-groups-list-filter'); - const holder = document.querySelector('.js-groups-list-holder'); - - const store = new GroupsStore(); - const service = new GroupsService(appEl.dataset.endpoint); +document.addEventListener('DOMContentLoaded', () => { + const el = document.querySelector('#dashboard-group-app'); Vue.component('groups-component', GroupsComponent); Vue.component('group-folder', GroupFolder); Vue.component('group-item', GroupItem); - const GroupsApp = new Vue({ - el: appEl, + return new Vue({ + el, data() { + this.store = new GroupsStore(); + this.service = new GroupsService(el.dataset.endpoint); + return { - store, - state: store.state, + store: this.store, + state: this.store.state, }; }, methods: { @@ -47,9 +42,9 @@ $(() => { page = pageParam; } - getGroups = service.getGroups(parentId, page); + getGroups = this.service.getGroups(parentId, page); getGroups.then((response) => { - store.setGroups(response.json(), parentGroup); + this.store.setGroups(response.json(), parentGroup); }) .catch(() => { // TODO: Handle error @@ -59,14 +54,14 @@ $(() => { }, toggleSubGroups(parentGroup = null) { if (!parentGroup.isOpen) { - store.resetGroups(parentGroup); + this.store.resetGroups(parentGroup); this.fetchGroups(parentGroup); } GroupsStore.toggleSubGroups(parentGroup); }, leaveGroup(endpoint) { - service.leaveGroup(endpoint) + this.service.leaveGroup(endpoint) .then(() => { // TODO: Refresh? }) @@ -75,22 +70,32 @@ $(() => { }); }, }, - created() { + beforeMount() { let groupFilterList = null; + const form = document.querySelector('form#group-filter-form'); + const filter = document.querySelector('.js-groups-list-filter'); + const holder = document.querySelector('.js-groups-list-holder'); - groupFilterList = new GroupFilterableList({ form, filter, holder, store }); + const options = { + form, + filter, + holder, + store: this.store, + }; + groupFilterList = new GroupFilterableList(options); groupFilterList.initSearch(); + eventHub.$on('toggleSubGroups', this.toggleSubGroups); + eventHub.$on('leaveGroup', this.leaveGroup); + }, + mounted() { this.fetchGroups() .then((response) => { - store.storePagination(response.headers); + this.store.storePagination(response.headers); }) .catch(() => { // TODO: Handle error }); - - eventHub.$on('toggleSubGroups', this.toggleSubGroups); - eventHub.$on('leaveGroup', this.leaveGroup); }, }); }); -- cgit v1.2.1 From abdd18922bb99e4d85266ff6e9995599466e3ca4 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 19:45:05 -0500 Subject: Restore accidentally deleted code --- app/assets/javascripts/groups/components/group_item.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 3c92d6fd1eb..769ef05add6 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -84,6 +84,9 @@ export default { return fullPath; }, + hasGroups() { + return Object.keys(this.group.subGroups).length > 0; + }, }, }; @@ -158,6 +161,6 @@ export default { {{group.description}} - + -- cgit v1.2.1 From 1c1c08020b10257494a570d2f3ed2dec13796b0a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 20:00:34 -0500 Subject: Remove duplicated line --- app/assets/javascripts/groups/services/groups_service.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/groups/services/groups_service.js b/app/assets/javascripts/groups/services/groups_service.js index 86e911b73b9..ddebd1af47f 100644 --- a/app/assets/javascripts/groups/services/groups_service.js +++ b/app/assets/javascripts/groups/services/groups_service.js @@ -8,7 +8,6 @@ Vue.use(VueResource); export default class GroupsService { constructor(endpoint) { this.groups = Vue.resource(endpoint); - this.groups = Vue.resource(endpoint); } getGroups(parentId, page) { -- cgit v1.2.1 From 56a279be975147a800d6cd4879346def058ac4fa Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 20:25:23 -0500 Subject: Move eslint disable rule close to offending line to conform styleguide --- app/assets/javascripts/groups/services/groups_service.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/groups/services/groups_service.js b/app/assets/javascripts/groups/services/groups_service.js index ddebd1af47f..8d67c0244f3 100644 --- a/app/assets/javascripts/groups/services/groups_service.js +++ b/app/assets/javascripts/groups/services/groups_service.js @@ -1,5 +1,3 @@ -/* eslint-disable class-methods-use-this */ - import Vue from 'vue'; import VueResource from 'vue-resource'; @@ -23,6 +21,7 @@ export default class GroupsService { return this.groups.get(data); } + // eslint-disable-next-line class-methods-use-this leaveGroup(endpoint) { return Vue.http.delete(endpoint); } -- cgit v1.2.1 From 9c71b78145c649740815be7ee4f714d4451892a6 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 20:27:37 -0500 Subject: Fix JS error when filtering by option --- app/assets/javascripts/groups/components/group_item.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 769ef05add6..2013469f9e6 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -63,7 +63,7 @@ export default { if (this.group.isOrphan) { // check if current group is baseGroup - if (this.baseGroup) { + if (Object.keys(this.baseGroup).length > 0) { // Remove baseGroup prefix from our current group.fullName. e.g: // baseGroup.fullName: `level1` // group.fullName: `level1 / level2 / level3` -- cgit v1.2.1 From bf65c49c699f6f009c0a919a189499e2aca82118 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 20:39:44 -0500 Subject: Remove unnecesary return --- app/assets/javascripts/groups/stores/groups_store.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index fc925172c2f..eaad30638ac 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -4,8 +4,6 @@ export default class GroupsStore { this.state = {}; this.state.groups = {}; this.state.pageInfo = {}; - - return this; } setGroups(rawGroups, parent = null) { -- cgit v1.2.1 From 0a1b196643110380ec1fa672bfd829fb79a691a6 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 20:52:43 -0500 Subject: Remove default null value for parent param --- app/assets/javascripts/groups/stores/groups_store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index eaad30638ac..d343b61f378 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -6,7 +6,7 @@ export default class GroupsStore { this.state.pageInfo = {}; } - setGroups(rawGroups, parent = null) { + setGroups(rawGroups, parent) { const parentGroup = parent; const tree = this.buildTree(rawGroups, parentGroup); -- cgit v1.2.1 From af6e4ae900896931503073e2f74dd497ccf6a60d Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 21:01:23 -0500 Subject: Use map to iterate arrays --- app/assets/javascripts/groups/stores/groups_store.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index d343b61f378..06ef3b268e3 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -44,11 +44,11 @@ export default class GroupsStore { const orphans = []; // Map groups to an object - for (let i = 0, len = groups.length; i < len; i += 1) { - const group = groups[i]; + groups.map((group) => { mappedGroups[group.id] = group; mappedGroups[group.id].subGroups = {}; - } + return group; + }); Object.keys(mappedGroups).map((key) => { const currentGroup = mappedGroups[key]; -- cgit v1.2.1 From 7feda1a354274c4672a95f965b59de3c40118ccb Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 21:09:29 -0500 Subject: Use webpack_bundle_tag instead of deprecated page_specific_javascript_bundle_tag --- app/views/dashboard/groups/index.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index af9f9b1b363..f9b45a539a1 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -2,8 +2,8 @@ - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' -= page_specific_javascript_bundle_tag('common_vue') -= page_specific_javascript_bundle_tag('groups') += webpack_bundle_tag 'common_vue' += webpack_bundle_tag 'groups' - if @groups.empty? = render 'empty_state' -- cgit v1.2.1 From 24b456e6a3e978965715d53243407d059e485bdb Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 21:18:11 -0500 Subject: Move line to a better place --- app/assets/javascripts/groups/stores/groups_store.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index 06ef3b268e3..b6bc2a25776 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -52,10 +52,9 @@ export default class GroupsStore { Object.keys(mappedGroups).map((key) => { const currentGroup = mappedGroups[key]; - // If the group is not at the root level, add it to its parent array of subGroups. - const findParentGroup = mappedGroups[currentGroup.parentId]; - if (currentGroup.parentId) { + // If the group is not at the root level, add it to its parent array of subGroups. + const findParentGroup = mappedGroups[currentGroup.parentId]; if (findParentGroup) { mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup; mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups -- cgit v1.2.1 From 76543bce90b51f99f7185d7e9f1cc82f975c6d60 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 31 May 2017 21:37:56 -0500 Subject: Fix karma tests --- spec/javascripts/groups/mock_data.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/javascripts/groups/mock_data.js b/spec/javascripts/groups/mock_data.js index fdb809018cf..98f2aa52135 100644 --- a/spec/javascripts/groups/mock_data.js +++ b/spec/javascripts/groups/mock_data.js @@ -11,8 +11,8 @@ const group1 = { parent_id: null, created_at: '2017-05-15T19:01:23.670Z', updated_at: '2017-05-15T19:01:23.670Z', - number_projects: '1', - number_users: '1', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', has_subgroups: true, permissions: { group_access: 50, @@ -33,8 +33,8 @@ const group14 = { parent_id: 1127, created_at: '2017-05-15T19:02:01.645Z', updated_at: '2017-05-15T19:02:01.645Z', - number_projects: '1', - number_users: '1', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', has_subgroups: true, permissions: { group_access: 30, @@ -54,8 +54,8 @@ const group2 = { parent_id: null, created_at: '2017-05-11T19:35:09.635Z', updated_at: '2017-05-11T19:35:09.635Z', - number_projects: '1', - number_users: '1', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', has_subgroups: true, permissions: { group_access: 50, @@ -75,8 +75,8 @@ const group21 = { parent_id: 1119, created_at: '2017-05-11T19:51:04.060Z', updated_at: '2017-05-11T19:51:04.060Z', - number_projects: '1', - number_users: '1', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', has_subgroups: true, permissions: { group_access: 50, -- cgit v1.2.1 From 854e9de935c2bbb2a7143c73bb5e9edf98d3fc65 Mon Sep 17 00:00:00 2001 From: Simon Knox Date: Tue, 30 May 2017 18:32:49 +1000 Subject: animate adding issue to boards fix some false positive tests for board_new_issue --- .../javascripts/boards/components/board_list.js | 13 +++++-- .../boards/components/board_new_issue.js | 2 + app/assets/javascripts/boards/models/list.js | 8 +++- .../javascripts/boards/stores/boards_store.js | 1 + app/assets/stylesheets/pages/boards.scss | 41 ++++++++++++++++++-- changelogs/unreleased/31633-animate-issue.yml | 4 ++ spec/javascripts/boards/board_new_issue_spec.js | 45 +++++++++++++--------- 7 files changed, 88 insertions(+), 26 deletions(-) create mode 100644 changelogs/unreleased/31633-animate-issue.yml diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js index 7ee2696e720..bebca17fb1e 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.js @@ -57,6 +57,9 @@ export default { scrollTop() { return this.$refs.list.scrollTop + this.listHeight(); }, + scrollToTop() { + this.$refs.list.scrollTop = 0; + }, loadNextPage() { const getIssues = this.list.nextPage(); const loadingDone = () => { @@ -108,6 +111,7 @@ export default { }, created() { eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm); + eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop); }, mounted() { const options = gl.issueBoards.getBoardSortableDefaultOptions({ @@ -150,6 +154,7 @@ export default { }, beforeDestroy() { eventHub.$off(`hide-issue-form-${this.list.id}`, this.toggleForm); + eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop); this.$refs.list.removeEventListener('scroll', this.onScroll); }, template: ` @@ -160,9 +165,11 @@ export default { v-if="loading"> - + + +
    @@ -53,7 +62,8 @@ + :href="project.full_path" + > {{ project.full_name }} @@ -61,20 +71,30 @@ created {{ timeagoDate }} + + Edit + + type="enable" + /> + type="remove" + /> + type="disable" + /> diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue index eccc470578b..9e6fb244af6 100644 --- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue +++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue @@ -20,6 +20,10 @@ type: Object, required: true, }, + endpoint: { + type: String, + required: true, + }, }, components: { key, @@ -34,18 +38,22 @@ ({{ keys.length }})
      + v-if="keys.length" + >
    • + :store="store" + :endpoint="endpoint" + />
    + v-else-if="showHelpBox" + > No deploy keys found. Create one with the form above.
    diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb index 4f6a7e9e2cb..aa5e3f508fe 100644 --- a/app/controllers/admin/deploy_keys_controller.rb +++ b/app/controllers/admin/deploy_keys_controller.rb @@ -1,6 +1,6 @@ class Admin::DeployKeysController < Admin::ApplicationController before_action :deploy_keys, only: [:index] - before_action :deploy_key, only: [:destroy] + before_action :deploy_key, only: [:destroy, :edit, :update] def index end @@ -10,12 +10,24 @@ class Admin::DeployKeysController < Admin::ApplicationController end def create - @deploy_key = deploy_keys.new(deploy_key_params.merge(user: current_user)) + @deploy_key = deploy_keys.new(create_params.merge(user: current_user)) if @deploy_key.save redirect_to admin_deploy_keys_path else - render "new" + render 'new' + end + end + + def edit + end + + def update + if deploy_key.update_attributes(update_params) + flash[:notice] = 'Deploy key was successfully updated.' + redirect_to admin_deploy_keys_path + else + render 'edit' end end @@ -38,7 +50,11 @@ class Admin::DeployKeysController < Admin::ApplicationController @deploy_keys ||= DeployKey.are_public end - def deploy_key_params + def create_params params.require(:deploy_key).permit(:key, :title, :can_push) end + + def update_params + params.require(:deploy_key).permit(:title, :can_push) + end end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index f27089b8590..7f1469e107d 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -4,6 +4,7 @@ class Projects::DeployKeysController < Projects::ApplicationController # Authorize before_action :authorize_admin_project! + before_action :authorize_update_deploy_key!, only: [:edit, :update] layout "project_settings" @@ -21,7 +22,7 @@ class Projects::DeployKeysController < Projects::ApplicationController end def create - @key = DeployKey.new(deploy_key_params.merge(user: current_user)) + @key = DeployKey.new(create_params.merge(user: current_user)) unless @key.valid? && @project.deploy_keys << @key flash[:alert] = @key.errors.full_messages.join(', ').html_safe @@ -29,6 +30,18 @@ class Projects::DeployKeysController < Projects::ApplicationController redirect_to_repository_settings(@project) end + def edit + end + + def update + if deploy_key.update_attributes(update_params) + flash[:notice] = 'Deploy key was successfully updated.' + redirect_to_repository_settings(@project) + else + render 'edit' + end + end + def enable Projects::EnableDeployKeyService.new(@project, current_user, params).execute @@ -52,7 +65,19 @@ class Projects::DeployKeysController < Projects::ApplicationController protected - def deploy_key_params + def deploy_key + @deploy_key ||= @project.deploy_keys.find(params[:id]) + end + + def create_params params.require(:deploy_key).permit(:key, :title, :can_push) end + + def update_params + params.require(:deploy_key).permit(:title, :can_push) + end + + def authorize_update_deploy_key! + access_denied! unless can?(current_user, :update_deploy_key, deploy_key) + end end diff --git a/app/policies/deploy_key_policy.rb b/app/policies/deploy_key_policy.rb new file mode 100644 index 00000000000..ebab213e6be --- /dev/null +++ b/app/policies/deploy_key_policy.rb @@ -0,0 +1,11 @@ +class DeployKeyPolicy < BasePolicy + def rules + return unless @user + + can! :update_deploy_key if @user.admin? + + if @subject.private? && @user.project_deploy_keys.exists?(id: @subject.id) + can! :update_deploy_key + end + end +end diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb index 070b0c35e36..229311eb6ee 100644 --- a/app/presenters/projects/settings/deploy_keys_presenter.rb +++ b/app/presenters/projects/settings/deploy_keys_presenter.rb @@ -11,7 +11,7 @@ module Projects end def enabled_keys - @enabled_keys ||= project.deploy_keys + @enabled_keys ||= project.deploy_keys.includes(:projects) end def any_keys_enabled? @@ -23,11 +23,7 @@ module Projects end def available_project_keys - @available_project_keys ||= current_user.project_deploy_keys - enabled_keys - end - - def any_available_project_keys_enabled? - available_project_keys.any? + @available_project_keys ||= current_user.project_deploy_keys.includes(:projects) - enabled_keys end def key_available?(deploy_key) @@ -37,17 +33,13 @@ module Projects def available_public_keys return @available_public_keys if defined?(@available_public_keys) - @available_public_keys ||= DeployKey.are_public - enabled_keys + @available_public_keys ||= DeployKey.are_public.includes(:projects) - enabled_keys # Public keys that are already used by another accessible project are already # in @available_project_keys. @available_public_keys -= available_project_keys end - def any_available_public_keys_enabled? - available_public_keys.any? - end - def as_json serializer = DeployKeySerializer.new opts = { user: current_user } diff --git a/app/serializers/deploy_key_entity.rb b/app/serializers/deploy_key_entity.rb index d75a83d0fa5..068013c8829 100644 --- a/app/serializers/deploy_key_entity.rb +++ b/app/serializers/deploy_key_entity.rb @@ -11,4 +11,11 @@ class DeployKeyEntity < Grape::Entity expose :projects, using: ProjectEntity do |deploy_key| deploy_key.projects.select { |project| options[:user].can?(:read_project, project) } end + expose :can_edit + + private + + def can_edit + options[:user].can?(:update_deploy_key, object) + end end diff --git a/app/views/admin/deploy_keys/edit.html.haml b/app/views/admin/deploy_keys/edit.html.haml new file mode 100644 index 00000000000..3a59282e578 --- /dev/null +++ b/app/views/admin/deploy_keys/edit.html.haml @@ -0,0 +1,10 @@ +- page_title 'Edit Deploy Key' +%h3.page-title Edit public deploy key +%hr + +%div + = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f| + = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } + .form-actions + = f.submit 'Save changes', class: 'btn-save btn' + = link_to 'Cancel', admin_deploy_keys_path, class: 'btn btn-cancel' diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 007da8c1d29..92370034baa 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -31,4 +31,6 @@ %span.cgray added #{time_ago_with_tooltip(deploy_key.created_at)} %td - = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove delete-key pull-right' + .pull-right + = link_to 'Edit', edit_admin_deploy_key_path(deploy_key), class: 'btn btn-sm' + = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove delete-key' diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml index a064efc231f..13f5259698f 100644 --- a/app/views/admin/deploy_keys/new.html.haml +++ b/app/views/admin/deploy_keys/new.html.haml @@ -1,31 +1,10 @@ -- page_title "New Deploy Key" +- page_title 'New Deploy Key' %h3.page-title New public deploy key %hr %div = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f| - = form_errors(@deploy_key) - - .form-group - = f.label :title, class: "control-label" - .col-sm-10= f.text_field :title, class: 'form-control' - .form-group - = f.label :key, class: "control-label" - .col-sm-10 - %p.light - Paste a machine public key here. Read more about how to generate it - = link_to "here", help_page_path("ssh/README") - = f.text_area :key, class: "form-control thin_area", rows: 5 - .form-group - .control-label - .col-sm-10 - = f.label :can_push do - = f.check_box :can_push - %strong Write access allowed - %p.light.append-bottom-0 - Allow this key to push to repository as well? (Default only allows pull access.) - + = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } .form-actions - = f.submit 'Create', class: "btn-create btn" - = link_to "Cancel", admin_deploy_keys_path, class: "btn btn-cancel" - + = f.submit 'Create', class: 'btn-create btn' + = link_to 'Cancel', admin_deploy_keys_path, class: 'btn btn-cancel' diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml deleted file mode 100644 index ec8fc4c9ee8..00000000000 --- a/app/views/projects/deploy_keys/_deploy_key.html.haml +++ /dev/null @@ -1,30 +0,0 @@ -%li - .pull-left.append-right-10.hidden-xs - = icon "key", class: "key-icon" - .deploy-key-content.key-list-item-info - %strong.title - = deploy_key.title - .description - = deploy_key.fingerprint - - if deploy_key.can_push? - .write-access-allowed - Write access allowed - .deploy-key-content.prepend-left-default.deploy-key-projects - - deploy_key.projects.each do |project| - - if can?(current_user, :read_project, project) - = link_to namespace_project_path(project.namespace, project), class: "label deploy-project-label" do - = project.name_with_namespace - .deploy-key-content - %span.key-created-at - created #{time_ago_with_tooltip(deploy_key.created_at)} - .visible-xs-block.visible-sm-block - - if @deploy_keys.key_available?(deploy_key) - = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: "btn btn-sm prepend-left-10", method: :put do - Enable - - else - - if deploy_key.destroyed_when_orphaned? && deploy_key.almost_orphaned? - = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: "You are going to remove deploy key. Are you sure?" }, method: :put, class: "btn btn-warning btn-sm prepend-left-10" do - Remove - - else - = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: "btn btn-warning btn-sm prepend-left-10", method: :put do - Disable diff --git a/app/views/projects/deploy_keys/edit.html.haml b/app/views/projects/deploy_keys/edit.html.haml new file mode 100644 index 00000000000..37219f8d7ae --- /dev/null +++ b/app/views/projects/deploy_keys/edit.html.haml @@ -0,0 +1,10 @@ +- page_title 'Edit Deploy Key' +%h3.page-title Edit Deploy Key +%hr + +%div + = form_for [@project.namespace.becomes(Namespace), @project, @deploy_key], html: { class: 'form-horizontal js-requires-input' } do |f| + = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } + .form-actions + = f.submit 'Save changes', class: 'btn-save btn' + = link_to 'Cancel', namespace_project_settings_repository_path(@project.namespace, @project), class: 'btn btn-cancel' diff --git a/app/views/projects/deploy_keys/new.html.haml b/app/views/projects/deploy_keys/new.html.haml deleted file mode 100644 index 01fab3008a7..00000000000 --- a/app/views/projects/deploy_keys/new.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -- page_title "New Deploy Key" -%h3.page-title New Deploy Key -%hr - -= render 'form' diff --git a/app/views/shared/deploy_keys/_form.html.haml b/app/views/shared/deploy_keys/_form.html.haml new file mode 100644 index 00000000000..e6075c3ae3a --- /dev/null +++ b/app/views/shared/deploy_keys/_form.html.haml @@ -0,0 +1,30 @@ +- form = local_assigns.fetch(:form) +- deploy_key = local_assigns.fetch(:deploy_key) + += form_errors(deploy_key) + +.form-group + = form.label :title, class: 'control-label' + .col-sm-10= form.text_field :title, class: 'form-control' + +.form-group + - if deploy_key.new_record? + = form.label :key, class: 'control-label' + .col-sm-10 + %p.light + Paste a machine public key here. Read more about how to generate it + = link_to 'here', help_page_path('ssh/README') + = form.text_area :key, class: 'form-control thin_area', rows: 5 + - else + = form.label :fingerprint, class: 'control-label' + .col-sm-10 + = form.text_field :fingerprint, class: 'form-control', readonly: 'readonly' + +.form-group + .control-label + .col-sm-10 + = form.label :can_push do + = form.check_box :can_push + %strong Write access allowed + %p.light.append-bottom-0 + Allow this key to push to repository as well? (Default only allows pull access.) diff --git a/changelogs/unreleased/3191-deploy-keys-update.yml b/changelogs/unreleased/3191-deploy-keys-update.yml new file mode 100644 index 00000000000..4100163e94f --- /dev/null +++ b/changelogs/unreleased/3191-deploy-keys-update.yml @@ -0,0 +1,4 @@ +--- +title: Implement ability to update deploy keys +merge_request: 10383 +author: Alexander Randa diff --git a/config/routes/admin.rb b/config/routes/admin.rb index ccfd85aed63..f739dccfbfd 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -48,7 +48,7 @@ namespace :admin do end end - resources :deploy_keys, only: [:index, :new, :create, :destroy] + resources :deploy_keys, only: [:index, :new, :create, :edit, :update, :destroy] resources :hooks, only: [:index, :create, :edit, :update, :destroy] do member do diff --git a/config/routes/project.rb b/config/routes/project.rb index 5aac44fce10..343de4106f3 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -73,7 +73,7 @@ constraints(ProjectUrlConstrainer.new) do resource :mattermost, only: [:new, :create] - resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do + resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create, :edit, :update] do member do put :enable put :disable diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 8a54f7f3f05..7cdee8aced7 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -76,6 +76,27 @@ module API end end + desc 'Update an existing deploy key for a project' do + success Entities::SSHKey + end + params do + requires :key_id, type: Integer, desc: 'The ID of the deploy key' + optional :title, type: String, desc: 'The name of the deploy key' + optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository" + at_least_one_of :title, :can_push + end + put ":id/deploy_keys/:key_id" do + key = user_project.deploy_keys.find(params.delete(:key_id)) + + authorize!(:update_deploy_key, key) + + if key.update_attributes(declared_params(include_missing: false)) + present key, with: Entities::SSHKey + else + render_validation_error!(key) + end + end + desc 'Enable a deploy key for a project' do detail 'This feature was added in GitLab 8.11' success Entities::SSHKey diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb index c0b6995a84a..5f5fa4e932a 100644 --- a/spec/features/admin/admin_deploy_keys_spec.rb +++ b/spec/features/admin/admin_deploy_keys_spec.rb @@ -11,40 +11,67 @@ RSpec.describe 'admin deploy keys', type: :feature do it 'show all public deploy keys' do visit admin_deploy_keys_path - expect(page).to have_content(deploy_key.title) - expect(page).to have_content(another_deploy_key.title) + page.within(find('.deploy-keys-list', match: :first)) do + expect(page).to have_content(deploy_key.title) + expect(page).to have_content(another_deploy_key.title) + end end - describe 'create new deploy key' do + describe 'create a new deploy key' do + let(:new_ssh_key) { attributes_for(:key)[:key] } + before do visit admin_deploy_keys_path click_link 'New deploy key' end - it 'creates new deploy key' do - fill_deploy_key + it 'creates a new deploy key' do + fill_in 'deploy_key_title', with: 'laptop' + fill_in 'deploy_key_key', with: new_ssh_key + check 'deploy_key_can_push' click_button 'Create' - expect_renders_new_key - end + expect(current_path).to eq admin_deploy_keys_path - it 'creates new deploy key with write access' do - fill_deploy_key - check "deploy_key_can_push" - click_button "Create" + page.within(find('.deploy-keys-list', match: :first)) do + expect(page).to have_content('laptop') + expect(page).to have_content('Yes') + end + end + end - expect_renders_new_key - expect(page).to have_content('Yes') + describe 'update an existing deploy key' do + before do + visit admin_deploy_keys_path + find('tr', text: deploy_key.title).click_link('Edit') end - def expect_renders_new_key + it 'updates an existing deploy key' do + fill_in 'deploy_key_title', with: 'new-title' + check 'deploy_key_can_push' + click_button 'Save changes' + expect(current_path).to eq admin_deploy_keys_path - expect(page).to have_content('laptop') + + page.within(find('.deploy-keys-list', match: :first)) do + expect(page).to have_content('new-title') + expect(page).to have_content('Yes') + end end + end - def fill_deploy_key - fill_in 'deploy_key_title', with: 'laptop' - fill_in 'deploy_key_key', with: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop' + describe 'remove an existing deploy key' do + before do + visit admin_deploy_keys_path + end + + it 'removes an existing deploy key' do + find('tr', text: deploy_key.title).click_link('Remove') + + expect(current_path).to eq admin_deploy_keys_path + page.within(find('.deploy-keys-list', match: :first)) do + expect(page).not_to have_content(deploy_key.title) + end end end end diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb new file mode 100644 index 00000000000..4cc38c5286e --- /dev/null +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +feature 'Repository settings', feature: true do + let(:project) { create(:project_empty_repo) } + let(:user) { create(:user) } + let(:role) { :developer } + + background do + project.team << [user, role] + login_as(user) + end + + context 'for developer' do + given(:role) { :developer } + + scenario 'is not allowed to view' do + visit namespace_project_settings_repository_path(project.namespace, project) + + expect(page.status_code).to eq(404) + end + end + + context 'for master' do + given(:role) { :master } + + context 'Deploy Keys', js: true do + let(:private_deploy_key) { create(:deploy_key, title: 'private_deploy_key', public: false) } + let(:public_deploy_key) { create(:another_deploy_key, title: 'public_deploy_key', public: true) } + let(:new_ssh_key) { attributes_for(:key)[:key] } + + scenario 'get list of keys' do + project.deploy_keys << private_deploy_key + project.deploy_keys << public_deploy_key + + visit namespace_project_settings_repository_path(project.namespace, project) + + expect(page.status_code).to eq(200) + expect(page).to have_content('private_deploy_key') + expect(page).to have_content('public_deploy_key') + end + + scenario 'add a new deploy key' do + visit namespace_project_settings_repository_path(project.namespace, project) + + fill_in 'deploy_key_title', with: 'new_deploy_key' + fill_in 'deploy_key_key', with: new_ssh_key + check 'deploy_key_can_push' + click_button 'Add key' + + expect(page).to have_content('new_deploy_key') + expect(page).to have_content('Write access allowed') + end + + scenario 'edit an existing deploy key' do + project.deploy_keys << private_deploy_key + visit namespace_project_settings_repository_path(project.namespace, project) + + find('li', text: private_deploy_key.title).click_link('Edit') + + fill_in 'deploy_key_title', with: 'updated_deploy_key' + check 'deploy_key_can_push' + click_button 'Save changes' + + expect(page).to have_content('updated_deploy_key') + expect(page).to have_content('Write access allowed') + end + + scenario 'remove an existing deploy key' do + project.deploy_keys << private_deploy_key + visit namespace_project_settings_repository_path(project.namespace, project) + + find('li', text: private_deploy_key.title).click_button('Remove') + + expect(page).not_to have_content(private_deploy_key.title) + end + end + end +end diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js index 793ab8c451d..a4b98f6140d 100644 --- a/spec/javascripts/deploy_keys/components/key_spec.js +++ b/spec/javascripts/deploy_keys/components/key_spec.js @@ -39,9 +39,15 @@ describe('Deploy keys key', () => { ).toBe(`created ${gl.utils.getTimeago().format(deployKey.created_at)}`); }); + it('shows edit button', () => { + expect( + vm.$el.querySelectorAll('.btn')[0].textContent.trim(), + ).toBe('Edit'); + }); + it('shows remove button', () => { expect( - vm.$el.querySelector('.btn').textContent.trim(), + vm.$el.querySelectorAll('.btn')[1].textContent.trim(), ).toBe('Remove'); }); @@ -71,9 +77,15 @@ describe('Deploy keys key', () => { setTimeout(done); }); + it('shows edit button', () => { + expect( + vm.$el.querySelectorAll('.btn')[0].textContent.trim(), + ).toBe('Edit'); + }); + it('shows enable button', () => { expect( - vm.$el.querySelector('.btn').textContent.trim(), + vm.$el.querySelectorAll('.btn')[1].textContent.trim(), ).toBe('Enable'); }); @@ -82,7 +94,7 @@ describe('Deploy keys key', () => { Vue.nextTick(() => { expect( - vm.$el.querySelector('.btn').textContent.trim(), + vm.$el.querySelectorAll('.btn')[1].textContent.trim(), ).toBe('Disable'); done(); diff --git a/spec/policies/deploy_key_policy_spec.rb b/spec/policies/deploy_key_policy_spec.rb new file mode 100644 index 00000000000..28e10f0bfe2 --- /dev/null +++ b/spec/policies/deploy_key_policy_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe DeployKeyPolicy, models: true do + subject { described_class.abilities(current_user, deploy_key).to_set } + + describe 'updating a deploy_key' do + context 'when a regular user' do + let(:current_user) { create(:user) } + + context 'tries to update private deploy key attached to project' do + let(:deploy_key) { create(:deploy_key, public: false) } + let(:project) { create(:project_empty_repo) } + + before do + project.add_master(current_user) + project.deploy_keys << deploy_key + end + + it { is_expected.to include(:update_deploy_key) } + end + + context 'tries to update private deploy key attached to other project' do + let(:deploy_key) { create(:deploy_key, public: false) } + let(:other_project) { create(:project_empty_repo) } + + before do + other_project.deploy_keys << deploy_key + end + + it { is_expected.not_to include(:update_deploy_key) } + end + + context 'tries to update public deploy key' do + let(:deploy_key) { create(:another_deploy_key, public: true) } + + it { is_expected.not_to include(:update_deploy_key) } + end + end + + context 'when an admin user' do + let(:current_user) { create(:user, :admin) } + + context ' tries to update private deploy key' do + let(:deploy_key) { create(:deploy_key, public: false) } + + it { is_expected.to include(:update_deploy_key) } + end + + context 'when an admin user tries to update public deploy key' do + let(:deploy_key) { create(:another_deploy_key, public: true) } + + it { is_expected.to include(:update_deploy_key) } + end + end + end +end diff --git a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb index 6443f86b6a1..5c39e1b5f96 100644 --- a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb +++ b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb @@ -51,10 +51,6 @@ describe Projects::Settings::DeployKeysPresenter do expect(presenter.available_project_keys).not_to be_empty end - it 'returns false if any available_project_keys are enabled' do - expect(presenter.any_available_project_keys_enabled?).to eq(true) - end - it 'returns the available_project_keys size' do expect(presenter.available_project_keys_size).to eq(1) end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 843e9862b0c..4d9cd5f3a27 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -13,7 +13,7 @@ describe API::DeployKeys do describe 'GET /deploy_keys' do context 'when unauthenticated' do - it 'should return authentication error' do + it 'returns authentication error' do get api('/deploy_keys') expect(response.status).to eq(401) @@ -21,7 +21,7 @@ describe API::DeployKeys do end context 'when authenticated as non-admin user' do - it 'should return a 403 error' do + it 'returns a 403 error' do get api('/deploy_keys', user) expect(response.status).to eq(403) @@ -29,7 +29,7 @@ describe API::DeployKeys do end context 'when authenticated as admin' do - it 'should return all deploy keys' do + it 'returns all deploy keys' do get api('/deploy_keys', admin) expect(response.status).to eq(200) @@ -43,7 +43,7 @@ describe API::DeployKeys do describe 'GET /projects/:id/deploy_keys' do before { deploy_key } - it 'should return array of ssh keys' do + it 'returns array of ssh keys' do get api("/projects/#{project.id}/deploy_keys", admin) expect(response).to have_http_status(200) @@ -54,14 +54,14 @@ describe API::DeployKeys do end describe 'GET /projects/:id/deploy_keys/:key_id' do - it 'should return a single key' do + it 'returns a single key' do get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) expect(response).to have_http_status(200) expect(json_response['title']).to eq(deploy_key.title) end - it 'should return 404 Not Found with invalid ID' do + it 'returns 404 Not Found with invalid ID' do get api("/projects/#{project.id}/deploy_keys/404", admin) expect(response).to have_http_status(404) @@ -69,26 +69,26 @@ describe API::DeployKeys do end describe 'POST /projects/:id/deploy_keys' do - it 'should not create an invalid ssh key' do + it 'does not create an invalid ssh key' do post api("/projects/#{project.id}/deploy_keys", admin), { title: 'invalid key' } expect(response).to have_http_status(400) expect(json_response['error']).to eq('key is missing') end - it 'should not create a key without title' do + it 'does not create a key without title' do post api("/projects/#{project.id}/deploy_keys", admin), key: 'some key' expect(response).to have_http_status(400) expect(json_response['error']).to eq('title is missing') end - it 'should create new ssh key' do + it 'creates new ssh key' do key_attrs = attributes_for :another_key expect do post api("/projects/#{project.id}/deploy_keys", admin), key_attrs - end.to change{ project.deploy_keys.count }.by(1) + end.to change { project.deploy_keys.count }.by(1) end it 'returns an existing ssh key when attempting to add a duplicate' do @@ -117,10 +117,53 @@ describe API::DeployKeys do end end + describe 'PUT /projects/:id/deploy_keys/:key_id' do + let(:private_deploy_key) { create(:another_deploy_key, public: false) } + let(:project_private_deploy_key) do + create(:deploy_keys_project, project: project, deploy_key: private_deploy_key) + end + + it 'updates a public deploy key as admin' do + expect do + put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin), { title: 'new title' } + end.not_to change(deploy_key, :title) + + expect(response).to have_http_status(200) + end + + it 'does not update a public deploy key as non admin' do + expect do + put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", user), { title: 'new title' } + end.not_to change(deploy_key, :title) + + expect(response).to have_http_status(404) + end + + it 'does not update a private key with invalid title' do + project_private_deploy_key + + expect do + put api("/projects/#{project.id}/deploy_keys/#{private_deploy_key.id}", admin), { title: '' } + end.not_to change(deploy_key, :title) + + expect(response).to have_http_status(400) + end + + it 'updates a private ssh key with correct attributes' do + project_private_deploy_key + + put api("/projects/#{project.id}/deploy_keys/#{private_deploy_key.id}", admin), { title: 'new title', can_push: true } + + expect(json_response['id']).to eq(private_deploy_key.id) + expect(json_response['title']).to eq('new title') + expect(json_response['can_push']).to eq(true) + end + end + describe 'DELETE /projects/:id/deploy_keys/:key_id' do before { deploy_key } - it 'should delete existing key' do + it 'deletes existing key' do expect do delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) @@ -128,7 +171,7 @@ describe API::DeployKeys do end.to change{ project.deploy_keys.count }.by(-1) end - it 'should return 404 Not Found with invalid ID' do + it 'returns 404 Not Found with invalid ID' do delete api("/projects/#{project.id}/deploy_keys/404", admin) expect(response).to have_http_status(404) @@ -150,7 +193,7 @@ describe API::DeployKeys do end context 'when authenticated as non-admin user' do - it 'should return a 404 error' do + it 'returns a 404 error' do post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", user) expect(response).to have_http_status(404) diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 54417f6b3e1..0a6778ae2ef 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -201,10 +201,12 @@ describe 'project routing' do # POST /:project_id/deploy_keys(.:format) deploy_keys#create # new_project_deploy_key GET /:project_id/deploy_keys/new(.:format) deploy_keys#new # project_deploy_key GET /:project_id/deploy_keys/:id(.:format) deploy_keys#show + # edit_project_deploy_key GET /:project_id/deploy_keys/:id/edit(.:format) deploy_keys#edit + # project_deploy_key PATCH /:project_id/deploy_keys/:id(.:format) deploy_keys#update # DELETE /:project_id/deploy_keys/:id(.:format) deploy_keys#destroy describe Projects::DeployKeysController, 'routing' do it_behaves_like 'RESTful project resources' do - let(:actions) { [:index, :new, :create] } + let(:actions) { [:index, :new, :create, :edit, :update] } let(:controller) { 'deploy_keys' } end end diff --git a/spec/serializers/deploy_key_entity_spec.rb b/spec/serializers/deploy_key_entity_spec.rb index e73fbe190ca..ed89fccc3d0 100644 --- a/spec/serializers/deploy_key_entity_spec.rb +++ b/spec/serializers/deploy_key_entity_spec.rb @@ -12,27 +12,44 @@ describe DeployKeyEntity do let(:entity) { described_class.new(deploy_key, user: user) } - it 'returns deploy keys with projects a user can read' do - expected_result = { - id: deploy_key.id, - user_id: deploy_key.user_id, - title: deploy_key.title, - fingerprint: deploy_key.fingerprint, - can_push: deploy_key.can_push, - destroyed_when_orphaned: true, - almost_orphaned: false, - created_at: deploy_key.created_at, - updated_at: deploy_key.updated_at, - projects: [ - { - id: project.id, - name: project.name, - full_path: namespace_project_path(project.namespace, project), - full_name: project.full_name - } - ] - } - - expect(entity.as_json).to eq(expected_result) + describe 'returns deploy keys with projects a user can read' do + let(:expected_result) do + { + id: deploy_key.id, + user_id: deploy_key.user_id, + title: deploy_key.title, + fingerprint: deploy_key.fingerprint, + can_push: deploy_key.can_push, + destroyed_when_orphaned: true, + almost_orphaned: false, + created_at: deploy_key.created_at, + updated_at: deploy_key.updated_at, + can_edit: false, + projects: [ + { + id: project.id, + name: project.name, + full_path: namespace_project_path(project.namespace, project), + full_name: project.full_name + } + ] + } + end + + it { expect(entity.as_json).to eq(expected_result) } + end + + describe 'returns can_edit true if user is a master of project' do + before do + project.add_master(user) + end + + it { expect(entity.as_json).to include(can_edit: true) } + end + + describe 'returns can_edit true if a user admin' do + let(:user) { create(:user, :admin) } + + it { expect(entity.as_json).to include(can_edit: true) } end end -- cgit v1.2.1 From f1cb09913754a9f5d449f01c91de397d7153d3c1 Mon Sep 17 00:00:00 2001 From: Kevin Lyda Date: Sat, 25 Feb 2017 19:18:46 +0000 Subject: Add the prometheus gem. --- Gemfile.lock | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index f8adfec6143..3affa434e2a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,11 @@ +GIT + remote: https://github.com/prometheus/client_ruby.git + revision: 30dc5987c6bf574044d75d97362bd3bd27d542f2 + branch: exchangeable-value-types + specs: + prometheus-client (0.7.0.beta1) + quantile (~> 0.2.0) + GEM remote: https://rubygems.org/ specs: @@ -570,6 +578,7 @@ GEM pry-rails (0.3.5) pry (>= 0.9.10) pyu-ruby-sasl (0.0.3.3) + quantile (0.2.0) rack (1.6.5) rack-accept (0.4.5) rack (>= 0.4) @@ -995,6 +1004,7 @@ DEPENDENCIES pg (~> 0.18.2) poltergeist (~> 1.9.0) premailer-rails (~> 1.9.0) + prometheus-client! pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) rack-attack (~> 4.4.1) -- cgit v1.2.1 From e4fb16218693edd4382b96482ba94eba6c8a7f1a Mon Sep 17 00:00:00 2001 From: Kevin Lyda Date: Wed, 29 Mar 2017 19:20:13 +0100 Subject: Initial pass at prometheus monitoring. This is a step for #29118. Add a single metric to count successful logins. Summary types are not supported so remove Collector. Either we need to support the summary type or we need to create a multiprocess-friendly Collector. Add config to load prometheus and set up the Collector and the Exporter. Fix `Gemfile` as current prometheus-client gemspec is missing the `mmap2` dependency. --- Gemfile.lock | 2 ++ app/controllers/sessions_controller.rb | 1 + app/services/prom_service.rb | 16 ++++++++++++++++ config.ru | 7 +++++++ 4 files changed, 26 insertions(+) create mode 100644 app/services/prom_service.rb diff --git a/Gemfile.lock b/Gemfile.lock index 3affa434e2a..7c0f405091d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -465,6 +465,7 @@ GEM mimemagic (0.3.0) mini_portile2 (2.1.0) minitest (5.7.0) + mmap2 (2.2.6) mousetrap-rails (1.4.6) multi_json (1.12.1) multi_xml (0.6.0) @@ -977,6 +978,7 @@ DEPENDENCIES mail_room (~> 0.9.1) method_source (~> 0.8) minitest (~> 5.7.0) + mmap2 (~> 2.2.6) mousetrap-rails (~> 1.4.6) mysql2 (~> 0.3.16) net-ssh (~> 3.0.1) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 8c6ba4915cd..9870d4286a6 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -34,6 +34,7 @@ class SessionsController < Devise::SessionsController end # hide the signed-in notification flash[:notice] = nil + PromService.instance.login.increment log_audit_event(current_user, with: authentication_method) log_user_activity(current_user) end diff --git a/app/services/prom_service.rb b/app/services/prom_service.rb new file mode 100644 index 00000000000..e9ccce758e9 --- /dev/null +++ b/app/services/prom_service.rb @@ -0,0 +1,16 @@ +require 'prometheus/client' +require 'singleton' + +class PromService + include Singleton + + attr_reader :login + + def initialize + @prometheus = Prometheus::Client.registry + + @login = Prometheus::Client::Counter.new(:login, 'Login counter') + @prometheus.register(@login) + + end +end diff --git a/config.ru b/config.ru index 065ce59932f..82af814341b 100644 --- a/config.ru +++ b/config.ru @@ -13,6 +13,13 @@ if defined?(Unicorn) # Max memory size (RSS) per worker use Unicorn::WorkerKiller::Oom, min, max end + + # TODO(lyda): Needs to be set externally. + ENV['prometheus_multiproc_dir'] = '/tmp' + + require 'prometheus/client/rack/exporter' + + use Prometheus::Client::Rack::Exporter, path: '/admin/metrics' end require ::File.expand_path('../config/environment', __FILE__) -- cgit v1.2.1 From f74b133a7e5621ba3a3e93a4f29489fe28a10ae3 Mon Sep 17 00:00:00 2001 From: Kevin Lyda Date: Wed, 26 Apr 2017 15:06:12 +0100 Subject: Use the gem for the ruby prometheus client. --- Gemfile.lock | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 7c0f405091d..b6a7e815170 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,3 @@ -GIT - remote: https://github.com/prometheus/client_ruby.git - revision: 30dc5987c6bf574044d75d97362bd3bd27d542f2 - branch: exchangeable-value-types - specs: - prometheus-client (0.7.0.beta1) - quantile (~> 0.2.0) - GEM remote: https://rubygems.org/ specs: @@ -569,6 +561,8 @@ GEM premailer-rails (1.9.2) actionmailer (>= 3, < 6) premailer (~> 1.7, >= 1.7.9) + prometheus-client-mmap (0.7.0.beta1) + quantile (~> 0.2.0) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -1006,7 +1000,7 @@ DEPENDENCIES pg (~> 0.18.2) poltergeist (~> 1.9.0) premailer-rails (~> 1.9.0) - prometheus-client! + prometheus-client-mmap (~> 0.7.0.beta1) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) rack-attack (~> 4.4.1) -- cgit v1.2.1 From 5bc099c2de1e05fa4dbe45b59caeced209834178 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Tue, 9 May 2017 09:39:28 +0200 Subject: Prometheus metrics first pass metrics wip --- app/controllers/health_controller.rb | 4 +- app/services/prom_service.rb | 1 - config.ru | 6 +-- lib/gitlab/metrics.rb | 65 ++++++++++++++++++++++++++++++-- lib/gitlab/metrics/dummy_metric.rb | 29 ++++++++++++++ lib/gitlab/metrics/prometheus_sampler.rb | 51 +++++++++++++++++++++++++ 6 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 lib/gitlab/metrics/dummy_metric.rb create mode 100644 lib/gitlab/metrics/prometheus_sampler.rb diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb index 125746d0426..03cc896fce9 100644 --- a/app/controllers/health_controller.rb +++ b/app/controllers/health_controller.rb @@ -1,3 +1,5 @@ +require 'prometheus/client/formats/text' + class HealthController < ActionController::Base protect_from_forgery with: :exception include RequiresHealthToken @@ -24,7 +26,7 @@ class HealthController < ActionController::Base results = CHECKS.flat_map(&:metrics) response = results.map(&method(:metric_to_prom_line)).join("\n") - + response = ::Prometheus::Client::Formats::Text.marshal_multiprocess render text: response, content_type: 'text/plain; version=0.0.4' end diff --git a/app/services/prom_service.rb b/app/services/prom_service.rb index e9ccce758e9..93d52fd2c79 100644 --- a/app/services/prom_service.rb +++ b/app/services/prom_service.rb @@ -11,6 +11,5 @@ class PromService @login = Prometheus::Client::Counter.new(:login, 'Login counter') @prometheus.register(@login) - end end diff --git a/config.ru b/config.ru index 82af814341b..cba44122da9 100644 --- a/config.ru +++ b/config.ru @@ -15,11 +15,7 @@ if defined?(Unicorn) end # TODO(lyda): Needs to be set externally. - ENV['prometheus_multiproc_dir'] = '/tmp' - - require 'prometheus/client/rack/exporter' - - use Prometheus::Client::Rack::Exporter, path: '/admin/metrics' + ENV['prometheus_multiproc_dir'] = '/tmp/somestuff' end require ::File.expand_path('../config/environment', __FILE__) diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index cb8db2f1e9f..e784ca785f0 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -1,3 +1,5 @@ +require 'prometheus/client' + module Gitlab module Metrics extend Gitlab::CurrentSettings @@ -9,6 +11,7 @@ module Gitlab def self.settings @settings ||= { enabled: current_application_settings[:metrics_enabled], + prometheus_metrics_enabled: true, pool_size: current_application_settings[:metrics_pool_size], timeout: current_application_settings[:metrics_timeout], method_call_threshold: current_application_settings[:metrics_method_call_threshold], @@ -19,10 +22,18 @@ module Gitlab } end - def self.enabled? + def self.prometheus_metrics_enabled? + settings[:prometheus_metrics_enabled] || false + end + + def self.influx_metrics_enabled? settings[:enabled] || false end + def self.enabled? + influx_metrics_enabled? || prometheus_metrics_enabled? || false + end + def self.mri? RUBY_ENGINE == 'ruby' end @@ -38,10 +49,58 @@ module Gitlab @pool end + def self.registry + @registry ||= ::Prometheus::Client.registry + end + + def self.counter(name, docstring, base_labels = {}) + dummy_metric || registry.get(name) || registry.counter(name, docstring, base_labels) + end + + def self.summary(name, docstring, base_labels = {}) + dummy_metric || registry.get(name) || registry.summary(name, docstring, base_labels) + end + + def self.gauge(name, docstring, base_labels = {}) + dummy_metric || registry.get(name) || registry.gauge(name, docstring, base_labels) + end + + def self.histogram(name, docstring, base_labels = {}, buckets = Histogram::DEFAULT_BUCKETS) + dummy_metric || registry.get(name) || registry.histogram(name, docstring, base_labels, buckets) + end + + def self.dummy_metric + unless prometheus_metrics_enabled? + DummyMetric.new + end + end + def self.submit_metrics(metrics) prepared = prepare_metrics(metrics) - pool.with do |connection| + if prometheus_metrics_enabled? + metrics.map do |metric| + known = [:series, :tags,:values, :timestamp] + value = metric&.[](:values)&.[](:value) + handled= [:rails_gc_statistics] + if handled.include? metric[:series].to_sym + next + end + + if metric.keys.any? {|k| !known.include?(k)} || value.nil? + print metric + print "\n" + + {:series=>"rails_gc_statistics", :tags=>{}, :values=>{:count=>0, :heap_allocated_pages=>4245, :heap_sorted_length=>4426, :heap_allocatable_pages=>0, :heap_available_slots=>1730264, :heap_live_slots=>1729935, :heap_free_slots=>329, :heap_final_slots=>0, :heap_marked_slots=>1184216, :heap_swept_slots=>361843, :heap_eden_pages=>4245, :heap_tomb_pages=>0, :total_allocated_pages=>4245, :total_freed_pages=>0, :total_allocated_objects=>15670757, :total_freed_objects=>13940822, :malloc_increase_bytes=>4842256, :malloc_increase_bytes_limit=>29129457, :minor_gc_count=>0, :major_gc_count=>0, :remembered_wb_unprotected_objects=>39905, :remembered_wb_unprotected_objects_limit=>74474, :old_objects=>1078731, :old_objects_limit=>1975860, :oldmalloc_increase_bytes=>4842640, :oldmalloc_increase_bytes_limit=>31509677, :total_time=>0.0}, :timestamp=>1494356175592659968} + + next + end + metric_value = gauge(metric[:series].to_sym, metric[:series]) + metric_value.set(metric[:tags], value) + end + end + + pool&.with do |connection| prepared.each_slice(settings[:packet_size]) do |slice| begin connection.write_points(slice) @@ -148,7 +207,7 @@ module Gitlab # When enabled this should be set before being used as the usual pattern # "@foo ||= bar" is _not_ thread-safe. - if enabled? + if influx_metrics_enabled? @pool = ConnectionPool.new(size: settings[:pool_size], timeout: settings[:timeout]) do host = settings[:host] port = settings[:port] diff --git a/lib/gitlab/metrics/dummy_metric.rb b/lib/gitlab/metrics/dummy_metric.rb new file mode 100644 index 00000000000..d27bb83854a --- /dev/null +++ b/lib/gitlab/metrics/dummy_metric.rb @@ -0,0 +1,29 @@ +module Gitlab + module Metrics + # Mocks ::Prometheus::Client::Metric and all derived metrics + class DummyMetric + def get(*args) + raise NotImplementedError + end + + def values(*args) + raise NotImplementedError + end + + # counter + def increment(*args) + # noop + end + + # gauge + def set(*args) + # noop + end + + # histogram / summary + def observe(*args) + # noop + end + end + end +end diff --git a/lib/gitlab/metrics/prometheus_sampler.rb b/lib/gitlab/metrics/prometheus_sampler.rb new file mode 100644 index 00000000000..5f90d4f0b66 --- /dev/null +++ b/lib/gitlab/metrics/prometheus_sampler.rb @@ -0,0 +1,51 @@ +module Gitlab + module Metrics + # Class that sends certain metrics to InfluxDB at a specific interval. + # + # This class is used to gather statistics that can't be directly associated + # with a transaction such as system memory usage, garbage collection + # statistics, etc. + class PrometheusSamples + # interval - The sampling interval in seconds. + def initialize(interval = Metrics.settings[:sample_interval]) + interval_half = interval.to_f / 2 + + @interval = interval + @interval_steps = (-interval_half..interval_half).step(0.1).to_a + end + + def start + Thread.new do + Thread.current.abort_on_exception = true + + loop do + sleep(sleep_interval) + + sample + end + end + end + + def sidekiq? + Sidekiq.server? + end + + # Returns the sleep interval with a random adjustment. + # + # The random adjustment is put in place to ensure we: + # + # 1. Don't generate samples at the exact same interval every time (thus + # potentially missing anything that happens in between samples). + # 2. Don't sample data at the same interval two times in a row. + def sleep_interval + while step = @interval_steps.sample + if step != @last_step + @last_step = step + + return @interval + @last_step + end + end + end + end + end +end -- cgit v1.2.1 From 6b9a091ceeb1c760be14f749956807bc429af46d Mon Sep 17 00:00:00 2001 From: Kevin Lyda Date: Tue, 16 May 2017 15:32:07 +0100 Subject: Add trailing newline to response. Prometheus requires a trailing newline in its response. + cleanup --- app/controllers/health_controller.rb | 18 +++++++++++++++--- app/controllers/sessions_controller.rb | 6 +++++- app/services/prom_service.rb | 15 --------------- 3 files changed, 20 insertions(+), 19 deletions(-) delete mode 100644 app/services/prom_service.rb diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb index 03cc896fce9..6f8038f6ec3 100644 --- a/app/controllers/health_controller.rb +++ b/app/controllers/health_controller.rb @@ -23,15 +23,27 @@ class HealthController < ActionController::Base end def metrics - results = CHECKS.flat_map(&:metrics) + response = health_metrics_text + "\n" + + if Gitlab::Metrics.prometheus_metrics_enabled? + response += Prometheus::Client::Formats::Text.marshal_multiprocess(ENV['prometheus_multiproc_dir']) + end - response = results.map(&method(:metric_to_prom_line)).join("\n") - response = ::Prometheus::Client::Formats::Text.marshal_multiprocess render text: response, content_type: 'text/plain; version=0.0.4' end private + def health_metrics_text + results = CHECKS.flat_map(&:metrics) + + types = results.map(&:name) + .uniq + .map { |metric_name| "# TYPE #{metric_name} gauge" } + metrics = results.map(&method(:metric_to_prom_line)) + types.concat(metrics).join("\n") + end + def metric_to_prom_line(metric) labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || '' if labels.empty? diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 9870d4286a6..eaed878283e 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -34,7 +34,6 @@ class SessionsController < Devise::SessionsController end # hide the signed-in notification flash[:notice] = nil - PromService.instance.login.increment log_audit_event(current_user, with: authentication_method) log_user_activity(current_user) end @@ -48,6 +47,10 @@ class SessionsController < Devise::SessionsController private + def self.login_counter + @login_counter ||= Gitlab::Metrics.counter(:user_session_logins, 'User logins count') + end + # Handle an "initial setup" state, where there's only one user, it's an admin, # and they require a password change. def check_initial_setup @@ -126,6 +129,7 @@ class SessionsController < Devise::SessionsController end def log_user_activity(user) + SessionsController.login_counter.increment Users::ActivityService.new(user, 'login').execute end diff --git a/app/services/prom_service.rb b/app/services/prom_service.rb deleted file mode 100644 index 93d52fd2c79..00000000000 --- a/app/services/prom_service.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'prometheus/client' -require 'singleton' - -class PromService - include Singleton - - attr_reader :login - - def initialize - @prometheus = Prometheus::Client.registry - - @login = Prometheus::Client::Counter.new(:login, 'Login counter') - @prometheus.register(@login) - end -end -- cgit v1.2.1 From c28546177e2b4d5f7f3cc0e5b3a7b404206565fb Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 19 May 2017 12:49:15 +0200 Subject: Prometheus settings --- .../admin/application_settings_controller.rb | 1 + db/schema.rb | 1 + lib/api/settings.rb | 1 + lib/gitlab/metrics.rb | 20 ++++++++++---------- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 152d7baad49..75fb19e815f 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -149,6 +149,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :version_check_enabled, :terminal_max_session_time, :polling_interval_multiplier, + :prometheus_metrics_enabled, :usage_ping_enabled, disabled_oauth_sign_in_sources: [], diff --git a/db/schema.rb b/db/schema.rb index fa1c5dc15c4..96aa05f7da3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -123,6 +123,7 @@ ActiveRecord::Schema.define(version: 20170525174156) do t.integer "cached_markdown_version" t.boolean "clientside_sentry_enabled", default: false, null: false t.string "clientside_sentry_dsn" + t.boolean "prometheus_metrics_enabled", default: false, null: false end create_table "audit_events", force: :cascade do |t| diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 82f513c984e..25027c3b114 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -110,6 +110,7 @@ module API optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts" optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' + optional :prometheus_metrics_enabled, type: Boolean, desc: 'Enable Prometheus metrics' optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics' given metrics_enabled: ->(val) { val } do requires :metrics_host, type: String, desc: 'The InfluxDB host' diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index e784ca785f0..6f50c0aa028 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -10,15 +10,15 @@ module Gitlab def self.settings @settings ||= { - enabled: current_application_settings[:metrics_enabled], - prometheus_metrics_enabled: true, - pool_size: current_application_settings[:metrics_pool_size], - timeout: current_application_settings[:metrics_timeout], - method_call_threshold: current_application_settings[:metrics_method_call_threshold], - host: current_application_settings[:metrics_host], - port: current_application_settings[:metrics_port], - sample_interval: current_application_settings[:metrics_sample_interval] || 15, - packet_size: current_application_settings[:metrics_packet_size] || 1 + enabled: current_application_settings[:metrics_enabled], + prometheus_metrics_enabled: current_application_settings[:prometheus_metrics_enabled], + pool_size: current_application_settings[:metrics_pool_size], + timeout: current_application_settings[:metrics_timeout], + method_call_threshold: current_application_settings[:metrics_method_call_threshold], + host: current_application_settings[:metrics_host], + port: current_application_settings[:metrics_port], + sample_interval: current_application_settings[:metrics_sample_interval] || 15, + packet_size: current_application_settings[:metrics_packet_size] || 1 } end @@ -31,7 +31,7 @@ module Gitlab end def self.enabled? - influx_metrics_enabled? || prometheus_metrics_enabled? || false + influx_metrics_enabled? || prometheus_metrics_enabled? end def self.mri? -- cgit v1.2.1 From cf932df2348dc3ccd06ca557b68edc60f518c893 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 19 May 2017 15:10:15 +0200 Subject: Add Prometheus metrics configuration + Cleanup Gemfile --- Gemfile | 4 ++++ app/views/admin/application_settings/_form.html.haml | 19 +++++++++++++++++-- ...115_add_prometheus_settings_to_metrics_settings.rb | 13 +++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20170519102115_add_prometheus_settings_to_metrics_settings.rb diff --git a/Gemfile b/Gemfile index c49b60ffc23..a6b0a20ef61 100644 --- a/Gemfile +++ b/Gemfile @@ -268,6 +268,10 @@ group :metrics do gem 'allocations', '~> 1.0', require: false, platform: :mri gem 'method_source', '~> 0.8', require: false gem 'influxdb', '~> 0.2', require: false + +# Prometheus + gem 'mmap2', '~> 2.2.6' + gem 'prometheus-client-mmap' end group :development do diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index e1b4e34cd2b..3c020c71832 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -232,9 +232,9 @@ = f.number_field :container_registry_token_expire_delay, class: 'form-control' %fieldset - %legend Metrics + %legend Metrics - Influx %p - Setup InfluxDB to measure a wide variety of statistics like the time spent + Setup Influx to measure a wide variety of statistics like the time spent in running SQL queries. These settings require a = link_to 'restart', help_page_path('administration/restart_gitlab') to take effect. @@ -296,6 +296,21 @@ The amount of points to store in a single UDP packet. More points results in fewer but larger UDP packets being sent. + %fieldset + %legend Metrics - Prometheus + %p + Setup Prometheus to measure a variety of statistics that partially overlap and complement Influx based metrics. + This setting requires a + = link_to 'restart', help_page_path('administration/restart_gitlab') + to take effect. + = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction') + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :prometheus_metrics_enabled do + = f.check_box :prometheus_metrics_enabled + Enable Prometheus Metrics + %fieldset %legend Background Jobs %p diff --git a/db/migrate/20170519102115_add_prometheus_settings_to_metrics_settings.rb b/db/migrate/20170519102115_add_prometheus_settings_to_metrics_settings.rb new file mode 100644 index 00000000000..e675fb7cd58 --- /dev/null +++ b/db/migrate/20170519102115_add_prometheus_settings_to_metrics_settings.rb @@ -0,0 +1,13 @@ +class AddPrometheusSettingsToMetricsSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + def up + add_column_with_default(:application_settings, :prometheus_metrics_enabled, :boolean, + default: false, allow_null: false) + end + + def down + remove_column(:application_settings, :prometheus_metrics_enabled) + end +end -- cgit v1.2.1 From 0f4050430d400daffbc5a68b15d79b896bb8a692 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 19 May 2017 17:03:10 +0200 Subject: Split metrics from health controller into metrics controller --- app/controllers/health_controller.rb | 29 ---------------- app/controllers/metrics_controller.rb | 41 +++++++++++++++++++++++ config/gitlab.yml.example | 5 +++ config/initializers/1_settings.rb | 1 + lib/gitlab/metrics.rb | 22 ------------- spec/controllers/health_controller_spec.rb | 39 ---------------------- spec/controllers/metrics_controller_spec.rb | 51 +++++++++++++++++++++++++++++ 7 files changed, 98 insertions(+), 90 deletions(-) create mode 100644 app/controllers/metrics_controller.rb create mode 100644 spec/controllers/metrics_controller_spec.rb diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb index 6f8038f6ec3..b646216caa2 100644 --- a/app/controllers/health_controller.rb +++ b/app/controllers/health_controller.rb @@ -22,37 +22,8 @@ class HealthController < ActionController::Base render_check_results(results) end - def metrics - response = health_metrics_text + "\n" - - if Gitlab::Metrics.prometheus_metrics_enabled? - response += Prometheus::Client::Formats::Text.marshal_multiprocess(ENV['prometheus_multiproc_dir']) - end - - render text: response, content_type: 'text/plain; version=0.0.4' - end - private - def health_metrics_text - results = CHECKS.flat_map(&:metrics) - - types = results.map(&:name) - .uniq - .map { |metric_name| "# TYPE #{metric_name} gauge" } - metrics = results.map(&method(:metric_to_prom_line)) - types.concat(metrics).join("\n") - end - - def metric_to_prom_line(metric) - labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || '' - if labels.empty? - "#{metric.name} #{metric.value}" - else - "#{metric.name}{#{labels}} #{metric.value}" - end - end - def render_check_results(results) flattened = results.flat_map do |name, result| if result.is_a?(Gitlab::HealthChecks::Result) diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb new file mode 100644 index 00000000000..a4ba77e235f --- /dev/null +++ b/app/controllers/metrics_controller.rb @@ -0,0 +1,41 @@ +require 'prometheus/client/formats/text' + +class MetricsController < ActionController::Base + protect_from_forgery with: :exception + + CHECKS = [ + Gitlab::HealthChecks::DbCheck, + Gitlab::HealthChecks::RedisCheck, + Gitlab::HealthChecks::FsShardsCheck, + ].freeze + + def metrics + render_404 unless Gitlab::Metrics.prometheus_metrics_enabled? + + metrics_text = Prometheus::Client::Formats::Text.marshal_multiprocess(Settings.gitlab['prometheus_multiproc_dir']) + response = health_metrics_text + "\n" + metrics_text + + render text: response, content_type: 'text/plain; version=0.0.4' + end + + private + + def health_metrics_text + results = CHECKS.flat_map(&:metrics) + + types = results.map(&:name) + .uniq + .map { |metric_name| "# TYPE #{metric_name} gauge" } + metrics = results.map(&method(:metric_to_prom_line)) + types.concat(metrics).join("\n") + end + + def metric_to_prom_line(metric) + labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || '' + if labels.empty? + "#{metric.name} #{metric.value}" + else + "#{metric.name}{#{labels}} #{metric.value}" + end + end +end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index d2aeb66ebf6..a6e4337912b 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -102,6 +102,11 @@ production: &base # The default is 'shared/cache/archive/' relative to the root of the Rails app. # repository_downloads_path: shared/cache/archive/ + ## Prometheus Client Data directory + # To be used efficiently in multiprocess Ruby setup like Unicorn, Prometheus client needs to share metrics with other instances. + # The default is 'tmp/prometheus_data_dir' relative to Rails.root + # prometheus_multiproc_dir: tmp/prometheus_data_dir + ## Reply by email # Allow users to comment on issues and merge requests by replying to notification emails. # For documentation on how to set this up, see http://doc.gitlab.com/ce/administration/reply_by_email.html diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 45ea2040d23..5db8746ef4c 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -242,6 +242,7 @@ Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fog Settings.gitlab['trusted_proxies'] ||= [] Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml')) Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil? +Settings.gitlab['prometheus_multiproc_dir'] ||= ENV['prometheus_multiproc_dir'] || 'tmp/prometheus_data_dir' # # CI diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 6f50c0aa028..9783d4e3582 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -78,28 +78,6 @@ module Gitlab def self.submit_metrics(metrics) prepared = prepare_metrics(metrics) - if prometheus_metrics_enabled? - metrics.map do |metric| - known = [:series, :tags,:values, :timestamp] - value = metric&.[](:values)&.[](:value) - handled= [:rails_gc_statistics] - if handled.include? metric[:series].to_sym - next - end - - if metric.keys.any? {|k| !known.include?(k)} || value.nil? - print metric - print "\n" - - {:series=>"rails_gc_statistics", :tags=>{}, :values=>{:count=>0, :heap_allocated_pages=>4245, :heap_sorted_length=>4426, :heap_allocatable_pages=>0, :heap_available_slots=>1730264, :heap_live_slots=>1729935, :heap_free_slots=>329, :heap_final_slots=>0, :heap_marked_slots=>1184216, :heap_swept_slots=>361843, :heap_eden_pages=>4245, :heap_tomb_pages=>0, :total_allocated_pages=>4245, :total_freed_pages=>0, :total_allocated_objects=>15670757, :total_freed_objects=>13940822, :malloc_increase_bytes=>4842256, :malloc_increase_bytes_limit=>29129457, :minor_gc_count=>0, :major_gc_count=>0, :remembered_wb_unprotected_objects=>39905, :remembered_wb_unprotected_objects_limit=>74474, :old_objects=>1078731, :old_objects_limit=>1975860, :oldmalloc_increase_bytes=>4842640, :oldmalloc_increase_bytes_limit=>31509677, :total_time=>0.0}, :timestamp=>1494356175592659968} - - next - end - metric_value = gauge(metric[:series].to_sym, metric[:series]) - metric_value.set(metric[:tags], value) - end - end - pool&.with do |connection| prepared.each_slice(settings[:packet_size]) do |slice| begin diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb index b8b6e0c3a88..e7c19b47a6a 100644 --- a/spec/controllers/health_controller_spec.rb +++ b/spec/controllers/health_controller_spec.rb @@ -54,43 +54,4 @@ describe HealthController do end end end - - describe '#metrics' do - context 'authorization token provided' do - before do - request.headers['TOKEN'] = token - end - - it 'returns DB ping metrics' do - get :metrics - expect(response.body).to match(/^db_ping_timeout 0$/) - expect(response.body).to match(/^db_ping_success 1$/) - expect(response.body).to match(/^db_ping_latency [0-9\.]+$/) - end - - it 'returns Redis ping metrics' do - get :metrics - expect(response.body).to match(/^redis_ping_timeout 0$/) - expect(response.body).to match(/^redis_ping_success 1$/) - expect(response.body).to match(/^redis_ping_latency [0-9\.]+$/) - end - - it 'returns file system check metrics' do - get :metrics - expect(response.body).to match(/^filesystem_access_latency{shard="default"} [0-9\.]+$/) - expect(response.body).to match(/^filesystem_accessible{shard="default"} 1$/) - expect(response.body).to match(/^filesystem_write_latency{shard="default"} [0-9\.]+$/) - expect(response.body).to match(/^filesystem_writable{shard="default"} 1$/) - expect(response.body).to match(/^filesystem_read_latency{shard="default"} [0-9\.]+$/) - expect(response.body).to match(/^filesystem_readable{shard="default"} 1$/) - end - end - - context 'without authorization token' do - it 'returns proper response' do - get :metrics - expect(response.status).to eq(404) - end - end - end end diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb new file mode 100644 index 00000000000..d2d4b361a62 --- /dev/null +++ b/spec/controllers/metrics_controller_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe MetricsController do + include StubENV + + let(:token) { current_application_settings.health_check_access_token } + let(:json_response) { JSON.parse(response.body) } + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + end + + describe '#metrics' do + context 'authorization token provided' do + before do + request.headers['TOKEN'] = token + end + + it 'returns DB ping metrics' do + get :metrics + expect(response.body).to match(/^db_ping_timeout 0$/) + expect(response.body).to match(/^db_ping_success 1$/) + expect(response.body).to match(/^db_ping_latency [0-9\.]+$/) + end + + it 'returns Redis ping metrics' do + get :metrics + expect(response.body).to match(/^redis_ping_timeout 0$/) + expect(response.body).to match(/^redis_ping_success 1$/) + expect(response.body).to match(/^redis_ping_latency [0-9\.]+$/) + end + + it 'returns file system check metrics' do + get :metrics + expect(response.body).to match(/^filesystem_access_latency{shard="default"} [0-9\.]+$/) + expect(response.body).to match(/^filesystem_accessible{shard="default"} 1$/) + expect(response.body).to match(/^filesystem_write_latency{shard="default"} [0-9\.]+$/) + expect(response.body).to match(/^filesystem_writable{shard="default"} 1$/) + expect(response.body).to match(/^filesystem_read_latency{shard="default"} [0-9\.]+$/) + expect(response.body).to match(/^filesystem_readable{shard="default"} 1$/) + end + end + + context 'without authorization token' do + it 'returns proper response' do + get :metrics + expect(response.status).to eq(404) + end + end + end +end -- cgit v1.2.1 From 138a5577a9e083933c02675dfa11112693ca7a94 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 19 May 2017 17:23:34 +0200 Subject: remove prometheus sampler --- lib/gitlab/metrics/prometheus_sampler.rb | 51 -------------------------------- 1 file changed, 51 deletions(-) delete mode 100644 lib/gitlab/metrics/prometheus_sampler.rb diff --git a/lib/gitlab/metrics/prometheus_sampler.rb b/lib/gitlab/metrics/prometheus_sampler.rb deleted file mode 100644 index 5f90d4f0b66..00000000000 --- a/lib/gitlab/metrics/prometheus_sampler.rb +++ /dev/null @@ -1,51 +0,0 @@ -module Gitlab - module Metrics - # Class that sends certain metrics to InfluxDB at a specific interval. - # - # This class is used to gather statistics that can't be directly associated - # with a transaction such as system memory usage, garbage collection - # statistics, etc. - class PrometheusSamples - # interval - The sampling interval in seconds. - def initialize(interval = Metrics.settings[:sample_interval]) - interval_half = interval.to_f / 2 - - @interval = interval - @interval_steps = (-interval_half..interval_half).step(0.1).to_a - end - - def start - Thread.new do - Thread.current.abort_on_exception = true - - loop do - sleep(sleep_interval) - - sample - end - end - end - - def sidekiq? - Sidekiq.server? - end - - # Returns the sleep interval with a random adjustment. - # - # The random adjustment is put in place to ensure we: - # - # 1. Don't generate samples at the exact same interval every time (thus - # potentially missing anything that happens in between samples). - # 2. Don't sample data at the same interval two times in a row. - def sleep_interval - while step = @interval_steps.sample - if step != @last_step - @last_step = step - - return @interval + @last_step - end - end - end - end - end -end -- cgit v1.2.1 From 6726922890fa0becd6e0bcd91ca5d2fa79fcccef Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 19 May 2017 17:30:51 +0200 Subject: Bring back the token --- app/controllers/metrics_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb index a4ba77e235f..f283aeb9db0 100644 --- a/app/controllers/metrics_controller.rb +++ b/app/controllers/metrics_controller.rb @@ -2,6 +2,7 @@ require 'prometheus/client/formats/text' class MetricsController < ActionController::Base protect_from_forgery with: :exception + include RequiresHealthToken CHECKS = [ Gitlab::HealthChecks::DbCheck, -- cgit v1.2.1 From c10d55a6dafc6dafe0d3b2d9fad1dc66aee07ff6 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 19 May 2017 18:27:36 +0200 Subject: Use only ENV for metrics folder location --- app/controllers/metrics_controller.rb | 8 ++++++-- config.ru | 5 ++--- config/gitlab.yml.example | 5 ----- config/initializers/1_settings.rb | 1 - config/routes.rb | 8 ++++---- 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb index f283aeb9db0..18c9625c36a 100644 --- a/app/controllers/metrics_controller.rb +++ b/app/controllers/metrics_controller.rb @@ -11,9 +11,9 @@ class MetricsController < ActionController::Base ].freeze def metrics - render_404 unless Gitlab::Metrics.prometheus_metrics_enabled? + return render_404 unless Gitlab::Metrics.prometheus_metrics_enabled? - metrics_text = Prometheus::Client::Formats::Text.marshal_multiprocess(Settings.gitlab['prometheus_multiproc_dir']) + metrics_text = Prometheus::Client::Formats::Text.marshal_multiprocess(multiprocess_metrics_path) response = health_metrics_text + "\n" + metrics_text render text: response, content_type: 'text/plain; version=0.0.4' @@ -21,6 +21,10 @@ class MetricsController < ActionController::Base private + def multiprocess_metrics_path + Rails.root.join(ENV['prometheus_multiproc_dir']) + end + def health_metrics_text results = CHECKS.flat_map(&:metrics) diff --git a/config.ru b/config.ru index cba44122da9..9e474a7c08c 100644 --- a/config.ru +++ b/config.ru @@ -13,10 +13,9 @@ if defined?(Unicorn) # Max memory size (RSS) per worker use Unicorn::WorkerKiller::Oom, min, max end - - # TODO(lyda): Needs to be set externally. - ENV['prometheus_multiproc_dir'] = '/tmp/somestuff' end +# set default for multiproces metrics gathering +ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_data_dir' require ::File.expand_path('../config/environment', __FILE__) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index a6e4337912b..d2aeb66ebf6 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -102,11 +102,6 @@ production: &base # The default is 'shared/cache/archive/' relative to the root of the Rails app. # repository_downloads_path: shared/cache/archive/ - ## Prometheus Client Data directory - # To be used efficiently in multiprocess Ruby setup like Unicorn, Prometheus client needs to share metrics with other instances. - # The default is 'tmp/prometheus_data_dir' relative to Rails.root - # prometheus_multiproc_dir: tmp/prometheus_data_dir - ## Reply by email # Allow users to comment on issues and merge requests by replying to notification emails. # For documentation on how to set this up, see http://doc.gitlab.com/ce/administration/reply_by_email.html diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 5db8746ef4c..45ea2040d23 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -242,7 +242,6 @@ Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fog Settings.gitlab['trusted_proxies'] ||= [] Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml')) Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil? -Settings.gitlab['prometheus_multiproc_dir'] ||= ENV['prometheus_multiproc_dir'] || 'tmp/prometheus_data_dir' # # CI diff --git a/config/routes.rb b/config/routes.rb index 846054e6917..4051c33d5d4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,10 +38,10 @@ Rails.application.routes.draw do # Health check get 'health_check(/:checks)' => 'health_check#index', as: :health_check - scope path: '-', controller: 'health' do - get :liveness - get :readiness - get :metrics + scope path: '-' do + get 'liveness' => 'health#liveness' + get 'readiness' => 'health#readiness' + get 'metrics' => 'metrics#metrics' end # Koding route -- cgit v1.2.1 From d38b064719a629c7059db0f1f75c6ceac187ab5b Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Mon, 22 May 2017 13:22:02 +0200 Subject: Bump prometheus client version --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b6a7e815170..0b795f41783 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -561,7 +561,7 @@ GEM premailer-rails (1.9.2) actionmailer (>= 3, < 6) premailer (~> 1.7, >= 1.7.9) - prometheus-client-mmap (0.7.0.beta1) + prometheus-client-mmap (0.7.0.beta3) quantile (~> 0.2.0) pry (0.10.4) coderay (~> 1.1.0) @@ -1000,7 +1000,7 @@ DEPENDENCIES pg (~> 0.18.2) poltergeist (~> 1.9.0) premailer-rails (~> 1.9.0) - prometheus-client-mmap (~> 0.7.0.beta1) + prometheus-client-mmap pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) rack-attack (~> 4.4.1) -- cgit v1.2.1 From dc4d4c18bdb2a0138ebb05496c14738a9304d1e8 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Mon, 22 May 2017 13:36:25 +0200 Subject: Ensure prometheus_data_dir exists --- tmp/prometheus_data_dir/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tmp/prometheus_data_dir/.gitkeep diff --git a/tmp/prometheus_data_dir/.gitkeep b/tmp/prometheus_data_dir/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d -- cgit v1.2.1 From 57902dbe826e0fd1db0a33662cafbef66b060ce2 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Mon, 22 May 2017 13:44:33 +0200 Subject: Add Changelog fix textual description in config.ru --- .../29118-add-prometheus-instrumenting-to-gitlab-webapp.yml | 4 ++++ config.ru | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/29118-add-prometheus-instrumenting-to-gitlab-webapp.yml diff --git a/changelogs/unreleased/29118-add-prometheus-instrumenting-to-gitlab-webapp.yml b/changelogs/unreleased/29118-add-prometheus-instrumenting-to-gitlab-webapp.yml new file mode 100644 index 00000000000..99c55f128e3 --- /dev/null +++ b/changelogs/unreleased/29118-add-prometheus-instrumenting-to-gitlab-webapp.yml @@ -0,0 +1,4 @@ +--- +title: Add prometheus based metrics collection to gitlab webapp +merge_request: +author: diff --git a/config.ru b/config.ru index 9e474a7c08c..cac4cf10c36 100644 --- a/config.ru +++ b/config.ru @@ -14,7 +14,7 @@ if defined?(Unicorn) use Unicorn::WorkerKiller::Oom, min, max end end -# set default for multiproces metrics gathering +# set default directory for multiproces metrics gathering ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_data_dir' require ::File.expand_path('../config/environment', __FILE__) -- cgit v1.2.1 From ef9d9ddeb2e063fa8ed1b01e4f82cc9662b919b2 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Mon, 22 May 2017 15:47:04 +0200 Subject: Add tests for metrics behavior --- lib/gitlab/metrics.rb | 16 ++--- spec/lib/gitlab/metrics_spec.rb | 125 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 9783d4e3582..a41cbd214a1 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -54,23 +54,25 @@ module Gitlab end def self.counter(name, docstring, base_labels = {}) - dummy_metric || registry.get(name) || registry.counter(name, docstring, base_labels) + provide_metric(name) || registry.counter(name, docstring, base_labels) end def self.summary(name, docstring, base_labels = {}) - dummy_metric || registry.get(name) || registry.summary(name, docstring, base_labels) + provide_metric(name) || registry.summary(name, docstring, base_labels) end def self.gauge(name, docstring, base_labels = {}) - dummy_metric || registry.get(name) || registry.gauge(name, docstring, base_labels) + provide_metric(name) || registry.gauge(name, docstring, base_labels) end - def self.histogram(name, docstring, base_labels = {}, buckets = Histogram::DEFAULT_BUCKETS) - dummy_metric || registry.get(name) || registry.histogram(name, docstring, base_labels, buckets) + def self.histogram(name, docstring, base_labels = {}, buckets = ::Prometheus::Client::Histogram::DEFAULT_BUCKETS) + provide_metric(name) || registry.histogram(name, docstring, base_labels, buckets) end - def self.dummy_metric - unless prometheus_metrics_enabled? + def self.provide_metric(name) + if prometheus_metrics_enabled? + registry.get(name) + else DummyMetric.new end end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 208a8d028cd..65bd06cda08 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -13,6 +13,18 @@ describe Gitlab::Metrics do end end + describe '.prometheus_metrics_enabled?' do + it 'returns a boolean' do + expect([true, false].include?(described_class.prometheus_metrics_enabled?)).to eq(true) + end + end + + describe '.influx_metrics_enabled?' do + it 'returns a boolean' do + expect([true, false].include?(described_class.influx_metrics_enabled?)).to eq(true) + end + end + describe '.submit_metrics' do it 'prepares and writes the metrics to InfluxDB' do connection = double(:connection) @@ -177,4 +189,117 @@ describe Gitlab::Metrics do end end end + + shared_examples 'prometheus metrics API' do + describe '#counter' do + subject { described_class.counter(:couter, 'doc') } + + describe '#increment' do + it { expect { subject.increment }.not_to raise_exception } + it { expect { subject.increment({}) }.not_to raise_exception } + it { expect { subject.increment({}, 1) }.not_to raise_exception } + end + end + + describe '#summary' do + subject { described_class.summary(:summary, 'doc') } + + describe '#observe' do + it { expect { subject.observe({}, 2) }.not_to raise_exception } + end + end + + describe '#gauge' do + subject { described_class.gauge(:gauge, 'doc') } + + describe '#observe' do + it { expect { subject.set({}, 1) }.not_to raise_exception } + end + end + + describe '#histogram' do + subject { described_class.histogram(:histogram, 'doc') } + + describe '#observe' do + it { expect { subject.observe({}, 2) }.not_to raise_exception } + end + end + end + + context 'prometheus metrics disabled' do + before do + allow(described_class).to receive(:prometheus_metrics_enabled?).and_return(false) + end + + it_behaves_like 'prometheus metrics API' + + describe '#dummy_metric' do + subject { described_class.provide_metric(:test) } + + it { is_expected.to be_a(Gitlab::Metrics::DummyMetric) } + end + + describe '#counter' do + subject { described_class.counter(:counter, 'doc') } + + it { is_expected.to be_a(Gitlab::Metrics::DummyMetric) } + + end + + describe '#summary' do + subject { described_class.summary(:summary, 'doc') } + + it { is_expected.to be_a(Gitlab::Metrics::DummyMetric) } + end + + describe '#gauge' do + subject { described_class.gauge(:gauge, 'doc') } + + it { is_expected.to be_a(Gitlab::Metrics::DummyMetric) } + end + + describe '#histogram' do + subject { described_class.histogram(:histogram, 'doc') } + + it { is_expected.to be_a(Gitlab::Metrics::DummyMetric) } + end + end + + context 'prometheus metrics enabled' do + before do + allow(described_class).to receive(:prometheus_metrics_enabled?).and_return(true) + end + + it_behaves_like 'prometheus metrics API' + + describe '#dummy_metric' do + subject { described_class.provide_metric(:test) } + + it { is_expected.to be_nil } + end + + describe '#counter' do + subject { described_class.counter(:name, 'doc') } + + it { is_expected.not_to be_a(Gitlab::Metrics::DummyMetric) } + end + + describe '#summary' do + subject { described_class.summary(:name, 'doc') } + + it { is_expected.not_to be_a(Gitlab::Metrics::DummyMetric) } + end + + describe '#gauge' do + subject { described_class.gauge(:name, 'doc') } + + it { is_expected.not_to be_a(Gitlab::Metrics::DummyMetric) } + end + + describe '#histogram' do + subject { described_class.histogram(:name, 'doc') } + + it { is_expected.not_to be_a(Gitlab::Metrics::DummyMetric) } + end + end end -- cgit v1.2.1 From 21561f3434021ad35d45c449f489802fd1dced67 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Mon, 22 May 2017 19:49:34 +0200 Subject: Correctly handle temporary folder for testing multiproces metrics --- Gemfile | 4 ++-- Gemfile.lock | 2 +- app/controllers/metrics_controller.rb | 2 +- spec/controllers/metrics_controller_spec.rb | 9 +++++++++ spec/lib/gitlab/metrics_spec.rb | 1 - 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index a6b0a20ef61..8ec2c94a2e0 100644 --- a/Gemfile +++ b/Gemfile @@ -269,9 +269,9 @@ group :metrics do gem 'method_source', '~> 0.8', require: false gem 'influxdb', '~> 0.2', require: false -# Prometheus + # Prometheus gem 'mmap2', '~> 2.2.6' - gem 'prometheus-client-mmap' + gem 'prometheus-client-mmap', '~>0.7.0.beta3' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 0b795f41783..bb574ae1834 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1000,7 +1000,7 @@ DEPENDENCIES pg (~> 0.18.2) poltergeist (~> 1.9.0) premailer-rails (~> 1.9.0) - prometheus-client-mmap + prometheus-client-mmap (~> 0.7.0.beta3) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) rack-attack (~> 4.4.1) diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb index 18c9625c36a..4c1d04c1262 100644 --- a/app/controllers/metrics_controller.rb +++ b/app/controllers/metrics_controller.rb @@ -7,7 +7,7 @@ class MetricsController < ActionController::Base CHECKS = [ Gitlab::HealthChecks::DbCheck, Gitlab::HealthChecks::RedisCheck, - Gitlab::HealthChecks::FsShardsCheck, + Gitlab::HealthChecks::FsShardsCheck ].freeze def metrics diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb index d2d4b361a62..7f2dcd3544f 100644 --- a/spec/controllers/metrics_controller_spec.rb +++ b/spec/controllers/metrics_controller_spec.rb @@ -6,8 +6,17 @@ describe MetricsController do let(:token) { current_application_settings.health_check_access_token } let(:json_response) { JSON.parse(response.body) } + around do |examples| + Dir.mktmpdir do |tmp_dir| + @metrics_multiproc_dir = tmp_dir + examples.run + end + end + before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + stub_env('prometheus_multiproc_dir', @metrics_multiproc_dir) + allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(true) end describe '#metrics' do diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 65bd06cda08..020bdbacead 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -243,7 +243,6 @@ describe Gitlab::Metrics do subject { described_class.counter(:counter, 'doc') } it { is_expected.to be_a(Gitlab::Metrics::DummyMetric) } - end describe '#summary' do -- cgit v1.2.1 From 62fe37e3f8e8ccee90a748324e1b40a54f4c55c8 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Tue, 23 May 2017 14:55:31 +0200 Subject: move check if metrics are enabled to before action --- app/controllers/metrics_controller.rb | 8 ++++++-- spec/controllers/metrics_controller_spec.rb | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb index 4c1d04c1262..b062496eb2a 100644 --- a/app/controllers/metrics_controller.rb +++ b/app/controllers/metrics_controller.rb @@ -3,6 +3,7 @@ require 'prometheus/client/formats/text' class MetricsController < ActionController::Base protect_from_forgery with: :exception include RequiresHealthToken + before_action :ensure_prometheus_metrics_are_enabled CHECKS = [ Gitlab::HealthChecks::DbCheck, @@ -10,9 +11,8 @@ class MetricsController < ActionController::Base Gitlab::HealthChecks::FsShardsCheck ].freeze - def metrics - return render_404 unless Gitlab::Metrics.prometheus_metrics_enabled? + def metrics metrics_text = Prometheus::Client::Formats::Text.marshal_multiprocess(multiprocess_metrics_path) response = health_metrics_text + "\n" + metrics_text @@ -21,6 +21,10 @@ class MetricsController < ActionController::Base private + def ensure_prometheus_metrics_are_enabled + return render_404 unless Gitlab::Metrics.prometheus_metrics_enabled? + end + def multiprocess_metrics_path Rails.root.join(ENV['prometheus_multiproc_dir']) end diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb index 7f2dcd3544f..c09c3a1f6b7 100644 --- a/spec/controllers/metrics_controller_spec.rb +++ b/spec/controllers/metrics_controller_spec.rb @@ -48,6 +48,15 @@ describe MetricsController do expect(response.body).to match(/^filesystem_read_latency{shard="default"} [0-9\.]+$/) expect(response.body).to match(/^filesystem_readable{shard="default"} 1$/) end + + context 'prometheus metrics are disabled' do + allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(false) + + it 'returns proper response' do + get :metrics + expect(response.status).to eq(404) + end + end end context 'without authorization token' do @@ -56,5 +65,6 @@ describe MetricsController do expect(response.status).to eq(404) end end + end end -- cgit v1.2.1 From 466beeb31f9ede870f1e6f4e85642a375663eaf2 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Tue, 23 May 2017 15:18:03 +0200 Subject: Use interpolation instead of concatenation --- app/controllers/metrics_controller.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb index b062496eb2a..8b99de06d85 100644 --- a/app/controllers/metrics_controller.rb +++ b/app/controllers/metrics_controller.rb @@ -14,7 +14,7 @@ class MetricsController < ActionController::Base def metrics metrics_text = Prometheus::Client::Formats::Text.marshal_multiprocess(multiprocess_metrics_path) - response = health_metrics_text + "\n" + metrics_text + response = "#{health_metrics_text}\n#{metrics_text}" render text: response, content_type: 'text/plain; version=0.0.4' end @@ -32,15 +32,15 @@ class MetricsController < ActionController::Base def health_metrics_text results = CHECKS.flat_map(&:metrics) - types = results.map(&:name) - .uniq - .map { |metric_name| "# TYPE #{metric_name} gauge" } + types = results.map(&:name).uniq.map { |metric_name| "# TYPE #{metric_name} gauge" } metrics = results.map(&method(:metric_to_prom_line)) + types.concat(metrics).join("\n") end def metric_to_prom_line(metric) labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || '' + if labels.empty? "#{metric.name} #{metric.value}" else -- cgit v1.2.1 From 254830c1f963f344585a45d96a03985e1ec2df0e Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Tue, 23 May 2017 15:42:36 +0200 Subject: Move most of MetricsController logic to MetricsService --- app/controllers/metrics_controller.rb | 41 +++++------------------------ app/services/metrics_service.rb | 38 ++++++++++++++++++++++++++ spec/controllers/metrics_controller_spec.rb | 5 ++-- 3 files changed, 47 insertions(+), 37 deletions(-) create mode 100644 app/services/metrics_service.rb diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb index 8b99de06d85..7191a66fe46 100644 --- a/app/controllers/metrics_controller.rb +++ b/app/controllers/metrics_controller.rb @@ -1,50 +1,21 @@ -require 'prometheus/client/formats/text' - class MetricsController < ActionController::Base protect_from_forgery with: :exception + before_action :validate_prometheus_metrics include RequiresHealthToken - before_action :ensure_prometheus_metrics_are_enabled - - CHECKS = [ - Gitlab::HealthChecks::DbCheck, - Gitlab::HealthChecks::RedisCheck, - Gitlab::HealthChecks::FsShardsCheck - ].freeze - def metrics - metrics_text = Prometheus::Client::Formats::Text.marshal_multiprocess(multiprocess_metrics_path) - response = "#{health_metrics_text}\n#{metrics_text}" + response = "#{metrics_service.health_metrics_text}\n#{metrics_service.prometheus_metrics_text}" render text: response, content_type: 'text/plain; version=0.0.4' end private - def ensure_prometheus_metrics_are_enabled - return render_404 unless Gitlab::Metrics.prometheus_metrics_enabled? - end - - def multiprocess_metrics_path - Rails.root.join(ENV['prometheus_multiproc_dir']) + def metrics_service + @metrics_service ||= MetricsService.new end - def health_metrics_text - results = CHECKS.flat_map(&:metrics) - - types = results.map(&:name).uniq.map { |metric_name| "# TYPE #{metric_name} gauge" } - metrics = results.map(&method(:metric_to_prom_line)) - - types.concat(metrics).join("\n") - end - - def metric_to_prom_line(metric) - labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || '' - - if labels.empty? - "#{metric.name} #{metric.value}" - else - "#{metric.name}{#{labels}} #{metric.value}" - end + def validate_prometheus_metrics + render_404 unless Gitlab::Metrics.prometheus_metrics_enabled? end end diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb new file mode 100644 index 00000000000..350c3639e92 --- /dev/null +++ b/app/services/metrics_service.rb @@ -0,0 +1,38 @@ +require 'prometheus/client/formats/text' + +class MetricsService + CHECKS = [ + Gitlab::HealthChecks::DbCheck, + Gitlab::HealthChecks::RedisCheck, + Gitlab::HealthChecks::FsShardsCheck + ].freeze + + def prometheus_metrics_text + Prometheus::Client::Formats::Text.marshal_multiprocess(multiprocess_metrics_path) + end + + def health_metrics_text + results = CHECKS.flat_map(&:metrics) + + types = results.map(&:name).uniq.map { |metric_name| "# TYPE #{metric_name} gauge" } + metrics = results.map(&method(:metric_to_prom_line)) + + types.concat(metrics).join("\n") + end + + private + + def multiprocess_metrics_path + Rails.root.join(ENV['prometheus_multiproc_dir']) + end + + def metric_to_prom_line(metric) + labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || '' + + if labels.empty? + "#{metric.name} #{metric.value}" + else + "#{metric.name}{#{labels}} #{metric.value}" + end + end +end diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb index c09c3a1f6b7..99ad7b7738b 100644 --- a/spec/controllers/metrics_controller_spec.rb +++ b/spec/controllers/metrics_controller_spec.rb @@ -50,7 +50,9 @@ describe MetricsController do end context 'prometheus metrics are disabled' do - allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(false) + before do + allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(false) + end it 'returns proper response' do get :metrics @@ -65,6 +67,5 @@ describe MetricsController do expect(response.status).to eq(404) end end - end end -- cgit v1.2.1 From ef9f23b797d7467a1d1bda15e29d1f33b070065f Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Tue, 23 May 2017 16:09:00 +0200 Subject: Mark migration as requiring no downtime + Add spaces for four phases approach + fix InfluxDB rename --- app/views/admin/application_settings/_form.html.haml | 2 +- config.ru | 1 + .../20170519102115_add_prometheus_settings_to_metrics_settings.rb | 2 ++ spec/controllers/metrics_controller_spec.rb | 5 +++++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 3c020c71832..d552704df88 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -234,7 +234,7 @@ %fieldset %legend Metrics - Influx %p - Setup Influx to measure a wide variety of statistics like the time spent + Setup InfluxDB to measure a wide variety of statistics like the time spent in running SQL queries. These settings require a = link_to 'restart', help_page_path('administration/restart_gitlab') to take effect. diff --git a/config.ru b/config.ru index cac4cf10c36..2614c9aaf74 100644 --- a/config.ru +++ b/config.ru @@ -14,6 +14,7 @@ if defined?(Unicorn) use Unicorn::WorkerKiller::Oom, min, max end end + # set default directory for multiproces metrics gathering ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_data_dir' diff --git a/db/migrate/20170519102115_add_prometheus_settings_to_metrics_settings.rb b/db/migrate/20170519102115_add_prometheus_settings_to_metrics_settings.rb index e675fb7cd58..4e2c94be943 100644 --- a/db/migrate/20170519102115_add_prometheus_settings_to_metrics_settings.rb +++ b/db/migrate/20170519102115_add_prometheus_settings_to_metrics_settings.rb @@ -2,6 +2,8 @@ class AddPrometheusSettingsToMetricsSettings < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! + DOWNTIME = false + def up add_column_with_default(:application_settings, :prometheus_metrics_enabled, :boolean, default: false, allow_null: false) diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb index 99ad7b7738b..cd31f750ffd 100644 --- a/spec/controllers/metrics_controller_spec.rb +++ b/spec/controllers/metrics_controller_spec.rb @@ -27,6 +27,7 @@ describe MetricsController do it 'returns DB ping metrics' do get :metrics + expect(response.body).to match(/^db_ping_timeout 0$/) expect(response.body).to match(/^db_ping_success 1$/) expect(response.body).to match(/^db_ping_latency [0-9\.]+$/) @@ -34,6 +35,7 @@ describe MetricsController do it 'returns Redis ping metrics' do get :metrics + expect(response.body).to match(/^redis_ping_timeout 0$/) expect(response.body).to match(/^redis_ping_success 1$/) expect(response.body).to match(/^redis_ping_latency [0-9\.]+$/) @@ -41,6 +43,7 @@ describe MetricsController do it 'returns file system check metrics' do get :metrics + expect(response.body).to match(/^filesystem_access_latency{shard="default"} [0-9\.]+$/) expect(response.body).to match(/^filesystem_accessible{shard="default"} 1$/) expect(response.body).to match(/^filesystem_write_latency{shard="default"} [0-9\.]+$/) @@ -56,6 +59,7 @@ describe MetricsController do it 'returns proper response' do get :metrics + expect(response.status).to eq(404) end end @@ -64,6 +68,7 @@ describe MetricsController do context 'without authorization token' do it 'returns proper response' do get :metrics + expect(response.status).to eq(404) end end -- cgit v1.2.1 From 394e962e52efdff36e3fae974ea51e9e2883f382 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Tue, 23 May 2017 16:16:23 +0200 Subject: Make tests of Gitlab::Metrics use explicit descriptions. --- spec/lib/gitlab/metrics_spec.rb | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 020bdbacead..863868e1576 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -9,19 +9,19 @@ describe Gitlab::Metrics do describe '.enabled?' do it 'returns a boolean' do - expect([true, false].include?(described_class.enabled?)).to eq(true) + expect(described_class.enabled?).to be_in([true, false]) end end describe '.prometheus_metrics_enabled?' do it 'returns a boolean' do - expect([true, false].include?(described_class.prometheus_metrics_enabled?)).to eq(true) + expect(described_class.prometheus_metrics_enabled?).to be_in([true, false]) end end describe '.influx_metrics_enabled?' do it 'returns a boolean' do - expect([true, false].include?(described_class.influx_metrics_enabled?)).to eq(true) + expect(described_class.influx_metrics_enabled?).to be_in([true, false]) end end @@ -195,9 +195,17 @@ describe Gitlab::Metrics do subject { described_class.counter(:couter, 'doc') } describe '#increment' do - it { expect { subject.increment }.not_to raise_exception } - it { expect { subject.increment({}) }.not_to raise_exception } - it { expect { subject.increment({}, 1) }.not_to raise_exception } + it 'successfully calls #increment without arguments' do + expect { subject.increment }.not_to raise_exception + end + + it 'successfully calls #increment with 1 argument' do + expect { subject.increment({}) }.not_to raise_exception + end + + it 'successfully calls #increment with 2 arguments' do + expect { subject.increment({}, 1) }.not_to raise_exception + end end end @@ -205,15 +213,19 @@ describe Gitlab::Metrics do subject { described_class.summary(:summary, 'doc') } describe '#observe' do - it { expect { subject.observe({}, 2) }.not_to raise_exception } + it 'successfully calls #observe with 2 arguments' do + expect { subject.observe({}, 2) }.not_to raise_exception + end end end describe '#gauge' do subject { described_class.gauge(:gauge, 'doc') } - describe '#observe' do - it { expect { subject.set({}, 1) }.not_to raise_exception } + describe '#set' do + it 'successfully calls #set with 2 arguments' do + expect { subject.set({}, 1) }.not_to raise_exception + end end end @@ -221,7 +233,9 @@ describe Gitlab::Metrics do subject { described_class.histogram(:histogram, 'doc') } describe '#observe' do - it { expect { subject.observe({}, 2) }.not_to raise_exception } + it 'successfully calls #observe with 2 arguments' do + expect { subject.observe({}, 2) }.not_to raise_exception + end end end end -- cgit v1.2.1 From 770f07cd5c68075bb261f4b6139c92b2ac9309c0 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Tue, 23 May 2017 16:23:43 +0200 Subject: Make login_counter instance variable instead of class one. + remove unecessarey require + fix small formatiing issues --- app/controllers/health_controller.rb | 2 -- app/controllers/metrics_controller.rb | 4 +++- app/controllers/sessions_controller.rb | 4 ++-- .../20170519102115_add_prometheus_settings_to_metrics_settings.rb | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb index b646216caa2..abc832e6ddc 100644 --- a/app/controllers/health_controller.rb +++ b/app/controllers/health_controller.rb @@ -1,5 +1,3 @@ -require 'prometheus/client/formats/text' - class HealthController < ActionController::Base protect_from_forgery with: :exception include RequiresHealthToken diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb index 7191a66fe46..9bcd6f96b34 100644 --- a/app/controllers/metrics_controller.rb +++ b/app/controllers/metrics_controller.rb @@ -1,7 +1,9 @@ class MetricsController < ActionController::Base + include RequiresHealthToken + protect_from_forgery with: :exception + before_action :validate_prometheus_metrics - include RequiresHealthToken def metrics response = "#{metrics_service.health_metrics_text}\n#{metrics_service.prometheus_metrics_text}" diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index eaed878283e..fc9de30f256 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -47,7 +47,7 @@ class SessionsController < Devise::SessionsController private - def self.login_counter + def login_counter @login_counter ||= Gitlab::Metrics.counter(:user_session_logins, 'User logins count') end @@ -129,7 +129,7 @@ class SessionsController < Devise::SessionsController end def log_user_activity(user) - SessionsController.login_counter.increment + login_counter.increment Users::ActivityService.new(user, 'login').execute end diff --git a/db/migrate/20170519102115_add_prometheus_settings_to_metrics_settings.rb b/db/migrate/20170519102115_add_prometheus_settings_to_metrics_settings.rb index 4e2c94be943..6ec2ed712b9 100644 --- a/db/migrate/20170519102115_add_prometheus_settings_to_metrics_settings.rb +++ b/db/migrate/20170519102115_add_prometheus_settings_to_metrics_settings.rb @@ -1,5 +1,6 @@ class AddPrometheusSettingsToMetricsSettings < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! DOWNTIME = false -- cgit v1.2.1 From c134a72cdb7e6de8b70dc60de99cf4edc68a9227 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Mon, 29 May 2017 14:19:43 +0200 Subject: Move Prometheus presentation logic to PrometheusText + Use NullMetrics to mock metrics when unused + Use method_missing in NullMetrics mocking + Update prometheus gem to version that correctly uses transitive dependencies + Ensure correct folders are used in Multiprocess prometheus client tests. + rename Sessions controller's metric --- Gemfile | 3 +-- Gemfile.lock | 8 +++--- app/controllers/metrics_controller.rb | 6 ++--- app/controllers/sessions_controller.rb | 2 +- app/services/metrics_service.rb | 15 ++++++++---- config.ru | 2 +- lib/gitlab/health_checks/prometheus_text.rb | 38 +++++++++++++++++++++++++++++ lib/gitlab/metrics.rb | 2 +- lib/gitlab/metrics/dummy_metric.rb | 29 ---------------------- lib/gitlab/metrics/null_metric.rb | 19 +++++++++++++++ spec/controllers/metrics_controller_spec.rb | 4 +-- spec/lib/gitlab/metrics_spec.rb | 32 +++++++++++++++--------- spec/spec_helper.rb | 1 + tmp/prometheus_data_dir/.gitkeep | 0 tmp/prometheus_multiproc_dir/.gitkeep | 0 15 files changed, 100 insertions(+), 61 deletions(-) create mode 100644 lib/gitlab/health_checks/prometheus_text.rb delete mode 100644 lib/gitlab/metrics/dummy_metric.rb create mode 100644 lib/gitlab/metrics/null_metric.rb delete mode 100644 tmp/prometheus_data_dir/.gitkeep create mode 100644 tmp/prometheus_multiproc_dir/.gitkeep diff --git a/Gemfile b/Gemfile index 8ec2c94a2e0..0c18007d447 100644 --- a/Gemfile +++ b/Gemfile @@ -270,8 +270,7 @@ group :metrics do gem 'influxdb', '~> 0.2', require: false # Prometheus - gem 'mmap2', '~> 2.2.6' - gem 'prometheus-client-mmap', '~>0.7.0.beta3' + gem 'prometheus-client-mmap', '~>0.7.0.beta5' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index bb574ae1834..12520570dfe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -561,8 +561,8 @@ GEM premailer-rails (1.9.2) actionmailer (>= 3, < 6) premailer (~> 1.7, >= 1.7.9) - prometheus-client-mmap (0.7.0.beta3) - quantile (~> 0.2.0) + prometheus-client-mmap (0.7.0.beta5) + mmap2 (~> 2.2.6) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -573,7 +573,6 @@ GEM pry-rails (0.3.5) pry (>= 0.9.10) pyu-ruby-sasl (0.0.3.3) - quantile (0.2.0) rack (1.6.5) rack-accept (0.4.5) rack (>= 0.4) @@ -972,7 +971,6 @@ DEPENDENCIES mail_room (~> 0.9.1) method_source (~> 0.8) minitest (~> 5.7.0) - mmap2 (~> 2.2.6) mousetrap-rails (~> 1.4.6) mysql2 (~> 0.3.16) net-ssh (~> 3.0.1) @@ -1000,7 +998,7 @@ DEPENDENCIES pg (~> 0.18.2) poltergeist (~> 1.9.0) premailer-rails (~> 1.9.0) - prometheus-client-mmap (~> 0.7.0.beta3) + prometheus-client-mmap (~> 0.7.0.beta5) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) rack-attack (~> 4.4.1) diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb index 9bcd6f96b34..0e9a19c0b6f 100644 --- a/app/controllers/metrics_controller.rb +++ b/app/controllers/metrics_controller.rb @@ -5,10 +5,8 @@ class MetricsController < ActionController::Base before_action :validate_prometheus_metrics - def metrics - response = "#{metrics_service.health_metrics_text}\n#{metrics_service.prometheus_metrics_text}" - - render text: response, content_type: 'text/plain; version=0.0.4' + def index + render text: metrics_service.metrics_text, content_type: 'text/plain; verssion=0.0.4' end private diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index fc9de30f256..ab52b676e01 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -48,7 +48,7 @@ class SessionsController < Devise::SessionsController private def login_counter - @login_counter ||= Gitlab::Metrics.counter(:user_session_logins, 'User logins count') + @login_counter ||= Gitlab::Metrics.counter(:user_session_logins, 'User sign in count') end # Handle an "initial setup" state, where there's only one user, it's an admin, diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb index 350c3639e92..0faa86a228b 100644 --- a/app/services/metrics_service.rb +++ b/app/services/metrics_service.rb @@ -12,18 +12,23 @@ class MetricsService end def health_metrics_text - results = CHECKS.flat_map(&:metrics) + metrics = CHECKS.flat_map(&:metrics) - types = results.map(&:name).uniq.map { |metric_name| "# TYPE #{metric_name} gauge" } - metrics = results.map(&method(:metric_to_prom_line)) + formatter.marshal(metrics) + end - types.concat(metrics).join("\n") + def metrics_text + "#{health_metrics_text}\n#{prometheus_metrics_text}" end private + def formatter + @formatter ||= PrometheusText.new + end + def multiprocess_metrics_path - Rails.root.join(ENV['prometheus_multiproc_dir']) + @multiprocess_metrics_path ||= Rails.root.join(ENV['prometheus_multiproc_dir']) end def metric_to_prom_line(metric) diff --git a/config.ru b/config.ru index 2614c9aaf74..89aba462f19 100644 --- a/config.ru +++ b/config.ru @@ -16,7 +16,7 @@ if defined?(Unicorn) end # set default directory for multiproces metrics gathering -ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_data_dir' +ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_multiproc_dir' require ::File.expand_path('../config/environment', __FILE__) diff --git a/lib/gitlab/health_checks/prometheus_text.rb b/lib/gitlab/health_checks/prometheus_text.rb new file mode 100644 index 00000000000..a01e6b2be1f --- /dev/null +++ b/lib/gitlab/health_checks/prometheus_text.rb @@ -0,0 +1,38 @@ +module Gitlab::HealthChecks + class PrometheusText + def marshal(metrics) + metrics_with_type_declarations(metrics).join("\n") + end + + private + + def metrics_with_type_declarations(metrics) + type_declaration_added = {} + + metrics.flat_map do |metric| + metric_lines = [] + + unless type_declaration_added.has_key?(metric.name) + type_declaration_added[metric.name] = true + metric_lines << metric_type_declaration(metric) + end + + metric_lines << metric_text(metric) + end + end + + def metric_type_declaration(metric) + "# TYPE #{metric.name} gauge" + end + + def metric_text(metric) + labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || '' + + if labels.empty? + "#{metric.name} #{metric.value}" + else + "#{metric.name}{#{labels}} #{metric.value}" + end + end + end +end diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index a41cbd214a1..34f6b32f7da 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -73,7 +73,7 @@ module Gitlab if prometheus_metrics_enabled? registry.get(name) else - DummyMetric.new + NullMetric.new end end diff --git a/lib/gitlab/metrics/dummy_metric.rb b/lib/gitlab/metrics/dummy_metric.rb deleted file mode 100644 index d27bb83854a..00000000000 --- a/lib/gitlab/metrics/dummy_metric.rb +++ /dev/null @@ -1,29 +0,0 @@ -module Gitlab - module Metrics - # Mocks ::Prometheus::Client::Metric and all derived metrics - class DummyMetric - def get(*args) - raise NotImplementedError - end - - def values(*args) - raise NotImplementedError - end - - # counter - def increment(*args) - # noop - end - - # gauge - def set(*args) - # noop - end - - # histogram / summary - def observe(*args) - # noop - end - end - end -end diff --git a/lib/gitlab/metrics/null_metric.rb b/lib/gitlab/metrics/null_metric.rb new file mode 100644 index 00000000000..1501cd38676 --- /dev/null +++ b/lib/gitlab/metrics/null_metric.rb @@ -0,0 +1,19 @@ +module Gitlab + module Metrics + # Mocks ::Prometheus::Client::Metric and all derived metrics + class NullMetric + def method_missing(name, *args, &block) + nil + end + + # these methods shouldn't be called when metrics are disabled + def get(*args) + raise NotImplementedError + end + + def values(*args) + raise NotImplementedError + end + end + end +end diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb index cd31f750ffd..7baf7d1bef9 100644 --- a/spec/controllers/metrics_controller_spec.rb +++ b/spec/controllers/metrics_controller_spec.rb @@ -6,10 +6,10 @@ describe MetricsController do let(:token) { current_application_settings.health_check_access_token } let(:json_response) { JSON.parse(response.body) } - around do |examples| + around do |example| Dir.mktmpdir do |tmp_dir| @metrics_multiproc_dir = tmp_dir - examples.run + example.run end end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 863868e1576..87c9f4ebda4 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::Metrics do + include StubENV + describe '.settings' do it 'returns a Hash' do expect(described_class.settings).to be_an_instance_of(Hash) @@ -247,45 +249,53 @@ describe Gitlab::Metrics do it_behaves_like 'prometheus metrics API' - describe '#dummy_metric' do + describe '#null_metric' do subject { described_class.provide_metric(:test) } - it { is_expected.to be_a(Gitlab::Metrics::DummyMetric) } + it { is_expected.to be_a(Gitlab::Metrics::NullMetric) } end describe '#counter' do subject { described_class.counter(:counter, 'doc') } - it { is_expected.to be_a(Gitlab::Metrics::DummyMetric) } + it { is_expected.to be_a(Gitlab::Metrics::NullMetric) } end describe '#summary' do subject { described_class.summary(:summary, 'doc') } - it { is_expected.to be_a(Gitlab::Metrics::DummyMetric) } + it { is_expected.to be_a(Gitlab::Metrics::NullMetric) } end describe '#gauge' do subject { described_class.gauge(:gauge, 'doc') } - it { is_expected.to be_a(Gitlab::Metrics::DummyMetric) } + it { is_expected.to be_a(Gitlab::Metrics::NullMetric) } end describe '#histogram' do subject { described_class.histogram(:histogram, 'doc') } - it { is_expected.to be_a(Gitlab::Metrics::DummyMetric) } + it { is_expected.to be_a(Gitlab::Metrics::NullMetric) } end end context 'prometheus metrics enabled' do + around do |example| + Dir.mktmpdir do |tmp_dir| + @metrics_multiproc_dir = tmp_dir + example.run + end + end + before do + stub_const('Prometheus::Client::Multiprocdir', @metrics_multiproc_dir) allow(described_class).to receive(:prometheus_metrics_enabled?).and_return(true) end it_behaves_like 'prometheus metrics API' - describe '#dummy_metric' do + describe '#null_metric' do subject { described_class.provide_metric(:test) } it { is_expected.to be_nil } @@ -294,25 +304,25 @@ describe Gitlab::Metrics do describe '#counter' do subject { described_class.counter(:name, 'doc') } - it { is_expected.not_to be_a(Gitlab::Metrics::DummyMetric) } + it { is_expected.not_to be_a(Gitlab::Metrics::NullMetric) } end describe '#summary' do subject { described_class.summary(:name, 'doc') } - it { is_expected.not_to be_a(Gitlab::Metrics::DummyMetric) } + it { is_expected.not_to be_a(Gitlab::Metrics::NullMetric) } end describe '#gauge' do subject { described_class.gauge(:name, 'doc') } - it { is_expected.not_to be_a(Gitlab::Metrics::DummyMetric) } + it { is_expected.not_to be_a(Gitlab::Metrics::NullMetric) } end describe '#histogram' do subject { described_class.histogram(:name, 'doc') } - it { is_expected.not_to be_a(Gitlab::Metrics::DummyMetric) } + it { is_expected.not_to be_a(Gitlab::Metrics::NullMetric) } end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 994c7dcbb46..f800c5bcb07 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,6 +3,7 @@ SimpleCovEnv.start! ENV["RAILS_ENV"] ||= 'test' ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true' +# ENV['prometheus_multiproc_dir'] = 'tmp/prometheus_multiproc_dir_test' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' diff --git a/tmp/prometheus_data_dir/.gitkeep b/tmp/prometheus_data_dir/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tmp/prometheus_multiproc_dir/.gitkeep b/tmp/prometheus_multiproc_dir/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d -- cgit v1.2.1 From ae8f7666e597493ab404f8524c1216a924338291 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Mon, 29 May 2017 23:23:19 +0200 Subject: Add prometheus text formatter + rename controler method to #index from #metrics + remove assertion from nullMetric --- app/services/metrics_service.rb | 4 +- config/routes.rb | 2 +- lib/gitlab/health_checks/prometheus_text.rb | 38 ------------------- lib/gitlab/health_checks/prometheus_text_format.rb | 38 +++++++++++++++++++ lib/gitlab/metrics/null_metric.rb | 9 ----- spec/controllers/metrics_controller_spec.rb | 12 +++--- .../health_checks/prometheus_text_format_spec.rb | 44 ++++++++++++++++++++++ 7 files changed, 91 insertions(+), 56 deletions(-) delete mode 100644 lib/gitlab/health_checks/prometheus_text.rb create mode 100644 lib/gitlab/health_checks/prometheus_text_format.rb create mode 100644 spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb index 0faa86a228b..025598cdc76 100644 --- a/app/services/metrics_service.rb +++ b/app/services/metrics_service.rb @@ -24,11 +24,11 @@ class MetricsService private def formatter - @formatter ||= PrometheusText.new + @formatter ||= Gitlab::HealthChecks::PrometheusTextFormat.new end def multiprocess_metrics_path - @multiprocess_metrics_path ||= Rails.root.join(ENV['prometheus_multiproc_dir']) + @multiprocess_metrics_path ||= Rails.root.join(ENV['prometheus_multiproc_dir']).freeze end def metric_to_prom_line(metric) diff --git a/config/routes.rb b/config/routes.rb index 4051c33d5d4..d909be38b42 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -41,7 +41,7 @@ Rails.application.routes.draw do scope path: '-' do get 'liveness' => 'health#liveness' get 'readiness' => 'health#readiness' - get 'metrics' => 'metrics#metrics' + resources :metrics, only: [:index] end # Koding route diff --git a/lib/gitlab/health_checks/prometheus_text.rb b/lib/gitlab/health_checks/prometheus_text.rb deleted file mode 100644 index a01e6b2be1f..00000000000 --- a/lib/gitlab/health_checks/prometheus_text.rb +++ /dev/null @@ -1,38 +0,0 @@ -module Gitlab::HealthChecks - class PrometheusText - def marshal(metrics) - metrics_with_type_declarations(metrics).join("\n") - end - - private - - def metrics_with_type_declarations(metrics) - type_declaration_added = {} - - metrics.flat_map do |metric| - metric_lines = [] - - unless type_declaration_added.has_key?(metric.name) - type_declaration_added[metric.name] = true - metric_lines << metric_type_declaration(metric) - end - - metric_lines << metric_text(metric) - end - end - - def metric_type_declaration(metric) - "# TYPE #{metric.name} gauge" - end - - def metric_text(metric) - labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || '' - - if labels.empty? - "#{metric.name} #{metric.value}" - else - "#{metric.name}{#{labels}} #{metric.value}" - end - end - end -end diff --git a/lib/gitlab/health_checks/prometheus_text_format.rb b/lib/gitlab/health_checks/prometheus_text_format.rb new file mode 100644 index 00000000000..5fc6f19c37c --- /dev/null +++ b/lib/gitlab/health_checks/prometheus_text_format.rb @@ -0,0 +1,38 @@ +module Gitlab::HealthChecks + class PrometheusTextFormat + def marshal(metrics) + metrics_with_type_declarations(metrics).join("\n") + end + + private + + def metrics_with_type_declarations(metrics) + type_declaration_added = {} + + metrics.flat_map do |metric| + metric_lines = [] + + unless type_declaration_added.has_key?(metric.name) + type_declaration_added[metric.name] = true + metric_lines << metric_type_declaration(metric) + end + + metric_lines << metric_text(metric) + end + end + + def metric_type_declaration(metric) + "# TYPE #{metric.name} gauge" + end + + def metric_text(metric) + labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || '' + + if labels.empty? + "#{metric.name} #{metric.value}" + else + "#{metric.name}{#{labels}} #{metric.value}" + end + end + end +end diff --git a/lib/gitlab/metrics/null_metric.rb b/lib/gitlab/metrics/null_metric.rb index 1501cd38676..3b5a2907195 100644 --- a/lib/gitlab/metrics/null_metric.rb +++ b/lib/gitlab/metrics/null_metric.rb @@ -5,15 +5,6 @@ module Gitlab def method_missing(name, *args, &block) nil end - - # these methods shouldn't be called when metrics are disabled - def get(*args) - raise NotImplementedError - end - - def values(*args) - raise NotImplementedError - end end end end diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb index 7baf7d1bef9..b26ebc1377b 100644 --- a/spec/controllers/metrics_controller_spec.rb +++ b/spec/controllers/metrics_controller_spec.rb @@ -19,14 +19,14 @@ describe MetricsController do allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(true) end - describe '#metrics' do + describe '#index' do context 'authorization token provided' do before do request.headers['TOKEN'] = token end it 'returns DB ping metrics' do - get :metrics + get :index expect(response.body).to match(/^db_ping_timeout 0$/) expect(response.body).to match(/^db_ping_success 1$/) @@ -34,7 +34,7 @@ describe MetricsController do end it 'returns Redis ping metrics' do - get :metrics + get :index expect(response.body).to match(/^redis_ping_timeout 0$/) expect(response.body).to match(/^redis_ping_success 1$/) @@ -42,7 +42,7 @@ describe MetricsController do end it 'returns file system check metrics' do - get :metrics + get :index expect(response.body).to match(/^filesystem_access_latency{shard="default"} [0-9\.]+$/) expect(response.body).to match(/^filesystem_accessible{shard="default"} 1$/) @@ -58,7 +58,7 @@ describe MetricsController do end it 'returns proper response' do - get :metrics + get :index expect(response.status).to eq(404) end @@ -67,7 +67,7 @@ describe MetricsController do context 'without authorization token' do it 'returns proper response' do - get :metrics + get :index expect(response.status).to eq(404) end diff --git a/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb b/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb new file mode 100644 index 00000000000..a9feab8ff78 --- /dev/null +++ b/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb @@ -0,0 +1,44 @@ +describe Gitlab::HealthChecks::PrometheusTextFormat do + let(:metric_class) { Gitlab::HealthChecks::Metric } + subject { described_class.new } + + describe '#marshal' do + let(:sample_metrics) do + [ + metric_class.new('metric1', 1), + metric_class.new('metric2', 2) + ] + end + + it 'marshal to text with non repeating type definition' do + expected = <<-EXPECTED +# TYPE metric1 gauge +metric1 1 +# TYPE metric2 gauge +metric2 2 +EXPECTED + expect(subject.marshal(sample_metrics)).to eq(expected.chomp) + end + + context 'metrics where name repeats' do + let(:sample_metrics) do + [ + metric_class.new('metric1', 1), + metric_class.new('metric1', 2), + metric_class.new('metric2', 3) + ] + end + + it 'marshal to text with non repeating type definition' do + expected = <<-EXPECTED +# TYPE metric1 gauge +metric1 1 +metric1 2 +# TYPE metric2 gauge +metric2 3 + EXPECTED + expect(subject.marshal(sample_metrics)).to eq(expected.chomp) + end + end + end +end -- cgit v1.2.1 From b668aaf4268d552315152057729f73f5c5d72147 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Tue, 30 May 2017 00:18:46 +0200 Subject: Split the metrics implementation to separate modules for Influx and Prometheus --- app/services/metrics_service.rb | 10 -- lib/gitlab/health_checks/prometheus_text_format.rb | 54 +++--- lib/gitlab/metrics.rb | 196 +-------------------- lib/gitlab/metrics/influx_db.rb | 163 +++++++++++++++++ lib/gitlab/metrics/prometheus.rb | 41 +++++ 5 files changed, 235 insertions(+), 229 deletions(-) create mode 100644 lib/gitlab/metrics/influx_db.rb create mode 100644 lib/gitlab/metrics/prometheus.rb diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb index 025598cdc76..2a001dc5108 100644 --- a/app/services/metrics_service.rb +++ b/app/services/metrics_service.rb @@ -30,14 +30,4 @@ class MetricsService def multiprocess_metrics_path @multiprocess_metrics_path ||= Rails.root.join(ENV['prometheus_multiproc_dir']).freeze end - - def metric_to_prom_line(metric) - labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || '' - - if labels.empty? - "#{metric.name} #{metric.value}" - else - "#{metric.name}{#{labels}} #{metric.value}" - end - end end diff --git a/lib/gitlab/health_checks/prometheus_text_format.rb b/lib/gitlab/health_checks/prometheus_text_format.rb index 5fc6f19c37c..e7ec1c516a2 100644 --- a/lib/gitlab/health_checks/prometheus_text_format.rb +++ b/lib/gitlab/health_checks/prometheus_text_format.rb @@ -1,38 +1,40 @@ -module Gitlab::HealthChecks - class PrometheusTextFormat - def marshal(metrics) - metrics_with_type_declarations(metrics).join("\n") - end +module Gitlab + module HealthChecks + class PrometheusTextFormat + def marshal(metrics) + metrics_with_type_declarations(metrics).join("\n") + end - private + private - def metrics_with_type_declarations(metrics) - type_declaration_added = {} + def metrics_with_type_declarations(metrics) + type_declaration_added = {} - metrics.flat_map do |metric| - metric_lines = [] + metrics.flat_map do |metric| + metric_lines = [] - unless type_declaration_added.has_key?(metric.name) - type_declaration_added[metric.name] = true - metric_lines << metric_type_declaration(metric) - end + unless type_declaration_added.has_key?(metric.name) + type_declaration_added[metric.name] = true + metric_lines << metric_type_declaration(metric) + end - metric_lines << metric_text(metric) + metric_lines << metric_text(metric) + end end - end - def metric_type_declaration(metric) - "# TYPE #{metric.name} gauge" - end + def metric_type_declaration(metric) + "# TYPE #{metric.name} gauge" + end - def metric_text(metric) - labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || '' + def metric_text(metric) + labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || '' - if labels.empty? - "#{metric.name} #{metric.value}" - else - "#{metric.name}{#{labels}} #{metric.value}" + if labels.empty? + "#{metric.name} #{metric.value}" + else + "#{metric.name}{#{labels}} #{metric.value}" + end end end end -end +end \ No newline at end of file diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 34f6b32f7da..995715417f8 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -1,200 +1,10 @@ -require 'prometheus/client' - module Gitlab module Metrics - extend Gitlab::CurrentSettings - - RAILS_ROOT = Rails.root.to_s - METRICS_ROOT = Rails.root.join('lib', 'gitlab', 'metrics').to_s - PATH_REGEX = /^#{RAILS_ROOT}\/?/ - - def self.settings - @settings ||= { - enabled: current_application_settings[:metrics_enabled], - prometheus_metrics_enabled: current_application_settings[:prometheus_metrics_enabled], - pool_size: current_application_settings[:metrics_pool_size], - timeout: current_application_settings[:metrics_timeout], - method_call_threshold: current_application_settings[:metrics_method_call_threshold], - host: current_application_settings[:metrics_host], - port: current_application_settings[:metrics_port], - sample_interval: current_application_settings[:metrics_sample_interval] || 15, - packet_size: current_application_settings[:metrics_packet_size] || 1 - } - end - - def self.prometheus_metrics_enabled? - settings[:prometheus_metrics_enabled] || false - end - - def self.influx_metrics_enabled? - settings[:enabled] || false - end + extend Gitlab::Metrics::InfluxDb + extend Gitlab::Metrics::Prometheus def self.enabled? influx_metrics_enabled? || prometheus_metrics_enabled? end - - def self.mri? - RUBY_ENGINE == 'ruby' - end - - def self.method_call_threshold - # This is memoized since this method is called for every instrumented - # method. Loading data from an external cache on every method call slows - # things down too much. - @method_call_threshold ||= settings[:method_call_threshold] - end - - def self.pool - @pool - end - - def self.registry - @registry ||= ::Prometheus::Client.registry - end - - def self.counter(name, docstring, base_labels = {}) - provide_metric(name) || registry.counter(name, docstring, base_labels) - end - - def self.summary(name, docstring, base_labels = {}) - provide_metric(name) || registry.summary(name, docstring, base_labels) - end - - def self.gauge(name, docstring, base_labels = {}) - provide_metric(name) || registry.gauge(name, docstring, base_labels) - end - - def self.histogram(name, docstring, base_labels = {}, buckets = ::Prometheus::Client::Histogram::DEFAULT_BUCKETS) - provide_metric(name) || registry.histogram(name, docstring, base_labels, buckets) - end - - def self.provide_metric(name) - if prometheus_metrics_enabled? - registry.get(name) - else - NullMetric.new - end - end - - def self.submit_metrics(metrics) - prepared = prepare_metrics(metrics) - - pool&.with do |connection| - prepared.each_slice(settings[:packet_size]) do |slice| - begin - connection.write_points(slice) - rescue StandardError - end - end - end - rescue Errno::EADDRNOTAVAIL, SocketError => ex - Gitlab::EnvironmentLogger.error('Cannot resolve InfluxDB address. GitLab Performance Monitoring will not work.') - Gitlab::EnvironmentLogger.error(ex) - end - - def self.prepare_metrics(metrics) - metrics.map do |hash| - new_hash = hash.symbolize_keys - - new_hash[:tags].each do |key, value| - if value.blank? - new_hash[:tags].delete(key) - else - new_hash[:tags][key] = escape_value(value) - end - end - - new_hash - end - end - - def self.escape_value(value) - value.to_s.gsub('=', '\\=') - end - - # Measures the execution time of a block. - # - # Example: - # - # Gitlab::Metrics.measure(:find_by_username_duration) do - # User.find_by_username(some_username) - # end - # - # name - The name of the field to store the execution time in. - # - # Returns the value yielded by the supplied block. - def self.measure(name) - trans = current_transaction - - return yield unless trans - - real_start = Time.now.to_f - cpu_start = System.cpu_time - - retval = yield - - cpu_stop = System.cpu_time - real_stop = Time.now.to_f - - real_time = (real_stop - real_start) * 1000.0 - cpu_time = cpu_stop - cpu_start - - trans.increment("#{name}_real_time", real_time) - trans.increment("#{name}_cpu_time", cpu_time) - trans.increment("#{name}_call_count", 1) - - retval - end - - # Adds a tag to the current transaction (if any) - # - # name - The name of the tag to add. - # value - The value of the tag. - def self.tag_transaction(name, value) - trans = current_transaction - - trans&.add_tag(name, value) - end - - # Sets the action of the current transaction (if any) - # - # action - The name of the action. - def self.action=(action) - trans = current_transaction - - trans&.action = action - end - - # Tracks an event. - # - # See `Gitlab::Metrics::Transaction#add_event` for more details. - def self.add_event(*args) - trans = current_transaction - - trans&.add_event(*args) - end - - # Returns the prefix to use for the name of a series. - def self.series_prefix - @series_prefix ||= Sidekiq.server? ? 'sidekiq_' : 'rails_' - end - - # Allow access from other metrics related middlewares - def self.current_transaction - Transaction.current - end - - # When enabled this should be set before being used as the usual pattern - # "@foo ||= bar" is _not_ thread-safe. - if influx_metrics_enabled? - @pool = ConnectionPool.new(size: settings[:pool_size], timeout: settings[:timeout]) do - host = settings[:host] - port = settings[:port] - - InfluxDB::Client. - new(udp: { host: host, port: port }) - end - end end -end +end \ No newline at end of file diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb new file mode 100644 index 00000000000..c5d7a2b56b9 --- /dev/null +++ b/lib/gitlab/metrics/influx_db.rb @@ -0,0 +1,163 @@ +module Gitlab + module Metrics + module InfluxDb + include Gitlab::CurrentSettings + + def influx_metrics_enabled? + settings[:enabled] || false + end + + RAILS_ROOT = Rails.root.to_s + METRICS_ROOT = Rails.root.join('lib', 'gitlab', 'metrics').to_s + PATH_REGEX = /^#{RAILS_ROOT}\/?/ + + def settings + @settings ||= { + enabled: current_application_settings[:metrics_enabled], + pool_size: current_application_settings[:metrics_pool_size], + timeout: current_application_settings[:metrics_timeout], + method_call_threshold: current_application_settings[:metrics_method_call_threshold], + host: current_application_settings[:metrics_host], + port: current_application_settings[:metrics_port], + sample_interval: current_application_settings[:metrics_sample_interval] || 15, + packet_size: current_application_settings[:metrics_packet_size] || 1 + } + end + + def mri? + RUBY_ENGINE == 'ruby' + end + + def method_call_threshold + # This is memoized since this method is called for every instrumented + # method. Loading data from an external cache on every method call slows + # things down too much. + @method_call_threshold ||= settings[:method_call_threshold] + end + + def pool + @pool + end + + def submit_metrics(metrics) + prepared = prepare_metrics(metrics) + + pool&.with do |connection| + prepared.each_slice(settings[:packet_size]) do |slice| + begin + connection.write_points(slice) + rescue StandardError + end + end + end + rescue Errno::EADDRNOTAVAIL, SocketError => ex + Gitlab::EnvironmentLogger.error('Cannot resolve InfluxDB address. GitLab Performance Monitoring will not work.') + Gitlab::EnvironmentLogger.error(ex) + end + + def prepare_metrics(metrics) + metrics.map do |hash| + new_hash = hash.symbolize_keys + + new_hash[:tags].each do |key, value| + if value.blank? + new_hash[:tags].delete(key) + else + new_hash[:tags][key] = escape_value(value) + end + end + + new_hash + end + end + + def escape_value(value) + value.to_s.gsub('=', '\\=') + end + + # Measures the execution time of a block. + # + # Example: + # + # Gitlab::Metrics.measure(:find_by_username_duration) do + # User.find_by_username(some_username) + # end + # + # name - The name of the field to store the execution time in. + # + # Returns the value yielded by the supplied block. + def measure(name) + trans = current_transaction + + return yield unless trans + + real_start = Time.now.to_f + cpu_start = System.cpu_time + + retval = yield + + cpu_stop = System.cpu_time + real_stop = Time.now.to_f + + real_time = (real_stop - real_start) * 1000.0 + cpu_time = cpu_stop - cpu_start + + trans.increment("#{name}_real_time", real_time) + trans.increment("#{name}_cpu_time", cpu_time) + trans.increment("#{name}_call_count", 1) + + retval + end + + # Adds a tag to the current transaction (if any) + # + # name - The name of the tag to add. + # value - The value of the tag. + def tag_transaction(name, value) + trans = current_transaction + + trans&.add_tag(name, value) + end + + # Sets the action of the current transaction (if any) + # + # action - The name of the action. + def action=(action) + trans = current_transaction + + trans&.action = action + end + + # Tracks an event. + # + # See `Gitlab::Metrics::Transaction#add_event` for more details. + def add_event(*args) + trans = current_transaction + + trans&.add_event(*args) + end + + # Returns the prefix to use for the name of a series. + def series_prefix + @series_prefix ||= Sidekiq.server? ? 'sidekiq_' : 'rails_' + end + + # Allow access from other metrics related middlewares + def current_transaction + Transaction.current + end + + # When enabled this should be set before being used as the usual pattern + # "@foo ||= bar" is _not_ thread-safe. + if influx_metrics_enabled? + @pool = ConnectionPool.new(size: settings[:pool_size], timeout: settings[:timeout]) do + host = settings[:host] + port = settings[:port] + + InfluxDB::Client. + new(udp: { host: host, port: port }) + end + end + end + end +end \ No newline at end of file diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb new file mode 100644 index 00000000000..493da4d779c --- /dev/null +++ b/lib/gitlab/metrics/prometheus.rb @@ -0,0 +1,41 @@ +require 'prometheus/client' + +module Gitlab + module Metrics + module Prometheus + include Gitlab::CurrentSettings + + def prometheus_metrics_enabled? + @prometheus_metrics_enabled ||= current_application_settings[:prometheus_metrics_enabled] || false + end + + def registry + @registry ||= ::Prometheus::Client.registry + end + + def counter(name, docstring, base_labels = {}) + provide_metric(name) || registry.counter(name, docstring, base_labels) + end + + def summary(name, docstring, base_labels = {}) + provide_metric(name) || registry.summary(name, docstring, base_labels) + end + + def gauge(name, docstring, base_labels = {}) + provide_metric(name) || registry.gauge(name, docstring, base_labels) + end + + def histogram(name, docstring, base_labels = {}, buckets = ::Prometheus::Client::Histogram::DEFAULT_BUCKETS) + provide_metric(name) || registry.histogram(name, docstring, base_labels, buckets) + end + + def provide_metric(name) + if prometheus_metrics_enabled? + registry.get(name) + else + NullMetric.new + end + end + end + end +end \ No newline at end of file -- cgit v1.2.1 From 68b946e3c8d3a1e7463cf8923ecd748f33c8ccee Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Thu, 1 Jun 2017 01:46:19 +0200 Subject: Fix circular dependency condition with `current_application_settings` `current_application_settings` used by `influx_metrics_enabled` executed a markdown parsing code that was measured using `Gitlab::Metrics.measure` But since the Gitlab::Metrics::InfluxDb was not yet build so Gitlab::Metrics did not yet have `measure` method. Causing the NoMethodError. However If run was successful at least once then result was cached in a file and this code never executed again. Which caused this issue to only show up in CI preparation step. --- lib/gitlab/metrics/influx_db.rb | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb index c5d7a2b56b9..3a39791edbf 100644 --- a/lib/gitlab/metrics/influx_db.rb +++ b/lib/gitlab/metrics/influx_db.rb @@ -1,7 +1,11 @@ module Gitlab module Metrics module InfluxDb - include Gitlab::CurrentSettings + extend Gitlab::CurrentSettings + extend self + + MUTEX = Mutex.new + private_constant :MUTEX def influx_metrics_enabled? settings[:enabled] || false @@ -35,10 +39,6 @@ module Gitlab @method_call_threshold ||= settings[:method_call_threshold] end - def pool - @pool - end - def submit_metrics(metrics) prepared = prepare_metrics(metrics) @@ -143,21 +143,28 @@ module Gitlab end # Allow access from other metrics related middlewares - def current_transaction + def current_transaction Transaction.current end # When enabled this should be set before being used as the usual pattern # "@foo ||= bar" is _not_ thread-safe. - if influx_metrics_enabled? - @pool = ConnectionPool.new(size: settings[:pool_size], timeout: settings[:timeout]) do - host = settings[:host] - port = settings[:port] - - InfluxDB::Client. - new(udp: { host: host, port: port }) + def pool + if influx_metrics_enabled? + if @pool.nil? + MUTEX.synchronize do + @pool ||= ConnectionPool.new(size: settings[:pool_size], timeout: settings[:timeout]) do + host = settings[:host] + port = settings[:port] + + InfluxDB::Client. + new(udp: { host: host, port: port }) + end + end + end + @pool end end end end -end \ No newline at end of file +end -- cgit v1.2.1 From 7b75004d603618d67aa50574bfc80627a6eaf72b Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Thu, 1 Jun 2017 14:38:40 +0200 Subject: Add missing trailing newlines --- lib/gitlab/health_checks/prometheus_text_format.rb | 2 +- lib/gitlab/metrics.rb | 2 +- lib/gitlab/metrics/prometheus.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/health_checks/prometheus_text_format.rb b/lib/gitlab/health_checks/prometheus_text_format.rb index e7ec1c516a2..6b81eeddfc9 100644 --- a/lib/gitlab/health_checks/prometheus_text_format.rb +++ b/lib/gitlab/health_checks/prometheus_text_format.rb @@ -37,4 +37,4 @@ module Gitlab end end end -end \ No newline at end of file +end diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb index 995715417f8..4779755bb22 100644 --- a/lib/gitlab/metrics.rb +++ b/lib/gitlab/metrics.rb @@ -7,4 +7,4 @@ module Gitlab influx_metrics_enabled? || prometheus_metrics_enabled? end end -end \ No newline at end of file +end diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb index 493da4d779c..60686509332 100644 --- a/lib/gitlab/metrics/prometheus.rb +++ b/lib/gitlab/metrics/prometheus.rb @@ -38,4 +38,4 @@ module Gitlab end end end -end \ No newline at end of file +end -- cgit v1.2.1 From e21b1501ff16de7657a4a5eccb3b8124ba07709c Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 2 Jun 2017 13:41:30 +0200 Subject: Allow enabling Prometheus metrics via ENV variable when db is seeded --- db/fixtures/production/010_settings.rb | 27 ++++++++++++++------ spec/db/production/settings_spec.rb | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 spec/db/production/settings_spec.rb diff --git a/db/fixtures/production/010_settings.rb b/db/fixtures/production/010_settings.rb index 5522f31629a..7978ceefa79 100644 --- a/db/fixtures/production/010_settings.rb +++ b/db/fixtures/production/010_settings.rb @@ -1,16 +1,29 @@ -if ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'].present? - settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults - settings.set_runners_registration_token(ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN']) - +def save(settings, topic) if settings.save - puts "Saved Runner Registration Token".color(:green) + puts "Saved #{topic}".color(:green) else - puts "Could not save Runner Registration Token".color(:red) + puts "Could not save #{topic}".color(:red) puts settings.errors.full_messages.map do |message| puts "--> #{message}".color(:red) end puts - exit 1 + exit(1) + end +end + +envs = %w{ GITLAB_PROMETHEUS_METRICS_ENABLED GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN } + +if envs.any? {|env_name| ENV[env_name].present? } + settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults + if ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'].present? + settings.set_runners_registration_token(ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN']) + save(settings, 'Runner Registration Token') + end + + if ENV['GITLAB_PROMETHEUS_METRICS_ENABLED'].present? + value = Gitlab::Utils.to_boolean(ENV['GITLAB_PROMETHEUS_METRICS_ENABLED']) + settings.prometheus_metrics_enabled = value + save(settings, 'GITLAB_PROMETHEUS_METRICS_ENABLED') end end diff --git a/spec/db/production/settings_spec.rb b/spec/db/production/settings_spec.rb new file mode 100644 index 00000000000..c6b772f4e93 --- /dev/null +++ b/spec/db/production/settings_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' +require 'rainbow/ext/string' + +describe 'seed production settings', lib: true do + include StubENV + let(:settings_file) { File.join(__dir__, '../../../db/fixtures/production/010_settings.rb') } + let(:settings) { ApplicationSetting.current || ApplicationSetting.create_from_defaults } + + context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do + before do + stub_env('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN', '013456789') + end + + it 'writes the token to the database' do + load(settings_file) + + expect(settings.runners_registration_token).to eq('013456789') + end + end + + context 'GITLAB_PROMETHEUS_METRICS_ENABLED is set in the environment' do + context 'GITLAB_PROMETHEUS_METRICS_ENABLED is true' do + before do + stub_env('GITLAB_PROMETHEUS_METRICS_ENABLED', 'true') + end + + it 'prometheus_metrics_enabled is set to true ' do + load(settings_file) + + expect(settings.prometheus_metrics_enabled).to eq(true) + end + end + + context 'GITLAB_PROMETHEUS_METRICS_ENABLED is false' do + before do + stub_env('GITLAB_PROMETHEUS_METRICS_ENABLED', 'false') + end + + it 'prometheus_metrics_enabled is set to false' do + load(settings_file) + + expect(settings.prometheus_metrics_enabled).to eq(false) + end + end + end +end -- cgit v1.2.1 From c86e1437eb415e816dcc29f0b1acafeed2dcc266 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 2 Jun 2017 14:21:58 +0200 Subject: Make fixture message more descriptive + use strip_heredoc to make the text in tests much more readable --- db/fixtures/production/010_settings.rb | 2 +- .../health_checks/prometheus_text_format_spec.rb | 25 +++++++++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/db/fixtures/production/010_settings.rb b/db/fixtures/production/010_settings.rb index 7978ceefa79..ed0718e7fa9 100644 --- a/db/fixtures/production/010_settings.rb +++ b/db/fixtures/production/010_settings.rb @@ -24,6 +24,6 @@ if envs.any? {|env_name| ENV[env_name].present? } if ENV['GITLAB_PROMETHEUS_METRICS_ENABLED'].present? value = Gitlab::Utils.to_boolean(ENV['GITLAB_PROMETHEUS_METRICS_ENABLED']) settings.prometheus_metrics_enabled = value - save(settings, 'GITLAB_PROMETHEUS_METRICS_ENABLED') + save(settings, 'Prometheus metrics enabled flag') end end diff --git a/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb b/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb index a9feab8ff78..b07f95443ee 100644 --- a/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb +++ b/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb @@ -11,12 +11,13 @@ describe Gitlab::HealthChecks::PrometheusTextFormat do end it 'marshal to text with non repeating type definition' do - expected = <<-EXPECTED -# TYPE metric1 gauge -metric1 1 -# TYPE metric2 gauge -metric2 2 -EXPECTED + expected = <<-EXPECTED.strip_heredoc + # TYPE metric1 gauge + metric1 1 + # TYPE metric2 gauge + metric2 2 + EXPECTED + expect(subject.marshal(sample_metrics)).to eq(expected.chomp) end @@ -30,12 +31,12 @@ EXPECTED end it 'marshal to text with non repeating type definition' do - expected = <<-EXPECTED -# TYPE metric1 gauge -metric1 1 -metric1 2 -# TYPE metric2 gauge -metric2 3 + expected = <<-EXPECTED.strip_heredoc + # TYPE metric1 gauge + metric1 1 + metric1 2 + # TYPE metric2 gauge + metric2 3 EXPECTED expect(subject.marshal(sample_metrics)).to eq(expected.chomp) end -- cgit v1.2.1 From 6a67148ed3543ee5073ab49dc4e825f3d87cc8b5 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 2 Jun 2017 15:25:54 +0200 Subject: Make production settings fixture use Gitlab::CurrentSettings.current_application_settings small code formatting changes --- db/fixtures/production/010_settings.rb | 23 ++++++++++------------ spec/db/production/settings_spec.rb | 4 ++-- .../health_checks/prometheus_text_format_spec.rb | 14 +++++-------- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/db/fixtures/production/010_settings.rb b/db/fixtures/production/010_settings.rb index ed0718e7fa9..a81782d16ff 100644 --- a/db/fixtures/production/010_settings.rb +++ b/db/fixtures/production/010_settings.rb @@ -12,18 +12,15 @@ def save(settings, topic) end end -envs = %w{ GITLAB_PROMETHEUS_METRICS_ENABLED GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN } - -if envs.any? {|env_name| ENV[env_name].present? } - settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults - if ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'].present? - settings.set_runners_registration_token(ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN']) - save(settings, 'Runner Registration Token') - end +if ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'].present? + settings = Gitlab::CurrentSettings.current_application_settings + settings.set_runners_registration_token(ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN']) + save(settings, 'Runner Registration Token') +end - if ENV['GITLAB_PROMETHEUS_METRICS_ENABLED'].present? - value = Gitlab::Utils.to_boolean(ENV['GITLAB_PROMETHEUS_METRICS_ENABLED']) - settings.prometheus_metrics_enabled = value - save(settings, 'Prometheus metrics enabled flag') - end +if ENV['GITLAB_PROMETHEUS_METRICS_ENABLED'].present? + settings = Gitlab::CurrentSettings.current_application_settings + value = Gitlab::Utils.to_boolean(ENV['GITLAB_PROMETHEUS_METRICS_ENABLED']) + settings.prometheus_metrics_enabled = value + save(settings, 'Prometheus metrics enabled flag') end diff --git a/spec/db/production/settings_spec.rb b/spec/db/production/settings_spec.rb index c6b772f4e93..00c631b866e 100644 --- a/spec/db/production/settings_spec.rb +++ b/spec/db/production/settings_spec.rb @@ -3,8 +3,8 @@ require 'rainbow/ext/string' describe 'seed production settings', lib: true do include StubENV - let(:settings_file) { File.join(__dir__, '../../../db/fixtures/production/010_settings.rb') } - let(:settings) { ApplicationSetting.current || ApplicationSetting.create_from_defaults } + let(:settings_file) { Rails.root.join('db/fixtures/production/010_settings.rb') } + let(:settings) { Gitlab::CurrentSettings.current_application_settings } context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do before do diff --git a/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb b/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb index b07f95443ee..7573792789a 100644 --- a/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb +++ b/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb @@ -4,10 +4,8 @@ describe Gitlab::HealthChecks::PrometheusTextFormat do describe '#marshal' do let(:sample_metrics) do - [ - metric_class.new('metric1', 1), - metric_class.new('metric2', 2) - ] + [metric_class.new('metric1', 1), + metric_class.new('metric2', 2)] end it 'marshal to text with non repeating type definition' do @@ -23,11 +21,9 @@ describe Gitlab::HealthChecks::PrometheusTextFormat do context 'metrics where name repeats' do let(:sample_metrics) do - [ - metric_class.new('metric1', 1), - metric_class.new('metric1', 2), - metric_class.new('metric2', 3) - ] + [metric_class.new('metric1', 1), + metric_class.new('metric1', 2), + metric_class.new('metric2', 3)] end it 'marshal to text with non repeating type definition' do -- cgit v1.2.1 From d26573c6e3de535f69437deaf54d5c151ac343c8 Mon Sep 17 00:00:00 2001 From: Pawel Chojnacki Date: Fri, 2 Jun 2017 15:55:44 +0200 Subject: Make PrometheusTextFormat return proper output terminated with '\n' remove file dangling after rebase --- app/services/metrics_service.rb | 2 +- lib/gitlab/health_checks/prometheus_text_format.rb | 2 +- spec/db/production/settings.rb | 16 ---------------- .../gitlab/health_checks/prometheus_text_format_spec.rb | 4 ++-- 4 files changed, 4 insertions(+), 20 deletions(-) delete mode 100644 spec/db/production/settings.rb diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb index 2a001dc5108..d726db4e99b 100644 --- a/app/services/metrics_service.rb +++ b/app/services/metrics_service.rb @@ -18,7 +18,7 @@ class MetricsService end def metrics_text - "#{health_metrics_text}\n#{prometheus_metrics_text}" + "#{health_metrics_text}#{prometheus_metrics_text}" end private diff --git a/lib/gitlab/health_checks/prometheus_text_format.rb b/lib/gitlab/health_checks/prometheus_text_format.rb index 6b81eeddfc9..462c8e736a0 100644 --- a/lib/gitlab/health_checks/prometheus_text_format.rb +++ b/lib/gitlab/health_checks/prometheus_text_format.rb @@ -2,7 +2,7 @@ module Gitlab module HealthChecks class PrometheusTextFormat def marshal(metrics) - metrics_with_type_declarations(metrics).join("\n") + "#{metrics_with_type_declarations(metrics).join("\n")}\n" end private diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb deleted file mode 100644 index 3cbb173c4cc..00000000000 --- a/spec/db/production/settings.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe 'seed production settings', lib: true do - include StubENV - - context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do - before do - stub_env('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN', '013456789') - end - - it 'writes the token to the database' do - load(File.join(__dir__, '../../../db/fixtures/production/010_settings.rb')) - expect(ApplicationSetting.current.runners_registration_token).to eq('013456789') - end - end -end diff --git a/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb b/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb index 7573792789a..ed757ed60d8 100644 --- a/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb +++ b/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::HealthChecks::PrometheusTextFormat do metric2 2 EXPECTED - expect(subject.marshal(sample_metrics)).to eq(expected.chomp) + expect(subject.marshal(sample_metrics)).to eq(expected) end context 'metrics where name repeats' do @@ -34,7 +34,7 @@ describe Gitlab::HealthChecks::PrometheusTextFormat do # TYPE metric2 gauge metric2 3 EXPECTED - expect(subject.marshal(sample_metrics)).to eq(expected.chomp) + expect(subject.marshal(sample_metrics)).to eq(expected) end end end -- cgit v1.2.1 From c88d9cf34c97a27db55f9b90b29ede5d20a1f156 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 2 Jun 2017 03:52:27 -0500 Subject: Fix NPE with horse racing emoji check Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/32587 --- app/assets/javascripts/behaviors/gl_emoji.js | 1 + .../gl_emoji/is_emoji_unicode_supported.js | 3 ++- spec/javascripts/gl_emoji_spec.js | 31 ++++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js index 23d91fdb259..36ce4fddb72 100644 --- a/app/assets/javascripts/behaviors/gl_emoji.js +++ b/app/assets/javascripts/behaviors/gl_emoji.js @@ -88,6 +88,7 @@ function installGlEmojiElement() { const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0; if ( + emojiUnicode && isEmojiUnicode && !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion) ) { diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js index 20ab2d7e827..4f8884d05ac 100644 --- a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js +++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js @@ -28,7 +28,8 @@ function isSkinToneComboEmoji(emojiUnicode) { // doesn't support the skin tone versions of horse racing const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) function isHorceRacingSkinToneComboEmoji(emojiUnicode) { - return Array.from(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && + const firstCharacter = Array.from(emojiUnicode)[0]; + return firstCharacter && firstCharacter.codePointAt(0) === horseRacingCodePoint && isSkinToneComboEmoji(emojiUnicode); } diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js index b2b46640e5b..a09e0072fa8 100644 --- a/spec/javascripts/gl_emoji_spec.js +++ b/spec/javascripts/gl_emoji_spec.js @@ -192,6 +192,9 @@ describe('gl_emoji', () => { }); describe('isFlagEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isFlagEmoji('')).toBeFalsy(); + }); it('should detect flag_ac', () => { expect(isFlagEmoji('🇦🇨')).toBeTruthy(); }); @@ -216,6 +219,9 @@ describe('gl_emoji', () => { }); describe('isKeycapEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isKeycapEmoji('')).toBeFalsy(); + }); it('should detect one(keycap)', () => { expect(isKeycapEmoji('1️⃣')).toBeTruthy(); }); @@ -231,6 +237,9 @@ describe('gl_emoji', () => { }); describe('isSkinToneComboEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isSkinToneComboEmoji('')).toBeFalsy(); + }); it('should detect hand_splayed_tone5', () => { expect(isSkinToneComboEmoji('🖐🏿')).toBeTruthy(); }); @@ -255,6 +264,9 @@ describe('gl_emoji', () => { }); describe('isHorceRacingSkinToneComboEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isHorceRacingSkinToneComboEmoji('')).toBeFalsy(); + }); it('should detect horse_racing_tone2', () => { expect(isHorceRacingSkinToneComboEmoji('🏇🏼')).toBeTruthy(); }); @@ -264,6 +276,9 @@ describe('gl_emoji', () => { }); describe('isPersonZwjEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isPersonZwjEmoji('')).toBeFalsy(); + }); it('should detect couple_mm', () => { expect(isPersonZwjEmoji('👨‍❤️‍👨')).toBeTruthy(); }); @@ -300,6 +315,22 @@ describe('gl_emoji', () => { }); describe('isEmojiUnicodeSupported', () => { + it('should gracefully handle empty string with unicode support', () => { + const isSupported = isEmojiUnicodeSupported( + { '1.0': true }, + '', + '1.0', + ); + expect(isSupported).toBeTruthy(); + }); + it('should gracefully handle empty string without unicode support', () => { + const isSupported = isEmojiUnicodeSupported( + {}, + '', + '1.0', + ); + expect(isSupported).toBeFalsy(); + }); it('bomb(6.0) with 6.0 support', () => { const emojiKey = 'bomb'; const unicodeSupportMap = Object.assign({}, emptySupportMap, { -- cgit v1.2.1 From 11b741c5f05d167788efa79411e9df2f1736aa3b Mon Sep 17 00:00:00 2001 From: Regis Date: Fri, 2 Jun 2017 17:08:34 -0600 Subject: Update CHANGELOG.md for 9.2.4 [ci skip] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1fa9e7c562..04e3f8ae6a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.2.4 (2017-06-02) + +- Fix visibility when referencing snippets. + ## 9.2.3 (2017-05-31) - Move uploads from 'public/uploads' to 'public/uploads/system'. -- cgit v1.2.1 From d83a9b42b6aa293b1333dbd17dc9aa15a57db136 Mon Sep 17 00:00:00 2001 From: Regis Date: Fri, 2 Jun 2017 17:49:09 -0600 Subject: Update CHANGELOG.md for 9.0.9 [ci skip] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04e3f8ae6a6..224b0341da3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -527,6 +527,10 @@ entry. - Only send chat notifications for the default branch. - Don't fill in the default kubernetes namespace. +## 9.0.9 (2017-06-02) + +- Fix visibility when referencing snippets. + ## 9.0.8 (2017-05-31) - Move uploads from 'public/uploads' to 'public/uploads/system'. -- cgit v1.2.1 From 78f131c0514bc99f73b9c063fac3158ab2e51e17 Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Sat, 3 Jun 2017 12:40:20 +1100 Subject: Fix duplication of commits header on commits page --- app/assets/javascripts/activities.js | 5 ++-- app/assets/javascripts/commits.js | 37 ++++++++++++++++++++++++--- app/assets/javascripts/pager.js | 5 ++-- app/views/projects/commits/_commits.html.haml | 7 +++-- changelogs/unreleased/fix_commits_page.yml | 4 +++ spec/javascripts/commits_spec.js | 26 +++++++++++++++++++ 6 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 changelogs/unreleased/fix_commits_page.yml diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index d816df831eb..5d060165f4b 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -5,7 +5,8 @@ import Cookies from 'js-cookie'; class Activities { constructor() { - Pager.init(20, true, false, this.updateTooltips); + Pager.init(20, true, false, data => data, this.updateTooltips); + $('.event-filter-link').on('click', (e) => { e.preventDefault(); this.toggleFilter(e.currentTarget); @@ -19,7 +20,7 @@ class Activities { reloadActivities() { $('.content_list').html(''); - Pager.init(20, true, false, this.updateTooltips); + Pager.init(20, true, false, data => data, this.updateTooltips); } toggleFilter(sender) { diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index e3f9eaaf39c..2b0bf49cf92 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -7,6 +7,8 @@ window.CommitsList = (function() { CommitsList.timer = null; CommitsList.init = function(limit) { + this.$contentList = $('.content_list'); + $("body").on("click", ".day-commits-table li.commit", function(e) { if (e.target.nodeName !== "A") { location.href = $(this).attr("url"); @@ -14,9 +16,9 @@ window.CommitsList = (function() { return false; } }); - Pager.init(limit, false, false, function() { - gl.utils.localTimeAgo($('.js-timeago')); - }); + + Pager.init(limit, false, false, this.processCommits); + this.content = $("#commits-list"); this.searchField = $("#commits-search"); this.lastSearch = this.searchField.val(); @@ -62,5 +64,34 @@ window.CommitsList = (function() { }); }; + // Prepare loaded data. + CommitsList.processCommits = (data) => { + let processedData = data; + const $processedData = $(processedData); + const $commitsHeadersLast = CommitsList.$contentList.find('li.js-commit-header').last(); + const lastShownDay = $commitsHeadersLast.data('day'); + const $loadedCommitsHeadersFirst = $processedData.filter('li.js-commit-header').first(); + const loadedShownDayFirst = $loadedCommitsHeadersFirst.data('day'); + let commitsCount; + + // If commits headers show the same date, + // remove the last header and change the previous one. + if (lastShownDay === loadedShownDayFirst) { + // Last shown commits count under the last commits header. + commitsCount = $commitsHeadersLast.nextUntil('li.js-commit-header').find('li.commit').length; + + // Remove duplicate of commits header. + processedData = $processedData.not(`li.js-commit-header[data-day="${loadedShownDayFirst}"]`); + + // Update commits count in the previous commits header. + commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length); + $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${gl.text.pluralize('commit', commitsCount)}`); + } + + gl.utils.localTimeAgo($processedData.find('.js-timeago')); + + return processedData; + }; + return CommitsList; })(); diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index 0ef20af9260..01110420cca 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -6,11 +6,12 @@ import '~/lib/utils/url_utility'; const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000; const Pager = { - init(limit = 0, preload = false, disable = false, callback = $.noop) { + init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) { this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']); this.limit = limit; this.offset = parseInt(gl.utils.getParameterByName('offset'), 10) || this.limit; this.disable = disable; + this.prepareData = prepareData; this.callback = callback; this.loading = $('.loading').first(); if (preload) { @@ -29,7 +30,7 @@ import '~/lib/utils/url_utility'; dataType: 'json', error: () => this.loading.hide(), success: (data) => { - this.append(data.count, data.html); + this.append(data.count, this.prepareData(data.html)); this.callback(); // keep loading until we've filled the viewport height diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 88c7d7bc44b..d3380c917e4 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -2,8 +2,11 @@ - commits, hidden = limited_commits(@commits) - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| - %li.commit-header #{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')} - %li.commits-row + %li.commit-header.js-commit-header{ data: { day: day } } + %span.day= day.strftime('%d %b, %Y') + %span.commits-count= pluralize(commits.count, 'commit') + + %li.commits-row{ data: { day: day } } %ul.content-list.commit-list = render commits, project: project, ref: ref diff --git a/changelogs/unreleased/fix_commits_page.yml b/changelogs/unreleased/fix_commits_page.yml new file mode 100644 index 00000000000..a2afaf6e626 --- /dev/null +++ b/changelogs/unreleased/fix_commits_page.yml @@ -0,0 +1,4 @@ +--- +title: Fix duplication of commits header on commits page +merge_request: 11006 +author: @blackst0ne diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index 187db7485a5..44a4386b250 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -28,6 +28,32 @@ import '~/commits'; expect(CommitsList).toBeDefined(); }); + describe('processCommits', () => { + it('should join commit headers', () => { + CommitsList.$contentList = $(` +
    +
  • + 20 Sep, 2016 + 1 commit +
  • +
  • +
    + `); + + const data = ` +
  • + 20 Sep, 2016 + 1 commit +
  • +
  • + `; + + // The last commit header should be removed + // since the previous one has the same data-day value. + expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0); + }); + }); + describe('on entering input', () => { let ajaxSpy; -- cgit v1.2.1 From fd496b8513c0b7c48870e5330a5e12b95728e61c Mon Sep 17 00:00:00 2001 From: Regis Date: Fri, 2 Jun 2017 22:14:58 -0600 Subject: Update CHANGELOG.md for 9.1.6 [ci skip] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 224b0341da3..75e354b2c25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -223,6 +223,10 @@ entry. - Fix preemptive scroll bar on user activity calendar. - Pipeline chat notifications convert seconds to minutes and hours. +## 9.1.6 (2017-06-02) + +- Fix visibility when referencing snippets. + ## 9.1.5 (2017-05-31) - Move uploads from 'public/uploads' to 'public/uploads/system'. -- cgit v1.2.1 From bd76cdc97b5883ee801149a6b37a9ad78e79d164 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 5 Jun 2017 10:18:12 +0200 Subject: Remove legacy method from pipeline class --- app/models/ci/pipeline.rb | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fb1d4720ba8..e84693bcf3a 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -285,17 +285,6 @@ module Ci end end - ## - # TODO, phase this method out - # - def config_builds_attributes - return [] unless config_processor - - config_processor. - builds_for_ref(ref, tag?, trigger_requests.first). - sort_by { |build| build[:stage_idx] } - end - def stage_seeds return [] unless config_processor -- cgit v1.2.1 From 469975c783d8c90e555b64d069e412a3b062073b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 5 Jun 2017 10:30:57 +0200 Subject: Revert invalid changes in new pipeline service specs --- spec/services/ci/create_pipeline_service_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index fe5de2ce227..68242fb4e15 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -9,10 +9,10 @@ describe Ci::CreatePipelineService, :services do end describe '#execute' do - def execute_service(after_sha: project.commit.id, message: 'Message', ref: 'refs/heads/master') + def execute_service(after: project.commit.id, message: 'Message', ref: 'refs/heads/master') params = { ref: ref, before: '00000000', - after: after_sha, + after: after, commits: [{ message: message }] } described_class.new(project, user, params).execute -- cgit v1.2.1 From 028423c2f51ff738d151df32254913664ac8e898 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 5 Jun 2017 11:07:10 +0200 Subject: Calculate previous migration version in specs support This makes it possible to test migration on the schema this migration was written for, without a need to specify a previous schema version manually. --- spec/migrations/migrate_build_stage_reference_spec.rb | 2 +- spec/migrations/migrate_pipeline_stages_spec.rb | 2 +- spec/spec_helper.rb | 6 +++--- spec/support/migrations_helpers.rb | 10 ++++++++++ 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/spec/migrations/migrate_build_stage_reference_spec.rb b/spec/migrations/migrate_build_stage_reference_spec.rb index 979f13a1398..eaac8f95892 100644 --- a/spec/migrations/migrate_build_stage_reference_spec.rb +++ b/spec/migrations/migrate_build_stage_reference_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170526185921_migrate_build_stage_reference.rb') -describe MigrateBuildStageReference, :migration, schema: 20170526185602 do +describe MigrateBuildStageReference, :migration do ## # Create test data - pipeline and CI/CD jobs. # diff --git a/spec/migrations/migrate_pipeline_stages_spec.rb b/spec/migrations/migrate_pipeline_stages_spec.rb index c9b38086deb..36d3bd13ac0 100644 --- a/spec/migrations/migrate_pipeline_stages_spec.rb +++ b/spec/migrations/migrate_pipeline_stages_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170526185842_migrate_pipeline_stages.rb') -describe MigratePipelineStages, :migration, schema: 20170526185602 do +describe MigratePipelineStages, :migration do ## # Create test data - pipeline and CI/CD jobs. # diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6a0e29f2edb..5cbb3dafcdf 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -94,10 +94,10 @@ RSpec.configure do |config| Sidekiq.redis(&:flushall) end - config.around(:example, migration: true) do |example| + config.around(:example, :migration) do |example| begin - schema_version = example.metadata.fetch(:schema) - ActiveRecord::Migrator.migrate(migrations_paths, schema_version) + ActiveRecord::Migrator + .migrate(migrations_paths, previous_migration.version) example.run ensure diff --git a/spec/support/migrations_helpers.rb b/spec/support/migrations_helpers.rb index ee17d1a40b7..91fbb4eaf48 100644 --- a/spec/support/migrations_helpers.rb +++ b/spec/support/migrations_helpers.rb @@ -11,6 +11,16 @@ module MigrationsHelpers ActiveRecord::Base.connection.table_exists?(name) end + def migrations + ActiveRecord::Migrator.migrations(migrations_paths) + end + + def previous_migration + migrations.each_cons(2) do |previous, migration| + break previous if migration.name == described_class.name + end + end + def migrate! ActiveRecord::Migrator.up(migrations_paths) do |migration| migration.name == described_class.name -- cgit v1.2.1 From 0b81b5ace0dd7c5ba3362238d8be41ce178e1ecc Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 31 May 2017 15:55:12 +0200 Subject: Create read_registry scope with JWT auth This is the first commit doing mainly 3 things: 1. create a new scope and allow users to use it 2. Have the JWTController respond correctly on this 3. Updates documentation to suggest usage of PATs There is one gotcha, there will be no support for impersonation tokens, as this seems not needed. Fixes gitlab-org/gitlab-ce#19219 --- app/controllers/jwt_controller.rb | 8 +++-- .../profiles/personal_access_tokens_controller.rb | 2 +- app/models/personal_access_token.rb | 11 +++--- changelogs/unreleased/zj-read-registry-pat.yml | 4 +++ doc/user/project/container_registry.md | 13 +++---- lib/gitlab/auth.rb | 42 ++++++++++++++-------- lib/gitlab/auth/result.rb | 4 +++ spec/lib/gitlab/auth_spec.rb | 7 ++++ spec/models/personal_access_token_spec.rb | 20 +++++++++-- spec/requests/jwt_controller_spec.rb | 15 +++++++- 10 files changed, 93 insertions(+), 33 deletions(-) create mode 100644 changelogs/unreleased/zj-read-registry-pat.yml diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 1c01be06451..2648b901f01 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -20,13 +20,15 @@ class JwtController < ApplicationController private def authenticate_project_or_user - @authentication_result = Gitlab::Auth::Result.new(nil, nil, :none, Gitlab::Auth.read_authentication_abilities) + @authentication_result = Gitlab::Auth::Result.new(nil, nil, :none, Gitlab::Auth.read_api_abilities) authenticate_with_http_basic do |login, password| @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip) - render_unauthorized unless @authentication_result.success? && - (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User)) + if @authentication_result.failed? || + (@authentication_result.actor.present? && !@authentication_result.actor.is_a?(User)) + render_unauthorized + end end rescue Gitlab::Auth::MissingPersonalTokenError render_missing_personal_token diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 0abe7ea3c9b..f748d191ef4 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -38,7 +38,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController end def set_index_vars - @scopes = Gitlab::Auth::API_SCOPES + @scopes = Gitlab::Auth::AVAILABLE_SCOPES @personal_access_token = finder.build @inactive_personal_access_tokens = finder(state: 'inactive').execute diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index e8b000ddad6..0aee696bed3 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -15,11 +15,10 @@ class PersonalAccessToken < ActiveRecord::Base scope :without_impersonation, -> { where(impersonation: false) } validates :scopes, presence: true - validate :validate_api_scopes + validate :validate_scopes def revoke! - self.revoked = true - self.save + update!(revoked: true) end def active? @@ -28,9 +27,9 @@ class PersonalAccessToken < ActiveRecord::Base protected - def validate_api_scopes - unless scopes.all? { |scope| Gitlab::Auth::API_SCOPES.include?(scope.to_sym) } - errors.add :scopes, "can only contain API scopes" + def validate_scopes + unless scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) } + errors.add :scopes, "can only contain available scopes" end end end diff --git a/changelogs/unreleased/zj-read-registry-pat.yml b/changelogs/unreleased/zj-read-registry-pat.yml new file mode 100644 index 00000000000..d36159bbdf5 --- /dev/null +++ b/changelogs/unreleased/zj-read-registry-pat.yml @@ -0,0 +1,4 @@ +--- +title: Allow pulling of container images using personal access tokens +merge_request: 11845 +author: diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index 6a2ca7fb428..b2eca9ef809 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -106,12 +106,14 @@ Make sure that your GitLab Runner is configured to allow building Docker images following the [Using Docker Build](../../ci/docker/using_docker_build.md) and [Using the GitLab Container Registry documentation](../../ci/docker/using_docker_build.md#using-the-gitlab-container-registry). -## Limitations +## Using with private projects -In order to use a container image from your private project as an `image:` in -your `.gitlab-ci.yml`, you have to follow the -[Using a private Docker Registry][private-docker] -documentation. This workflow will be simplified in the future. +If a project is private, credentials will need to be provided for authorization. +The preferred way to do this, is by using personal access tokens, which can be +created under `/profile/personal_access_tokens`. The minimal scope needed is: +`read_registry`. + +This feature was introduced in GitLab 9.3. ## Troubleshooting the GitLab Container Registry @@ -257,4 +259,3 @@ Once the right permissions were set, the error will go away. [ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040 [docker-docs]: https://docs.docker.com/engine/userguide/intro/ -[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 099c45dcfb7..f461d0f97f1 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -2,6 +2,8 @@ module Gitlab module Auth MissingPersonalTokenError = Class.new(StandardError) + REGISTRY_SCOPES = [:read_registry].freeze + # Scopes used for GitLab API access API_SCOPES = [:api, :read_user].freeze @@ -11,8 +13,10 @@ module Gitlab # Default scopes for OAuth applications that don't define their own DEFAULT_SCOPES = [:api].freeze + AVAILABLE_SCOPES = (API_SCOPES + REGISTRY_SCOPES).freeze + # Other available scopes - OPTIONAL_SCOPES = (API_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze + OPTIONAL_SCOPES = (AVAILABLE_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze class << self def find_for_git_client(login, password, project:, ip:) @@ -26,8 +30,8 @@ module Gitlab build_access_token_check(login, password) || lfs_token_check(login, password) || oauth_access_token_check(login, password) || - user_with_password_for_git(login, password) || personal_access_token_check(password) || + user_with_password_for_git(login, password) || Gitlab::Auth::Result.new rate_limit!(ip, success: result.success?, login: login) @@ -103,15 +107,16 @@ module Gitlab raise Gitlab::Auth::MissingPersonalTokenError if user.two_factor_enabled? - Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities) + Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_api_abilities) end def oauth_access_token_check(login, password) if login == "oauth2" && password.present? token = Doorkeeper::AccessToken.by_token(password) + if valid_oauth_token?(token) user = User.find_by(id: token.resource_owner_id) - Gitlab::Auth::Result.new(user, nil, :oauth, full_authentication_abilities) + Gitlab::Auth::Result.new(user, nil, :oauth, full_api_abilities) end end end @@ -121,17 +126,26 @@ module Gitlab token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password) - if token && valid_api_token?(token) - Gitlab::Auth::Result.new(token.user, nil, :personal_token, full_authentication_abilities) + if token && valid_scoped_token?(token, scopes: AVAILABLE_SCOPES.map(&:to_s)) + Gitlab::Auth::Result.new(token.user, nil, :personal_token, abilities_for_scope(token.scopes)) end end def valid_oauth_token?(token) - token && token.accessible? && valid_api_token?(token) + token && token.accessible? && valid_scoped_token?(token) end - def valid_api_token?(token) - AccessTokenValidationService.new(token).include_any_scope?(['api']) + def valid_scoped_token?(token, scopes: %w[api]) + AccessTokenValidationService.new(token).include_any_scope?(scopes) + end + + def abilities_for_scope(scopes) + abilities = Set.new + + abilities.merge(full_api_abilities) if scopes.include?("api") + abilities << :read_container_image if scopes.include?("read_registry") + + abilities.to_a end def lfs_token_check(login, password) @@ -150,9 +164,9 @@ module Gitlab authentication_abilities = if token_handler.user? - full_authentication_abilities + full_api_abilities else - read_authentication_abilities + read_api_abilities end if Devise.secure_compare(token_handler.token, password) @@ -188,7 +202,7 @@ module Gitlab ] end - def read_authentication_abilities + def read_api_abilities [ :read_project, :download_code, @@ -196,8 +210,8 @@ module Gitlab ] end - def full_authentication_abilities - read_authentication_abilities + [ + def full_api_abilities + read_api_abilities + [ :push_code, :create_container_image ] diff --git a/lib/gitlab/auth/result.rb b/lib/gitlab/auth/result.rb index 39b86c61a18..75451cf8aa9 100644 --- a/lib/gitlab/auth/result.rb +++ b/lib/gitlab/auth/result.rb @@ -15,6 +15,10 @@ module Gitlab def success? actor.present? || type == :ci end + + def failed? + !success? + end end end end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 50bc3ef1b7c..6574e6d0087 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -143,6 +143,13 @@ describe Gitlab::Auth, lib: true do expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities)) end + it 'succeeds for personal access tokens with the `read_registry` scope' do + personal_access_token = create(:personal_access_token, scopes: ['read_registry']) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image])) + end + it 'succeeds if it is an impersonation token' do impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api']) diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb index 823623d96fa..fa781195608 100644 --- a/spec/models/personal_access_token_spec.rb +++ b/spec/models/personal_access_token_spec.rb @@ -35,6 +35,16 @@ describe PersonalAccessToken, models: true do end end + describe 'revoke!' do + let(:active_personal_access_token) { create(:personal_access_token) } + + it 'revokes the token' do + active_personal_access_token.revoke! + + expect(active_personal_access_token.revoked?).to be true + end + end + context "validations" do let(:personal_access_token) { build(:personal_access_token) } @@ -51,11 +61,17 @@ describe PersonalAccessToken, models: true do expect(personal_access_token).to be_valid end - it "rejects creating a token with non-API scopes" do + it "allows creating a token with read_registry scope" do + personal_access_token.scopes = [:read_registry] + + expect(personal_access_token).to be_valid + end + + it "rejects creating a token with unavailable scopes" do personal_access_token.scopes = [:openid, :api] expect(personal_access_token).not_to be_valid - expect(personal_access_token.errors[:scopes].first).to eq "can only contain API scopes" + expect(personal_access_token.errors[:scopes].first).to eq "can only contain available scopes" end end end diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index a3e7844b2f3..8ddae9f6b89 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -41,6 +41,19 @@ describe JwtController do it { expect(response).to have_http_status(401) } end + + context 'using personal access tokens' do + let(:user) { create(:user) } + let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) } + let(:headers) { { authorization: credentials('personal_access_token', pat.token) } } + + subject! { get '/jwt/auth', parameters, headers } + + it 'authenticates correctly' do + expect(response).to have_http_status(200) + expect(service_class).to have_received(:new).with(nil, user, parameters) + end + end end context 'using User login' do @@ -89,7 +102,7 @@ describe JwtController do end it 'allows read access' do - expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_authentication_abilities) + expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_api_abilities) get '/jwt/auth', parameters end -- cgit v1.2.1 From 3801a0df80306a76dc340ca74427a124a1514dbb Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 5 Jun 2017 10:34:30 +0200 Subject: Export pipeline stages in import/export feature --- lib/gitlab/import_export/import_export.yml | 1 + lib/gitlab/import_export/relation_factory.rb | 1 + spec/lib/gitlab/import_export/all_models.yml | 7 +++++++ spec/lib/gitlab/import_export/safe_model_attributes.yml | 7 +++++++ 4 files changed, 16 insertions(+) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index d0f3cf2b514..ff2b1d08c3c 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -38,6 +38,7 @@ project_tree: - notes: - :author - :events + - :stages - :statuses - :triggers - :pipeline_schedules diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 19e23a4715f..695852526cb 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -3,6 +3,7 @@ module Gitlab class RelationFactory OVERRIDES = { snippets: :project_snippets, pipelines: 'Ci::Pipeline', + stages: 'Ci::Stage', statuses: 'commit_status', triggers: 'Ci::Trigger', pipeline_schedules: 'Ci::PipelineSchedule', diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 34f617e23a5..6e6e94d0bbb 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -91,6 +91,7 @@ merge_request_diff: pipelines: - project - user +- stages - statuses - builds - trigger_requests @@ -104,9 +105,15 @@ pipelines: - artifacts - pipeline_schedule - merge_requests +stages: +- project +- pipeline +- statuses +- builds statuses: - project - pipeline +- stage - user - auto_canceled_by variables: diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 2388aea24d9..37783f63843 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -191,6 +191,13 @@ Ci::Pipeline: - lock_version - auto_canceled_by_id - pipeline_schedule_id +Ci::Stage: +- id +- name +- project_id +- pipeline_id +- created_at +- updated_at CommitStatus: - id - project_id -- cgit v1.2.1 From 8808e7bcf518e16fa36762a9b01f6cf224233f06 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 5 Jun 2017 12:54:52 +0200 Subject: Add assertions about new pipeline stage in specs --- spec/lib/gitlab/ci/stage/seed_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/lib/gitlab/ci/stage/seed_spec.rb b/spec/lib/gitlab/ci/stage/seed_spec.rb index f4353040ce6..47a797cfe8f 100644 --- a/spec/lib/gitlab/ci/stage/seed_spec.rb +++ b/spec/lib/gitlab/ci/stage/seed_spec.rb @@ -48,6 +48,10 @@ describe Gitlab::Ci::Stage::Seed do expect(pipeline.builds).to all(satisfy { |job| job.stage_id.present? }) expect(pipeline.builds).to all(satisfy { |job| job.pipeline.present? }) expect(pipeline.builds).to all(satisfy { |job| job.project.present? }) + expect(pipeline.stages) + .to all(satisfy { |stage| stage.pipeline.present? }) + expect(pipeline.stages) + .to all(satisfy { |stage| stage.project.present? }) end end end -- cgit v1.2.1 From af72fa9c9c0af25cb7d8d349abd9dea4897aeea8 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 5 Jun 2017 13:21:49 +0200 Subject: Migrate pipeline stages only when not migrated already --- db/post_migrate/20170526185842_migrate_pipeline_stages.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/db/post_migrate/20170526185842_migrate_pipeline_stages.rb b/db/post_migrate/20170526185842_migrate_pipeline_stages.rb index 05e095f07cb..382791c4664 100644 --- a/db/post_migrate/20170526185842_migrate_pipeline_stages.rb +++ b/db/post_migrate/20170526185842_migrate_pipeline_stages.rb @@ -11,9 +11,9 @@ class MigratePipelineStages < ActiveRecord::Migration execute <<-SQL.strip_heredoc INSERT INTO ci_stages (project_id, pipeline_id, name) SELECT project_id, commit_id, stage FROM ci_builds - WHERE stage IS NOT NULL - GROUP BY project_id, commit_id, stage, stage_idx - ORDER BY stage_idx + WHERE stage IS NOT NULL AND stage_id IS NULL + GROUP BY project_id, commit_id, stage + ORDER BY MAX(stage_idx) SQL end -- cgit v1.2.1 From b72abd1d359da313b31097fcf042d2379e1c4ab2 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 5 Jun 2017 13:24:07 +0200 Subject: Migrate stage_id only it job does not have it already --- db/post_migrate/20170526185921_migrate_build_stage_reference.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/db/post_migrate/20170526185921_migrate_build_stage_reference.rb b/db/post_migrate/20170526185921_migrate_build_stage_reference.rb index 8f453b8cd82..3c30dfd6dde 100644 --- a/db/post_migrate/20170526185921_migrate_build_stage_reference.rb +++ b/db/post_migrate/20170526185921_migrate_build_stage_reference.rb @@ -10,7 +10,9 @@ class MigrateBuildStageReference < ActiveRecord::Migration 'WHERE ci_stages.pipeline_id = ci_builds.commit_id ' \ 'AND ci_stages.name = ci_builds.stage)') - update_column_in_batches(:ci_builds, :stage_id, stage_id) + update_column_in_batches(:ci_builds, :stage_id, stage_id) do |table, query| + query.where(table[:stage_id].eq(nil)) + end end def down -- cgit v1.2.1 From 06898af38f46daaa1c75cb4adead971062684875 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 5 Jun 2017 13:35:35 +0200 Subject: Create indexes on pipeline stages before migration Creates an index in ci_stages before migrating pipeline stages from ci_builds, to improve migration performance. --- .../20170526185748_create_index_in_pipeline_stages.rb | 15 +++++++++++++++ .../20170526190948_create_index_in_pipeline_stages.rb | 15 --------------- 2 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 db/post_migrate/20170526185748_create_index_in_pipeline_stages.rb delete mode 100644 db/post_migrate/20170526190948_create_index_in_pipeline_stages.rb diff --git a/db/post_migrate/20170526185748_create_index_in_pipeline_stages.rb b/db/post_migrate/20170526185748_create_index_in_pipeline_stages.rb new file mode 100644 index 00000000000..d049f87578a --- /dev/null +++ b/db/post_migrate/20170526185748_create_index_in_pipeline_stages.rb @@ -0,0 +1,15 @@ +class CreateIndexInPipelineStages < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index(:ci_stages, [:pipeline_id, :name]) + end + + def down + remove_index(:ci_stages, [:pipeline_id, :name]) + end +end diff --git a/db/post_migrate/20170526190948_create_index_in_pipeline_stages.rb b/db/post_migrate/20170526190948_create_index_in_pipeline_stages.rb deleted file mode 100644 index d049f87578a..00000000000 --- a/db/post_migrate/20170526190948_create_index_in_pipeline_stages.rb +++ /dev/null @@ -1,15 +0,0 @@ -class CreateIndexInPipelineStages < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - - disable_ddl_transaction! - - def up - add_concurrent_index(:ci_stages, [:pipeline_id, :name]) - end - - def down - remove_index(:ci_stages, [:pipeline_id, :name]) - end -end -- cgit v1.2.1 From 3326291f7e6c6b4452ca6aa4fc65faf1bd821220 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 5 Jun 2017 13:46:42 +0200 Subject: Remove obsolete stages/build before adding foreign keys --- ...0526190708_create_foreign_keys_for_pipeline_stages.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/db/post_migrate/20170526190708_create_foreign_keys_for_pipeline_stages.rb b/db/post_migrate/20170526190708_create_foreign_keys_for_pipeline_stages.rb index 40060d12d74..8a01de1fce6 100644 --- a/db/post_migrate/20170526190708_create_foreign_keys_for_pipeline_stages.rb +++ b/db/post_migrate/20170526190708_create_foreign_keys_for_pipeline_stages.rb @@ -6,6 +6,22 @@ class CreateForeignKeysForPipelineStages < ActiveRecord::Migration disable_ddl_transaction! def up + execute <<~SQL + DELETE FROM ci_stages + WHERE NOT EXISTS ( + SELECT true FROM projects + WHERE projects.id = ci_stages.project_id + ) + SQL + + execute <<~SQL + DELETE FROM ci_builds + WHERE NOT EXISTS ( + SELECT true FROM ci_stages + WHERE ci_stages.id = ci_builds.stage_id + ) + SQL + add_concurrent_foreign_key :ci_stages, :projects, column: :project_id, on_delete: :cascade add_concurrent_foreign_key :ci_builds, :ci_stages, column: :stage_id, on_delete: :cascade end -- cgit v1.2.1 From 7a2ad70f0b143e3fd195e6e5c475a7b7350c527c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 5 Jun 2017 13:48:41 +0200 Subject: Disable timeouts in foreign keys for stages migration --- .../20170526190708_create_foreign_keys_for_pipeline_stages.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/db/post_migrate/20170526190708_create_foreign_keys_for_pipeline_stages.rb b/db/post_migrate/20170526190708_create_foreign_keys_for_pipeline_stages.rb index 8a01de1fce6..5ae3c1ae098 100644 --- a/db/post_migrate/20170526190708_create_foreign_keys_for_pipeline_stages.rb +++ b/db/post_migrate/20170526190708_create_foreign_keys_for_pipeline_stages.rb @@ -6,6 +6,8 @@ class CreateForeignKeysForPipelineStages < ActiveRecord::Migration disable_ddl_transaction! def up + disable_statement_timeout + execute <<~SQL DELETE FROM ci_stages WHERE NOT EXISTS ( -- cgit v1.2.1 From faeb2f3a09d18a0041a6c425a60f26ed9a41498a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 5 Jun 2017 14:54:39 +0200 Subject: Remove stage index concurrently on migration rollback --- db/post_migrate/20170526185748_create_index_in_pipeline_stages.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/post_migrate/20170526185748_create_index_in_pipeline_stages.rb b/db/post_migrate/20170526185748_create_index_in_pipeline_stages.rb index d049f87578a..ec9ff33b6b7 100644 --- a/db/post_migrate/20170526185748_create_index_in_pipeline_stages.rb +++ b/db/post_migrate/20170526185748_create_index_in_pipeline_stages.rb @@ -10,6 +10,6 @@ class CreateIndexInPipelineStages < ActiveRecord::Migration end def down - remove_index(:ci_stages, [:pipeline_id, :name]) + remove_concurrent_index(:ci_stages, [:pipeline_id, :name]) end end -- cgit v1.2.1 From 00bc07a90f3b49231f7caac407028883aafff751 Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Mon, 5 Jun 2017 14:56:16 +0200 Subject: Should fix problem if you have an project without a project ID (Test in new_project_spec.rb) --- app/views/layouts/header/_new_dropdown.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index 9b9719a26dd..b220b40d6e3 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -18,7 +18,7 @@ %li.divider %li.dropdown-bold-header GitLab - - if @project && @project.namespace + - if @project && @project.namespace && :project_id - create_project_issue = can?(current_user, :create_issue, @project) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - create_project_snippet = can?(current_user, :create_project_snippet, @project) -- cgit v1.2.1 From cafb1bfea4dbc7b6aad47580e76d68b8075d177f Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 5 Jun 2017 14:56:53 +0200 Subject: Use the latest migration version as a schema version --- db/schema.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 47484e6b8fe..23c3ecdd517 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: 20170526190948) do +ActiveRecord::Schema.define(version: 20170526190708) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" -- cgit v1.2.1 From 25b26c2f2844178d66a6fde7f728a5e72841e9d2 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 5 Jun 2017 15:01:15 +0200 Subject: Fix typo in import/export safe model attributes --- spec/lib/gitlab/import_export/safe_model_attributes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 24665645277..34457bf36fe 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -199,7 +199,7 @@ Ci::Stage: - pipeline_id - created_at - updated_at -ommitStatus: +CommitStatus: - id - project_id - status -- cgit v1.2.1 From da0852e08aa07354706be1e0be9251ccf02e85be Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 5 Jun 2017 15:23:09 +0200 Subject: Improve specs for pipeline and pipeline seeds --- spec/lib/gitlab/ci/stage/seed_spec.rb | 2 +- spec/models/ci/pipeline_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/lib/gitlab/ci/stage/seed_spec.rb b/spec/lib/gitlab/ci/stage/seed_spec.rb index 47a797cfe8f..d7e91a5a62c 100644 --- a/spec/lib/gitlab/ci/stage/seed_spec.rb +++ b/spec/lib/gitlab/ci/stage/seed_spec.rb @@ -30,7 +30,7 @@ describe Gitlab::Ci::Stage::Seed do end describe '#user=' do - let(:user) { create(:user) } + let(:user) { build(:user) } it 'assignes relevant pipeline attributes' do subject.user = user diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index a4392ed073a..b50c7700bd3 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -535,9 +535,9 @@ describe Ci::Pipeline, models: true do end end - describe '#has_stage_seedss?' do + describe '#has_stage_seeds?' do context 'when pipeline has stage seeds' do - subject { create(:ci_pipeline_with_one_job) } + subject { build(:ci_pipeline_with_one_job) } it { is_expected.to have_stage_seeds } end -- cgit v1.2.1 From 8e9084df220d13d2974d910711dfc7617590a43f Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Fri, 26 May 2017 12:07:09 +0200 Subject: Added Dropdown + Options --- app/views/layouts/header/_default.html.haml | 46 ++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 9db98451f1d..00d143f4fb0 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -36,10 +36,48 @@ %li = link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('wrench fw') - - if current_user.can_create_project? - %li - = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('plus fw') + %li.header-new.dropdown + = link_to new_project_path, class: "header-new-dropdown-toggle", aria: { label: "New…" } , data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do + = icon('plus fw') + = icon('caret-down') + .dropdown-menu-nav.dropdown-menu-align-right + %ul + + - if @group && (can?(current_user, :create_projects, @group) || can?(current_user, :create_subgroup, @group)) + %li + .bold This group + - if can?(current_user, :create_projects, @group) + %li + = link_to 'New project', new_project_path(namespace_id: @group.id), aria: { label: "New project" } + - if can?(current_user, :create_subgroup, @group) + %li + = link_to 'New subgroup', new_group_path(parent_id: @group.id), aria: { label: "New subgroup" } + - if @project + %li + .bold This project + %li + = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), aria: { label: "New issue" } + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - if merge_project + %li + = link_to 'New merge request', new_namespace_project_merge_request_path(merge_project.namespace, merge_project), aria: { label: "New merge request" } + - if can?(current_user, :create_project_snippet, @project) + %li + = link_to 'New snippet', new_namespace_project_snippet_path(@project.namespace, @project), aria: { label: "New snippet" } + - if @group || @project + %li.divider + %li + .bold GitLab + - if current_user.can_create_project? + %li + = link_to 'New project', new_project_path, aria: { label: "New project" } + - if current_user.can_create_group? + %li + = link_to 'New group', new_group_path, aria: { label: "New group" } + %li + = link_to 'New snippet', new_snippet_path, aria: { label: "New snippet" } + + - if Gitlab::Sherlock.enabled? %li = link_to sherlock_transactions_path, title: 'Sherlock Transactions', -- cgit v1.2.1 From 6ef57fb95ba2a74de134bb5a3ba86d226b1623e6 Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Fri, 26 May 2017 12:58:03 +0200 Subject: Enabled Tooltips --- app/views/layouts/header/_default.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 00d143f4fb0..88fff245356 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -37,7 +37,7 @@ = link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('wrench fw') %li.header-new.dropdown - = link_to new_project_path, class: "header-new-dropdown-toggle", aria: { label: "New…" } , data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do + = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do = icon('plus fw') = icon('caret-down') .dropdown-menu-nav.dropdown-menu-align-right -- cgit v1.2.1 From 0b99806be1afbd6afa4ba0fb8a25c2715b9358c6 Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Fri, 26 May 2017 17:59:45 +0200 Subject: Seperated Dropdown in new partial Fixed different issues from discussion Fixed a Spinach Test which had now 2 New buttons --- app/views/layouts/header/_default.html.haml | 49 +++-------------------------- app/views/layouts/header/_new_dropdown.haml | 42 +++++++++++++++++++++++++ features/steps/project/fork.rb | 4 ++- 3 files changed, 49 insertions(+), 46 deletions(-) create mode 100644 app/views/layouts/header/_new_dropdown.haml diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 88fff245356..249253f4906 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -36,48 +36,7 @@ %li = link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('wrench fw') - %li.header-new.dropdown - = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do - = icon('plus fw') - = icon('caret-down') - .dropdown-menu-nav.dropdown-menu-align-right - %ul - - - if @group && (can?(current_user, :create_projects, @group) || can?(current_user, :create_subgroup, @group)) - %li - .bold This group - - if can?(current_user, :create_projects, @group) - %li - = link_to 'New project', new_project_path(namespace_id: @group.id), aria: { label: "New project" } - - if can?(current_user, :create_subgroup, @group) - %li - = link_to 'New subgroup', new_group_path(parent_id: @group.id), aria: { label: "New subgroup" } - - if @project - %li - .bold This project - %li - = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), aria: { label: "New issue" } - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - - if merge_project - %li - = link_to 'New merge request', new_namespace_project_merge_request_path(merge_project.namespace, merge_project), aria: { label: "New merge request" } - - if can?(current_user, :create_project_snippet, @project) - %li - = link_to 'New snippet', new_namespace_project_snippet_path(@project.namespace, @project), aria: { label: "New snippet" } - - if @group || @project - %li.divider - %li - .bold GitLab - - if current_user.can_create_project? - %li - = link_to 'New project', new_project_path, aria: { label: "New project" } - - if current_user.can_create_group? - %li - = link_to 'New group', new_group_path, aria: { label: "New group" } - %li - = link_to 'New snippet', new_snippet_path, aria: { label: "New snippet" } - - + = render 'layouts/header/new_dropdown' - if Gitlab::Sherlock.enabled? %li = link_to sherlock_transactions_path, title: 'Sherlock Transactions', @@ -112,12 +71,12 @@ @#{current_user.username} %li.divider %li - = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username } + = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } %li - = link_to "Settings", profile_path, aria: { label: "Settings" } + = link_to "Settings", profile_path %li.divider %li - = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", aria: { label: "Sign out" } + = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link" - else %li %div diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml new file mode 100644 index 00000000000..931c02b7268 --- /dev/null +++ b/app/views/layouts/header/_new_dropdown.haml @@ -0,0 +1,42 @@ +%li.header-new.dropdown + = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do + = icon('plus fw') + = icon('caret-down') + .dropdown-menu-nav.dropdown-menu-align-right + %ul + - create_group_project = can?(current_user, :create_projects, @group) + - create_group_subgroup = can?(current_user, :create_subgroup, @group) + - if @group && (create_group_project || create_group_subgroup) + %li + .bold This group + - if create_group_project + %li + = link_to 'New project', new_project_path(namespace_id: @group.id) + - if create_group_subgroup + %li + = link_to 'New subgroup', new_group_path(parent_id: @group.id) + + - if @project + %li + .bold This project + %li + = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project) + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - if merge_project + %li + = link_to 'New merge request', new_namespace_project_merge_request_path(merge_project.namespace, merge_project) + - if can?(current_user, :create_project_snippet, @project) + %li + = link_to 'New snippet', new_namespace_project_snippet_path(@project.namespace, @project) + - if @group || @project + %li.divider + %li + .bold GitLab + - if current_user.can_create_project? + %li + = link_to 'New project', new_project_path + - if current_user.can_create_group? + %li + = link_to 'New group', new_group_path + %li + = link_to 'New snippet', new_snippet_path diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 7591e7d5612..7297cb85bf0 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -42,7 +42,9 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I click link "New merge request"' do - page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') + page.within '#content-body' do + page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') + end end step 'I should see the new merge request page for my namespace' do -- cgit v1.2.1 From e84ea7adcce95123faac13594523f96523d9f1fb Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Mon, 29 May 2017 10:24:53 +0200 Subject: Externalised Dropdown Checks for creating new issue Styling of .dropdown-bold-header Fixed Spinach Tests to limit them to the main content area for clicking 'New Project' etc. so that they don't click the dropdown menu --- app/assets/stylesheets/framework/dropdowns.scss | 8 +++- app/views/layouts/header/_new_dropdown.haml | 55 +++++++++++++------------ features/steps/dashboard/new_project.rb | 2 +- features/steps/project/create.rb | 4 +- features/steps/project/forked_merge_requests.rb | 4 +- features/steps/project/issues/issues.rb | 4 +- 6 files changed, 46 insertions(+), 31 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 5ab48b6c874..9613006d021 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -261,7 +261,13 @@ text-transform: capitalize; } - .separator + .dropdown-header { + .dropdown-bold-header { + font-weight: 600; + line-height: 22px; + padding: 0 16px; + } + + .separator + .dropdown-header, .separator + .dropdown-bold-header { padding-top: 2px; } diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index 931c02b7268..e9d45802bac 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -3,35 +3,38 @@ = icon('plus fw') = icon('caret-down') .dropdown-menu-nav.dropdown-menu-align-right - %ul - - create_group_project = can?(current_user, :create_projects, @group) - - create_group_subgroup = can?(current_user, :create_subgroup, @group) - - if @group && (create_group_project || create_group_subgroup) - %li - .bold This group - - if create_group_project - %li - = link_to 'New project', new_project_path(namespace_id: @group.id) - - if create_group_subgroup - %li - = link_to 'New subgroup', new_group_path(parent_id: @group.id) + %ul + - if @group + - create_group_project = can?(current_user, :create_projects, @group) + - create_group_subgroup = can?(current_user, :create_subgroup, @group) + - if (create_group_project || create_group_subgroup) + %li.dropdown-bold-header This group + - if create_group_project + %li + = link_to 'New project', new_project_path(namespace_id: @group.id) + - if create_group_subgroup + %li + = link_to 'New subgroup', new_group_path(parent_id: @group.id) + %li.divider + %li.dropdown-bold-header GitLab - if @project - %li - .bold This project - %li - = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project) + - create_project_issue = can?(current_user, :create_issue, @project) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - - if merge_project - %li - = link_to 'New merge request', new_namespace_project_merge_request_path(merge_project.namespace, merge_project) - - if can?(current_user, :create_project_snippet, @project) - %li - = link_to 'New snippet', new_namespace_project_snippet_path(@project.namespace, @project) - - if @group || @project - %li.divider - %li - .bold GitLab + - create_project_snippet = can?(current_user, :create_project_snippet, @project) + - if (create_project_issue || create_project_mr || create_project_snippet) + %li.dropdown-bold-header This project + - if create_project_issue + %li + = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project) + - if merge_project + %li + = link_to 'New merge request', new_namespace_project_merge_request_path(merge_project.namespace, merge_project) + - if create_project_snippet + %li + = link_to 'New snippet', new_namespace_project_snippet_path(@project.namespace, @project) + %li.divider + %li.dropdown-bold-header GitLab - if current_user.can_create_project? %li = link_to 'New project', new_project_path diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb index 4fb16d3bb57..766aa9b0468 100644 --- a/features/steps/dashboard/new_project.rb +++ b/features/steps/dashboard/new_project.rb @@ -4,7 +4,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps include SharedProject step 'I click "New project" link' do - page.within('.content') do + page.within '#content-body' do click_link "New project" end end diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb index 5f5f806df36..28be9c6df5b 100644 --- a/features/steps/project/create.rb +++ b/features/steps/project/create.rb @@ -5,7 +5,9 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps step 'fill project form with valid data' do fill_in 'project_path', with: 'Empty' - click_button "Create project" + page.within '#content-body' do + click_button "Create project" + end end step 'I should see project page' do diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 25514eb9ef2..2d9d3efd9d4 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -17,7 +17,9 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I click link "New Merge Request"' do - page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') + page.within '#content-body' do + page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') + end end step 'I should see merge request "Merge Request On Forked Project"' do diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 637e6568267..b376c5049c1 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -62,7 +62,9 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I click link "New issue"' do - page.has_link?('New Issue') ? click_link('New Issue') : click_link('New issue') + page.within '#content-body' do + page.has_link?('New Issue') ? click_link('New Issue') : click_link('New issue') + end end step 'I click "author" dropdown' do -- cgit v1.2.1 From fbe0186a14732d2a8165085d28ccffe487e65bce Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Mon, 29 May 2017 11:28:20 +0200 Subject: Check for Merge Request fixed Fixed couple of spinach test --- app/assets/stylesheets/framework/dropdowns.scss | 3 ++- app/views/layouts/header/_new_dropdown.haml | 2 +- features/steps/project/merge_requests.rb | 4 +++- features/steps/project/snippets.rb | 4 +++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 9613006d021..2ee95483040 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -267,7 +267,8 @@ padding: 0 16px; } - .separator + .dropdown-header, .separator + .dropdown-bold-header { + .separator + .dropdown-header, + .separator + .dropdown-bold-header { padding-top: 2px; } diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index e9d45802bac..ac8c9070f7d 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -22,7 +22,7 @@ - create_project_issue = can?(current_user, :create_issue, @project) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - create_project_snippet = can?(current_user, :create_project_snippet, @project) - - if (create_project_issue || create_project_mr || create_project_snippet) + - if (create_project_issue || merge_project || create_project_snippet) %li.dropdown-bold-header This project - if create_project_issue %li diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 54b6352c952..17d190711f5 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -14,7 +14,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I click link "New Merge Request"' do - page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') + page.within '#content-body' do + page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') + end end step 'I click link "Bug NS-04"' do diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index e3f5e9e3ef3..dd49701a3d9 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -23,7 +23,9 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps end step 'I click link "New snippet"' do - first(:link, "New snippet").click + page.within '#content-body' do + first(:link, "New snippet").click + end end step 'I click link "Snippet one"' do -- cgit v1.2.1 From c9d46fa8388118b969291bbd6830d14ecdf71ee5 Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Wed, 31 May 2017 17:23:48 +0200 Subject: Fixing more broken unscoped tests --- app/views/layouts/header/_new_dropdown.haml | 6 +++--- spec/features/admin/admin_groups_spec.rb | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index ac8c9070f7d..9b9719a26dd 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -3,7 +3,7 @@ = icon('plus fw') = icon('caret-down') .dropdown-menu-nav.dropdown-menu-align-right - %ul + %ul - if @group - create_group_project = can?(current_user, :create_projects, @group) - create_group_subgroup = can?(current_user, :create_subgroup, @group) @@ -18,13 +18,13 @@ %li.divider %li.dropdown-bold-header GitLab - - if @project + - if @project && @project.namespace - create_project_issue = can?(current_user, :create_issue, @project) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - create_project_snippet = can?(current_user, :create_project_snippet, @project) - if (create_project_issue || merge_project || create_project_snippet) %li.dropdown-bold-header This project - - if create_project_issue + - if create_project_issue %li = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project) - if merge_project diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index d5f595894d6..cf9d7bca255 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -24,7 +24,9 @@ feature 'Admin Groups', feature: true do it 'creates new group' do visit admin_groups_path - click_link "New group" + page.within '#content-body' do + click_link "New group" + end path_component = 'gitlab' group_name = 'GitLab group name' group_description = 'Description of group for GitLab' -- cgit v1.2.1 From 9ea6cc189518e05336ff84c9157bd04e4d1c4a0f Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Wed, 31 May 2017 23:02:56 +0200 Subject: Fix for linting problem --- app/assets/stylesheets/framework/dropdowns.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 2ee95483040..6cb11ea4ccf 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -267,7 +267,7 @@ padding: 0 16px; } - .separator + .dropdown-header, + .separator + .dropdown-header, .separator + .dropdown-bold-header { padding-top: 2px; } -- cgit v1.2.1 From adc13a03cc6dc892d087424dab89f835dbd446ed Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Mon, 5 Jun 2017 14:56:16 +0200 Subject: Should fix problem if you have an project without a project ID (Test in new_project_spec.rb) --- app/views/layouts/header/_new_dropdown.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index 9b9719a26dd..b220b40d6e3 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -18,7 +18,7 @@ %li.divider %li.dropdown-bold-header GitLab - - if @project && @project.namespace + - if @project && @project.namespace && :project_id - create_project_issue = can?(current_user, :create_issue, @project) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - create_project_snippet = can?(current_user, :create_project_snippet, @project) -- cgit v1.2.1 From ee828f2947e69d37a4c239f83bf49b96df59ecf1 Mon Sep 17 00:00:00 2001 From: ernstvn Date: Mon, 5 Jun 2017 14:20:33 -0700 Subject: GitLab GEO also does not support mysql replication --- doc/install/requirements.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 5338ccb9d3a..c25776c0adb 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -129,7 +129,8 @@ We _highly_ recommend the use of PostgreSQL instead of MySQL/MariaDB as not all features of GitLab may work with MySQL/MariaDB. For example, MySQL does not have the right features to support nested groups in an efficient manner; see for more information -about this. Existing users using GitLab with MySQL/MariaDB are advised to +about this. In GitLab Geo, MySQL replication is also [not supported](https://docs.gitlab.com/ee/gitlab-geo/database.html#mysql-replication). +Existing users using GitLab with MySQL/MariaDB are advised to migrate to PostgreSQL instead. The server running the database should have _at least_ 5-10 GB of storage -- cgit v1.2.1 From 3fd02428d42546bbfc753130e1bc7d1c1f0fa83a Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 5 Jun 2017 20:37:52 -0300 Subject: Refactor PostReceive worker to limit merge conflicts --- app/workers/post_receive.rb | 38 +++++++++++++++++--------------------- spec/workers/post_receive_spec.rb | 29 +++++++++++++---------------- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index c29571d3c62..89286595ca6 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -17,34 +17,18 @@ class PostReceive post_received = Gitlab::GitPostReceive.new(project, identifier, changes) if is_wiki - # Nothing defined here yet. + process_wiki_changes(post_received) else process_project_changes(post_received) - process_repository_update(post_received) end end - def process_repository_update(post_received) + private + + def process_project_changes(post_received) changes = [] refs = Set.new - post_received.changes_refs do |oldrev, newrev, ref| - @user ||= post_received.identify(newrev) - - unless @user - log("Triggered hook for non-existing user \"#{post_received.identifier}\"") - return false - end - - changes << Gitlab::DataBuilder::Repository.single_change(oldrev, newrev, ref) - refs << ref - end - - hook_data = Gitlab::DataBuilder::Repository.update(post_received.project, @user, changes, refs.to_a) - SystemHooksService.new.execute_hooks(hook_data, :repository_update_hooks) - end - - def process_project_changes(post_received) post_received.changes_refs do |oldrev, newrev, ref| @user ||= post_received.identify(newrev) @@ -58,10 +42,22 @@ class PostReceive elsif Gitlab::Git.branch_ref?(ref) GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute end + + changes << Gitlab::DataBuilder::Repository.single_change(oldrev, newrev, ref) + refs << ref end + + after_project_changes_hooks(post_received, @user, refs.to_a, changes) end - private + def after_project_changes_hooks(post_received, user, refs, changes) + hook_data = Gitlab::DataBuilder::Repository.update(post_received.project, user, changes, refs) + SystemHooksService.new.execute_hooks(hook_data, :repository_update_hooks) + end + + def process_wiki_changes(post_received) + # Nothing defined here yet. + end # To maintain backwards compatibility, we accept both gl_repository or # repository paths as project identifiers. Our plan is to migrate to diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index f4bc63bcc6a..44163c735ba 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -94,26 +94,23 @@ describe PostReceive do it { expect{ subject }.not_to change{ Ci::Pipeline.count } } end end - end - describe '#process_repository_update' do - let(:changes) {'123456 789012 refs/heads/tést'} - let(:fake_hook_data) do - { event_name: 'repository_update' } - end + context 'after project changes hooks' do + let(:changes) { '123456 789012 refs/heads/tést' } + let(:fake_hook_data) { Hash.new(event_name: 'repository_update') } - before do - allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) - allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data) - # silence hooks so we can isolate - allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true) - allow(subject).to receive(:process_project_changes).and_return(true) - end + before do + allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data) + # silence hooks so we can isolate + allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true) + allow_any_instance_of(GitPushService).to receive(:execute).and_return(true) + end - it 'calls SystemHooksService' do - expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) + it 'calls SystemHooksService' do + expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) - subject.perform(pwd(project), key_id, base64_changes) + described_class.new.perform(project_identifier, key_id, base64_changes) + end end end -- cgit v1.2.1 From 63746d2d22b79f0d0a53d7ad37121440308f0c57 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Jun 2017 20:06:38 -0500 Subject: Perform filtered search when state tab is changed --- .../filtered_search/filtered_search_manager.js | 49 +++++++++++++++++++++- app/assets/stylesheets/framework/nav.scss | 12 +++++- app/views/shared/issuable/_nav.html.haml | 19 ++++----- .../unreleased/auto-search-when-state-changed.yml | 4 ++ 4 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 changelogs/unreleased/auto-search-when-state-changed.yml diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 3be889c684b..fbb1a08089f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -77,6 +77,47 @@ class FilteredSearchManager { } } + bindStateEvents() { + const stateFilters = document.querySelector('.container-fluid .issues-state-filters'); + + if (stateFilters) { + this.searchStateOpened = this.search.bind(this, 'opened'); + this.searchStateMerged = this.search.bind(this, 'merged'); + this.searchStateClosed = this.search.bind(this, 'closed'); + this.searchStateAll = this.search.bind(this, 'all'); + + stateFilters.querySelector('.state-opened') + .addEventListener('click', this.searchStateOpened); + stateFilters.querySelector('.state-closed') + .addEventListener('click', this.searchStateClosed); + stateFilters.querySelector('.state-all') + .addEventListener('click', this.searchStateAll); + + const mergedState = stateFilters.querySelector('.state-merged'); + if (mergedState) { + mergedState.addEventListener('click', this.searchStateMerged); + } + } + } + + unbindStateEvents() { + const stateFilters = document.querySelector('.container-fluid .issues-state-filters'); + + if (stateFilters) { + stateFilters.querySelector('.state-opened') + .removeEventListener('click', this.searchStateOpened); + stateFilters.querySelector('.state-closed') + .removeEventListener('click', this.searchStateClosed); + stateFilters.querySelector('.state-all') + .removeEventListener('click', this.searchStateAll); + + const mergedState = stateFilters.querySelector('.state-merged'); + if (mergedState) { + mergedState.removeEventListener('click', this.searchStateMerged); + } + } + } + bindEvents() { this.handleFormSubmit = this.handleFormSubmit.bind(this); this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); @@ -114,6 +155,8 @@ class FilteredSearchManager { document.addEventListener('click', this.removeInputContainerFocusWrapper); document.addEventListener('keydown', this.removeSelectedTokenKeydownWrapper); eventHub.$on('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); + + this.bindStateEvents(); } unbindEvents() { @@ -136,6 +179,8 @@ class FilteredSearchManager { document.removeEventListener('click', this.removeInputContainerFocusWrapper); document.removeEventListener('keydown', this.removeSelectedTokenKeydownWrapper); eventHub.$off('recentSearchesItemSelected', this.onrecentSearchesItemSelectedWrapper); + + this.unbindStateEvents(); } checkForBackspace(e) { @@ -459,7 +504,7 @@ class FilteredSearchManager { } } - search() { + search(state = null) { const paths = []; const searchQuery = gl.DropdownUtils.getSearchQuery(); @@ -467,7 +512,7 @@ class FilteredSearchManager { const { tokens, searchToken } = this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys()); - const currentState = gl.utils.getParameterByName('state') || 'opened'; + const currentState = state || gl.utils.getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); tokens.forEach((token) => { diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 28b2a7cfacd..f64d9a4cabc 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -45,7 +45,8 @@ li { display: flex; - a { + a, + .div-btn { padding: $gl-btn-padding; padding-bottom: 11px; font-size: 14px; @@ -67,7 +68,14 @@ } } - &.active a { + .div-btn { + padding-top: 16px; + padding-left: 15px; + padding-right: 15px; + } + + &.active a, + &.active .div-btn { border-bottom: 2px solid $link-underline-blue; color: $black; font-weight: 600; diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index ad995cbe962..adccfa53a59 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -1,25 +1,24 @@ - type = local_assigns.fetch(:type, :issues) - page_context_word = type.to_s.humanize(capitalize: false) - issuables = @issues || @merge_requests +- closed_title = 'Filter by issues that are currently closed.' %ul.nav-links.issues-state-filters %li{ class: active_when(params[:state] == 'opened') }> - = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do + %div.div-btn.state-opened{ id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", role: 'button' } #{issuables_state_counter_text(type, :opened)} - if type == :merge_requests %li{ class: active_when(params[:state] == 'merged') }> - = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do + %div.div-btn.state-merged{ id: 'state-merged', title: 'Filter by merge requests that are currently merged.', role: 'button' } #{issuables_state_counter_text(type, :merged)} - %li{ class: active_when(params[:state] == 'closed') }> - = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do - #{issuables_state_counter_text(type, :closed)} - - else - %li{ class: active_when(params[:state] == 'closed') }> - = link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do - #{issuables_state_counter_text(type, :closed)} + - closed_title = 'Filter by merge requests that are currently closed and unmerged.' + + %li{ class: active_when(params[:state] == 'closed') }> + %div.div-btn.state-closed{ id: 'state-closed', title: closed_title, role: 'button' } + #{issuables_state_counter_text(type, :closed)} %li{ class: active_when(params[:state] == 'all') }> - = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do + %div.div-btn.state-all{ id: 'state-all', title: "Show all #{page_context_word}.", role: 'button' } #{issuables_state_counter_text(type, :all)} diff --git a/changelogs/unreleased/auto-search-when-state-changed.yml b/changelogs/unreleased/auto-search-when-state-changed.yml new file mode 100644 index 00000000000..2723beb8600 --- /dev/null +++ b/changelogs/unreleased/auto-search-when-state-changed.yml @@ -0,0 +1,4 @@ +--- +title: Perform filtered search when state tab is changed +merge_request: +author: -- cgit v1.2.1 From d14acc3fd503c7156f6c0e50f3c39a5c552fd6d4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Jun 2017 20:49:30 -0500 Subject: Fix haml lint --- app/views/shared/issuable/_nav.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index adccfa53a59..cacce91280e 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -5,20 +5,20 @@ %ul.nav-links.issues-state-filters %li{ class: active_when(params[:state] == 'opened') }> - %div.div-btn.state-opened{ id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", role: 'button' } + .div-btn.state-opened{ id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", role: 'button' } #{issuables_state_counter_text(type, :opened)} - if type == :merge_requests %li{ class: active_when(params[:state] == 'merged') }> - %div.div-btn.state-merged{ id: 'state-merged', title: 'Filter by merge requests that are currently merged.', role: 'button' } + .div-btn.state-merged{ id: 'state-merged', title: 'Filter by merge requests that are currently merged.', role: 'button' } #{issuables_state_counter_text(type, :merged)} - closed_title = 'Filter by merge requests that are currently closed and unmerged.' %li{ class: active_when(params[:state] == 'closed') }> - %div.div-btn.state-closed{ id: 'state-closed', title: closed_title, role: 'button' } + .div-btn.state-closed{ id: 'state-closed', title: closed_title, role: 'button' } #{issuables_state_counter_text(type, :closed)} %li{ class: active_when(params[:state] == 'all') }> - %div.div-btn.state-all{ id: 'state-all', title: "Show all #{page_context_word}.", role: 'button' } + .div-btn.state-all{ id: 'state-all', title: "Show all #{page_context_word}.", role: 'button' } #{issuables_state_counter_text(type, :all)} -- cgit v1.2.1 From 9512419d730588ccd144bd2c756b1142159b08e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Tue, 6 Jun 2017 11:38:28 +0800 Subject: add portuguese brazil translation of cycle analytics page Fix #33334 --- app/assets/javascripts/locale/pt_BR/app.js | 1 + lib/gitlab/i18n.rb | 1 + locale/pt_BR/gitlab.po | 260 +++++++++++++++++++++++++++++ locale/pt_BR/gitlab.po.time_stamp | 0 4 files changed, 262 insertions(+) create mode 100644 app/assets/javascripts/locale/pt_BR/app.js create mode 100644 locale/pt_BR/gitlab.po create mode 100644 locale/pt_BR/gitlab.po.time_stamp diff --git a/app/assets/javascripts/locale/pt_BR/app.js b/app/assets/javascripts/locale/pt_BR/app.js new file mode 100644 index 00000000000..f2eed3da064 --- /dev/null +++ b/app/assets/javascripts/locale/pt_BR/app.js @@ -0,0 +1 @@ +var locales = locales || {}; locales['pt_BR'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-05 03:29-0400","Last-Translator":"Alexandre Alencar ","Language-Team":"Portuguese (Brazil)","Language":"pt-BR","X-Generator":"Zanata 3.9.6","Plural-Forms":"nplurals=2; plural=(n != 1)","lang":"pt_BR","domain":"app","plural_forms":"nplurals=2; plural=(n != 1)"},"ByAuthor|by":["por"],"Commit":["Commit","Commits"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["A Análise de Ciclo fornece uma visão geral de quanto tempo uma ideia demora para ir para produção em seu projeto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Tarefa"],"CycleAnalyticsStage|Plan":["Plano"],"CycleAnalyticsStage|Production":["Produção"],"CycleAnalyticsStage|Review":["Revisão"],"CycleAnalyticsStage|Staging":["Homologação"],"CycleAnalyticsStage|Test":["Teste"],"Deploy":["Implantação","Implantações"],"FirstPushedBy|First":["Primeiro"],"FirstPushedBy|pushed by":["publicado por"],"From issue creation until deploy to production":["Da criação de tarefas até a implantação para a produção"],"From merge request merge until deploy to production":["Da incorporação do merge request até a implantação em produção"],"Introducing Cycle Analytics":["Apresentando a Análise de Ciclo"],"Last %d day":["Último %d dia","Últimos %d dias"],"Limited to showing %d event at most":["Limitado a mostrar %d evento no máximo","Limitado a mostrar %d eventos no máximo"],"Median":["Mediana"],"New Issue":["Nova Tarefa","Novas Tarefas"],"Not available":["Não disponível"],"Not enough data":["Dados insuficientes"],"OpenedNDaysAgo|Opened":["Aberto"],"Pipeline Health":["Saúde da Pipeline"],"ProjectLifecycle|Stage":["Etapa"],"Read more":["Ler mais"],"Related Commits":["Commits Relacionados"],"Related Deployed Jobs":["Jobs Relacionados Incorporados"],"Related Issues":["Tarefas Relacionadas"],"Related Jobs":["Jobs Relacionados"],"Related Merge Requests":["Merge Requests Relacionados"],"Related Merged Requests":["Merge Requests Relacionados"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["O estágio de codificação mostra o tempo desde o primeiro commit até a criação do merge request. \\nOs dados serão automaticamente adicionados aqui uma vez que você tenha criado seu primeiro merge request."],"The collection of events added to the data gathered for that stage.":["A coleção de eventos adicionados aos dados coletados para esse estágio."],"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.":["O estágio em questão mostra o tempo que leva desde a criação de uma tarefa até a sua assinatura para um milestone, ou a sua adição para a lista no seu Painel de Tarefas. Comece a criar tarefas para ver dados para esta etapa."],"The phase of the development lifecycle.":["A fase do ciclo de vida do desenvolvimento."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["A fase de planejamento mostra o tempo do passo anterior até empurrar o seu primeiro commit. Este tempo será adicionado automaticamente assim que você realizar seu primeiro commit."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["O estágio de produção mostra o tempo total que leva entre criar uma tarefa e implantar o código na produção. Os dados serão adicionados automaticamente até que você complete todo o ciclo de produção."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["A etapa de revisão mostra o tempo de criação de um merge request até que o merge seja feito. Os dados serão automaticamente adicionados depois que você fizer seu primeiro merge request."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["O estágio de estágio mostra o tempo entre a fusão do MR e o código de implantação para o ambiente de produção. Os dados serão automaticamente adicionados depois de implantar na produção pela primeira vez."],"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.":["A fase de teste mostra o tempo que o GitLab CI leva para executar cada pipeline para o merge request relacionado. Os dados serão automaticamente adicionados após a conclusão do primeiro pipeline."],"The time taken by each data entry gathered by that stage.":["O tempo necessário para cada entrada de dados reunida por essa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["O valor situado no ponto médio de uma série de valores observados. Ex., entre 3, 5, 9, a mediana é 5. Entre 3, 5, 7, 8, a mediana é (5 + 7) / 2 = 6."],"Time before an issue gets scheduled":["Tempo até que uma tarefa seja planejada"],"Time before an issue starts implementation":["Tempo até que uma tarefa comece a ser implementada"],"Time between merge request creation and merge/close":["Tempo entre a criação do merge request e o merge/fechamento"],"Time until first merge request":["Tempo até o primeiro merge request"],"Time|hr":["h","hs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tempo Total"],"Total test time for all commits/merges":["Tempo de teste total para todos os commits/merges"],"Want to see the data? Please ask an administrator for access.":["Precisa visualizar os dados? Solicite acesso ao administrador."],"We don't have enough data to show this stage.":["Não temos dados suficientes para mostrar esta fase."],"You need permission.":["Você precisa de permissão."],"day":["dia","dias"]}}}; \ No newline at end of file diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb index f7ac48f7dbd..6a8f9418df3 100644 --- a/lib/gitlab/i18n.rb +++ b/lib/gitlab/i18n.rb @@ -6,6 +6,7 @@ module Gitlab 'en' => 'English', 'es' => 'Español', 'de' => 'Deutsch', + 'pt_BR' => 'Português(Brasil)', 'zh_CN' => '简体中文', 'zh_HK' => '繁體中文(香港)', 'zh_TW' => '繁體中文(臺灣)' diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po new file mode 100644 index 00000000000..5ad41f92b64 --- /dev/null +++ b/locale/pt_BR/gitlab.po @@ -0,0 +1,260 @@ +# Alexandre Alencar , 2017. #zanata +# Fabio Beneditto , 2017. #zanata +# Leandro Nunes dos Santos , 2017. #zanata +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-05-04 19:24-0500\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2017-06-05 03:29-0400\n" +"Last-Translator: Alexandre Alencar \n" +"Language-Team: Portuguese (Brazil)\n" +"Language: pt-BR\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgid "ByAuthor|by" +msgstr "por" + +msgid "Commit" +msgid_plural "Commits" +msgstr[0] "Commit" +msgstr[1] "Commits" + +msgid "" +"Cycle Analytics gives an overview of how much time it takes to go from idea " +"to production in your project." +msgstr "" +"A Análise de Ciclo fornece uma visão geral de quanto tempo uma ideia demora " +"para ir para produção em seu projeto." + +msgid "CycleAnalyticsStage|Code" +msgstr "Código" + +msgid "CycleAnalyticsStage|Issue" +msgstr "Tarefa" + +msgid "CycleAnalyticsStage|Plan" +msgstr "Plano" + +msgid "CycleAnalyticsStage|Production" +msgstr "Produção" + +msgid "CycleAnalyticsStage|Review" +msgstr "Revisão" + +msgid "CycleAnalyticsStage|Staging" +msgstr "Homologação" + +msgid "CycleAnalyticsStage|Test" +msgstr "Teste" + +msgid "Deploy" +msgid_plural "Deploys" +msgstr[0] "Implantação" +msgstr[1] "Implantações" + +msgid "FirstPushedBy|First" +msgstr "Primeiro" + +msgid "FirstPushedBy|pushed by" +msgstr "publicado por" + +msgid "From issue creation until deploy to production" +msgstr "Da criação de tarefas até a implantação para a produção" + +msgid "From merge request merge until deploy to production" +msgstr "Da incorporação do merge request até a implantação em produção" + +msgid "Introducing Cycle Analytics" +msgstr "Apresentando a Análise de Ciclo" + +msgid "Last %d day" +msgid_plural "Last %d days" +msgstr[0] "Último %d dia" +msgstr[1] "Últimos %d dias" + +msgid "Limited to showing %d event at most" +msgid_plural "Limited to showing %d events at most" +msgstr[0] "Limitado a mostrar %d evento no máximo" +msgstr[1] "Limitado a mostrar %d eventos no máximo" + +msgid "Median" +msgstr "Mediana" + +msgid "New Issue" +msgid_plural "New Issues" +msgstr[0] "Nova Tarefa" +msgstr[1] "Novas Tarefas" + +msgid "Not available" +msgstr "Não disponível" + +msgid "Not enough data" +msgstr "Dados insuficientes" + +msgid "OpenedNDaysAgo|Opened" +msgstr "Aberto" + +msgid "Pipeline Health" +msgstr "Saúde da Pipeline" + +msgid "ProjectLifecycle|Stage" +msgstr "Etapa" + +msgid "Read more" +msgstr "Ler mais" + +msgid "Related Commits" +msgstr "Commits Relacionados" + +msgid "Related Deployed Jobs" +msgstr "Jobs Relacionados Incorporados" + +msgid "Related Issues" +msgstr "Tarefas Relacionadas" + +msgid "Related Jobs" +msgstr "Jobs Relacionados" + +msgid "Related Merge Requests" +msgstr "Merge Requests Relacionados" + +msgid "Related Merged Requests" +msgstr "Merge Requests Relacionados" + +msgid "Showing %d event" +msgid_plural "Showing %d events" +msgstr[0] "Mostrando %d evento" +msgstr[1] "Mostrando %d eventos" + +msgid "" +"The coding stage shows the time from the first commit to creating the merge " +"request. The data will automatically be added here once you create your " +"first merge request." +msgstr "" +"O estágio de codificação mostra o tempo desde o primeiro commit até a " +"criação do merge request. \n" +"Os dados serão automaticamente adicionados aqui uma vez que você tenha " +"criado seu primeiro merge request." + +msgid "The collection of events added to the data gathered for that stage." +msgstr "" +"A coleção de eventos adicionados aos dados coletados para esse estágio." + +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 "" +"O estágio em questão mostra o tempo que leva desde a criação de uma tarefa " +"até a sua assinatura para um milestone, ou a sua adição para a lista no seu " +"Painel de Tarefas. Comece a criar tarefas para ver dados para esta etapa." + +msgid "The phase of the development lifecycle." +msgstr "A fase do ciclo de vida do desenvolvimento." + +msgid "" +"The planning stage shows the time from the previous step to pushing your " +"first commit. This time will be added automatically once you push your first " +"commit." +msgstr "" +"A fase de planejamento mostra o tempo do passo anterior até empurrar o seu " +"primeiro commit. Este tempo será adicionado automaticamente assim que você " +"realizar seu primeiro commit." + +msgid "" +"The production stage shows the total time it takes between creating an issue " +"and deploying the code to production. The data will be automatically added " +"once you have completed the full idea to production cycle." +msgstr "" +"O estágio de produção mostra o tempo total que leva entre criar uma tarefa e " +"implantar o código na produção. Os dados serão adicionados automaticamente " +"até que você complete todo o ciclo de produção." + +msgid "" +"The review stage shows the time from creating the merge request to merging " +"it. The data will automatically be added after you merge your first merge " +"request." +msgstr "" +"A etapa de revisão mostra o tempo de criação de um merge request até que o " +"merge seja feito. Os dados serão automaticamente adicionados depois que você " +"fizer seu primeiro merge request." + +msgid "" +"The staging stage shows the time between merging the MR and deploying code " +"to the production environment. The data will be automatically added once you " +"deploy to production for the first time." +msgstr "" +"O estágio de estágio mostra o tempo entre a fusão do MR e o código de " +"implantação para o ambiente de produção. Os dados serão automaticamente " +"adicionados depois de implantar na produção pela primeira vez." + +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 "" +"A fase de teste mostra o tempo que o GitLab CI leva para executar cada " +"pipeline para o merge request relacionado. Os dados serão automaticamente " +"adicionados após a conclusão do primeiro pipeline." + +msgid "The time taken by each data entry gathered by that stage." +msgstr "O tempo necessário para cada entrada de dados reunida por essa etapa." + +msgid "" +"The value lying at the midpoint of a series of observed values. E.g., " +"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 =" +" 6." +msgstr "" +"O valor situado no ponto médio de uma série de valores observados. Ex., " +"entre 3, 5, 9, a mediana é 5. Entre 3, 5, 7, 8, a mediana é (5 + 7) / 2 = 6." + +msgid "Time before an issue gets scheduled" +msgstr "Tempo até que uma tarefa seja planejada" + +msgid "Time before an issue starts implementation" +msgstr "Tempo até que uma tarefa comece a ser implementada" + +msgid "Time between merge request creation and merge/close" +msgstr "Tempo entre a criação do merge request e o merge/fechamento" + +msgid "Time until first merge request" +msgstr "Tempo até o primeiro merge request" + +msgid "Time|hr" +msgid_plural "Time|hrs" +msgstr[0] "h" +msgstr[1] "hs" + +msgid "Time|min" +msgid_plural "Time|mins" +msgstr[0] "min" +msgstr[1] "mins" + +msgid "Time|s" +msgstr "s" + +msgid "Total Time" +msgstr "Tempo Total" + +msgid "Total test time for all commits/merges" +msgstr "Tempo de teste total para todos os commits/merges" + +msgid "Want to see the data? Please ask an administrator for access." +msgstr "Precisa visualizar os dados? Solicite acesso ao administrador." + +msgid "We don't have enough data to show this stage." +msgstr "Não temos dados suficientes para mostrar esta fase." + +msgid "You need permission." +msgstr "Você precisa de permissão." + +msgid "day" +msgid_plural "days" +msgstr[0] "dia" +msgstr[1] "dias" + diff --git a/locale/pt_BR/gitlab.po.time_stamp b/locale/pt_BR/gitlab.po.time_stamp new file mode 100644 index 00000000000..e69de29bb2d -- cgit v1.2.1 From 417a24de988e06177ccedf2b3367bf71c72a920b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E6=B6=9B?= Date: Tue, 6 Jun 2017 11:44:28 +0800 Subject: added changelog for Portuguese Brazil translations --- .../33334-portuguese_brazil_translation_of_cycle_analytics_page.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/33334-portuguese_brazil_translation_of_cycle_analytics_page.yml diff --git a/changelogs/unreleased/33334-portuguese_brazil_translation_of_cycle_analytics_page.yml b/changelogs/unreleased/33334-portuguese_brazil_translation_of_cycle_analytics_page.yml new file mode 100644 index 00000000000..a0e0458da16 --- /dev/null +++ b/changelogs/unreleased/33334-portuguese_brazil_translation_of_cycle_analytics_page.yml @@ -0,0 +1,4 @@ +--- +title: Add Portuguese Brazil of Cycle Analytics Page to I18N +merge_request: 11920 +author:Huang Tao -- cgit v1.2.1 From 5b8bc13c8fb613caa75e1bb87b930e14f997b966 Mon Sep 17 00:00:00 2001 From: Huang Tao Date: Tue, 6 Jun 2017 04:59:55 +0000 Subject: Update i18n.rb style --- lib/gitlab/i18n.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb index 6a8f9418df3..541235cf911 100644 --- a/lib/gitlab/i18n.rb +++ b/lib/gitlab/i18n.rb @@ -6,7 +6,7 @@ module Gitlab 'en' => 'English', 'es' => 'Español', 'de' => 'Deutsch', - 'pt_BR' => 'Português(Brasil)', + 'pt_BR' => 'Português(Brasil)', 'zh_CN' => '简体中文', 'zh_HK' => '繁體中文(香港)', 'zh_TW' => '繁體中文(臺灣)' -- cgit v1.2.1 From 75e0ec0fa6798de220ddeb207a8754d97d9da660 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Jun 2017 02:56:07 -0500 Subject: Fix rspec --- .../issues/filtered_search/filter_issues_spec.rb | 8 ++++---- .../merge_requests/filter_merge_requests_spec.rb | 16 +++++++--------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index e5e4ba06b5a..36f8369c142 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -777,17 +777,17 @@ describe 'Filter issues', js: true, feature: true do end it 'open state' do - find('.issues-state-filters a', text: 'Closed').click + find('.issues-state-filters .state-closed').click wait_for_requests - find('.issues-state-filters a', text: 'Open').click + find('.issues-state-filters .state-opened').click wait_for_requests expect(page).to have_selector('.issues-list .issue', count: 4) end it 'closed state' do - find('.issues-state-filters a', text: 'Closed').click + find('.issues-state-filters .state-closed').click wait_for_requests expect(page).to have_selector('.issues-list .issue', count: 1) @@ -795,7 +795,7 @@ describe 'Filter issues', js: true, feature: true do end it 'all state' do - find('.issues-state-filters a', text: 'All').click + find('.issues-state-filters .state-all').click wait_for_requests expect(page).to have_selector('.issues-list .issue', count: 5) diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb index 1e26b3d601e..5a13189c5bf 100644 --- a/spec/features/merge_requests/filter_merge_requests_spec.rb +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -40,13 +40,13 @@ describe 'Filter merge requests', feature: true do end it 'does not change when closed link is clicked' do - find('.issues-state-filters a', text: "Closed").click + find('.issues-state-filters .state-closed').click expect_assignee_visual_tokens() end it 'does not change when all link is clicked' do - find('.issues-state-filters a', text: "All").click + find('.issues-state-filters .state-all').click expect_assignee_visual_tokens() end @@ -73,13 +73,13 @@ describe 'Filter merge requests', feature: true do end it 'does not change when closed link is clicked' do - find('.issues-state-filters a', text: "Closed").click + find('.issues-state-filters .state-closed').click expect_milestone_visual_tokens() end it 'does not change when all link is clicked' do - find('.issues-state-filters a', text: "All").click + find('.issues-state-filters .state-all').click expect_milestone_visual_tokens() end @@ -142,11 +142,9 @@ describe 'Filter merge requests', feature: true do expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) expect_filtered_search_input_empty - input_filtered_search_keys("label:~#{label.title} ") + input_filtered_search_keys("label:~#{label.title}") expect_mr_list_count(1) - - find("#state-opened[href=\"#{URI.parse(current_url).path}?assignee_username=#{user.username}&label_name%5B%5D=#{label.title}&scope=all&state=opened\"]") end context 'assignee and label', js: true do @@ -163,13 +161,13 @@ describe 'Filter merge requests', feature: true do end it 'does not change when closed link is clicked' do - find('.issues-state-filters a', text: "Closed").click + find('.issues-state-filters .state-closed').click expect_assignee_label_visual_tokens() end it 'does not change when all link is clicked' do - find('.issues-state-filters a', text: "All").click + find('.issues-state-filters .state-all').click expect_assignee_label_visual_tokens() end -- cgit v1.2.1 From 323a326c73f4aabf37bf79f8e42350c128983c2d Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 6 Jun 2017 00:06:08 -0500 Subject: Improve pagination when searching or filtering [ci skip] --- app/assets/javascripts/filterable_list.js | 62 ++++++++++++++-------- .../javascripts/groups/components/groups.vue | 12 ++--- .../javascripts/groups/groups_filterable_list.js | 57 +++++++++++++++----- app/assets/javascripts/groups/index.js | 35 ++++++++++-- .../javascripts/groups/services/groups_service.js | 6 ++- app/assets/javascripts/lib/utils/common_utils.js | 4 +- app/views/dashboard/groups/_groups.html.haml | 2 +- 7 files changed, 130 insertions(+), 48 deletions(-) diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js index 17c39cc7bbb..139206cc185 100644 --- a/app/assets/javascripts/filterable_list.js +++ b/app/assets/javascripts/filterable_list.js @@ -8,7 +8,15 @@ export default class FilterableList { this.filterForm = form; this.listFilterElement = filter; this.listHolderElement = holder; - this.filterUrl = `${this.filterForm.getAttribute('action')}?${$(this.filterForm).serialize()}`; + this.isBusy = false; + } + + getFilterEndpoint() { + return `${this.filterForm.getAttribute('action')}?${$(this.filterForm).serialize()}`; + } + + getPagePath() { + return this.getFilterEndpoint(); } initSearch() { @@ -20,9 +28,19 @@ export default class FilterableList { } onFilterInput() { - const url = this.filterForm.getAttribute('action'); - const data = $(this.filterForm).serialize(); - this.filterResults(url, data, 'filter-input'); + const $form = $(this.filterForm); + const queryData = {}; + const filterGroupsParam = $form.find('[name="filter_groups"]').val(); + + if (filterGroupsParam) { + queryData.filter_groups = filterGroupsParam; + } + + this.filterResults(queryData); + + if (this.setDefaultFilterOption) { + this.setDefaultFilterOption(); + } } bindEvents() { @@ -33,42 +51,44 @@ export default class FilterableList { this.listFilterElement.removeEventListener('input', this.debounceFilter); } - filterResults(url, data, comingFrom) { - const endpoint = url || this.filterForm.getAttribute('action'); - const additionalData = data || $(this.filterForm).serialize(); + filterResults(queryData) { + if (this.isBusy) { + return false; + } $(this.listHolderElement).fadeTo(250, 0.5); return $.ajax({ - url: endpoint, - data: additionalData, + url: this.getFilterEndpoint(), + data: queryData, type: 'GET', dataType: 'json', context: this, complete: this.onFilterComplete, + beforeSend: () => { + this.isBusy = true; + }, success: (response, textStatus, xhr) => { - if (this.preOnFilterSuccess) { - this.preOnFilterSuccess(comingFrom); - } - - this.onFilterSuccess(response, xhr); + this.onFilterSuccess(response, xhr, queryData); }, }); } - onFilterSuccess(data) { - if (data.html) { - this.listHolderElement.innerHTML = data.html; + onFilterSuccess(response, xhr, queryData) { + if (response.html) { + this.listHolderElement.innerHTML = response.html; } - // Change url so if user reload a page - search results are saved - return window.history.replaceState({ - page: this.filterUrl, + // Change url so if user reload a page - search results are saved + const currentPath = this.getPagePath(queryData); - }, document.title, this.filterUrl); + return window.history.replaceState({ + page: currentPath, + }, document.title, currentPath); } onFilterComplete() { + this.isBusy = false; $(this.listHolderElement).fadeTo(250, 1); } } diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 6ddf54275d9..759b68c8bb4 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -1,5 +1,6 @@ diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 759b68c8bb4..ad85d84b1e6 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -1,11 +1,8 @@