diff options
20 files changed, 204 insertions, 20 deletions
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index f9e23189774..89fc002d4ce 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -533,6 +533,11 @@ import SearchAutocomplete from './search_autocomplete'; .then(callDefault) .catch(fail); break; + case 'dashboard:groups:index': + import('./pages/dashboard/groups/index') + .then(callDefault) + .catch(fail); + break; } switch (path[0]) { case 'sessions': diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 8b850765a1b..57eaac72906 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -10,7 +10,7 @@ import groupItemComponent from './components/group_item.vue'; Vue.use(Translate); -document.addEventListener('DOMContentLoaded', () => { +export default () => { const el = document.getElementById('js-groups-tree'); // Don't do anything if element doesn't exist (No groups) @@ -71,4 +71,4 @@ document.addEventListener('DOMContentLoaded', () => { }); }, }); -}); +}; diff --git a/app/assets/javascripts/groups/service/groups_service.js b/app/assets/javascripts/groups/service/groups_service.js index 639410384c2..b79ba291463 100644 --- a/app/assets/javascripts/groups/service/groups_service.js +++ b/app/assets/javascripts/groups/service/groups_service.js @@ -1,7 +1,5 @@ import Vue from 'vue'; -import VueResource from 'vue-resource'; - -Vue.use(VueResource); +import '../../vue_shared/vue_resource_interceptor'; export default class GroupsService { constructor(endpoint) { diff --git a/app/assets/javascripts/pages/dashboard/groups/index/index.js b/app/assets/javascripts/pages/dashboard/groups/index/index.js new file mode 100644 index 00000000000..8a2aae706c0 --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/groups/index/index.js @@ -0,0 +1,5 @@ +import initGroupsList from '../../../../groups'; + +export default () => { + initGroupsList(); +}; diff --git a/app/assets/javascripts/pages/explore/groups/index.js b/app/assets/javascripts/pages/explore/groups/index.js index 859b073f1cb..e59c38b8bc4 100644 --- a/app/assets/javascripts/pages/explore/groups/index.js +++ b/app/assets/javascripts/pages/explore/groups/index.js @@ -1,8 +1,10 @@ import GroupsList from '~/groups_list'; import Landing from '~/landing'; +import initGroupsList from '../../../groups'; export default function () { new GroupsList(); // eslint-disable-line no-new + initGroupsList(); const landingElement = document.querySelector('.js-explore-groups-landing'); if (!landingElement) return; const exploreGroupsLanding = new Landing( diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js index 45e11b64306..6ed0f010f15 100644 --- a/app/assets/javascripts/pages/groups/show/index.js +++ b/app/assets/javascripts/pages/groups/show/index.js @@ -5,6 +5,7 @@ import notificationsDropdown from '~/notifications_dropdown'; import NotificationsForm from '~/notifications_form'; import ProjectsList from '~/projects_list'; import ShortcutsNavigation from '~/shortcuts_navigation'; +import initGroupsList from '../../../groups'; export default () => { const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); @@ -16,4 +17,6 @@ export default () => { if (newGroupChildWrapper) { new NewGroupChild(newGroupChildWrapper); } + + initGroupsList(); }; diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml index 601b6a8b1a7..db856ef7d7b 100644 --- a/app/views/dashboard/groups/_groups.html.haml +++ b/app/views/dashboard/groups/_groups.html.haml @@ -1,2 +1,4 @@ .js-groups-list-holder #js-groups-tree{ data: { hide_projects: 'true', endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } } + .loading-container.text-center + = icon('spinner spin 2x', class: 'loading-animation prepend-top-20') diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 25bf08c6c12..50f39f93283 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -3,9 +3,6 @@ - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' -= webpack_bundle_tag 'common_vue' -= webpack_bundle_tag 'groups' - - if params[:filter].blank? && @groups.empty? = render 'shared/groups/empty_state' - else diff --git a/app/views/explore/groups/_groups.html.haml b/app/views/explore/groups/_groups.html.haml index 91149498248..ff57b39e947 100644 --- a/app/views/explore/groups/_groups.html.haml +++ b/app/views/explore/groups/_groups.html.haml @@ -1,2 +1,4 @@ .js-groups-list-holder #js-groups-tree{ data: { hide_projects: 'true', endpoint: explore_groups_path(format: :json), path: explore_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } } + .loading-container.text-center + = icon('spinner spin 2x', class: 'loading-animation prepend-top-20') diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 86abdf547cc..efa8b2706da 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -2,9 +2,6 @@ - page_title "Groups" - header_title "Groups", dashboard_groups_path -= webpack_bundle_tag 'common_vue' -= webpack_bundle_tag 'groups' - - if current_user = render 'dashboard/groups_head' - else diff --git a/app/views/groups/_children.html.haml b/app/views/groups/_children.html.haml index 3afb6b2f849..742b40784d3 100644 --- a/app/views/groups/_children.html.haml +++ b/app/views/groups/_children.html.haml @@ -1,5 +1,4 @@ -= webpack_bundle_tag 'common_vue' -= webpack_bundle_tag 'groups' - .js-groups-list-holder #js-groups-tree{ data: { hide_projects: 'false', group_id: group.id, endpoint: group_children_path(group, format: :json), path: group_path(group), form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } } + .loading-container.text-center + = icon('spinner spin 2x', class: 'loading-animation prepend-top-20') diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index d0b9e891b82..cb21f90696f 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -1,5 +1,3 @@ -- content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('group') - parent = @group.parent - group_path = root_url - group_path << parent.full_path + '/' if parent diff --git a/changelogs/unreleased/24035-api-create-application.yml b/changelogs/unreleased/24035-api-create-application.yml new file mode 100644 index 00000000000..c583a020d9d --- /dev/null +++ b/changelogs/unreleased/24035-api-create-application.yml @@ -0,0 +1,4 @@ +--- +title: Add application create API +merge_request: 8160 +author: Nicolas Merelli @PNSalocin diff --git a/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml b/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml new file mode 100644 index 00000000000..3854985e576 --- /dev/null +++ b/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml @@ -0,0 +1,5 @@ +--- +title: Return more consistent values for merge_status on MR APIs +merge_request: +author: +type: fixed diff --git a/config/webpack.config.js b/config/webpack.config.js index 26502efaf6c..783677b5b8d 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -44,9 +44,6 @@ var config = { graphs: './graphs/graphs_bundle.js', graphs_charts: './graphs/graphs_charts.js', graphs_show: './graphs/graphs_show.js', - group: './group.js', - groups: './groups/index.js', - groups_list: './groups_list.js', help: './help/help.js', how_to_merge: './how_to_merge.js', issue_show: './issue_show/index.js', diff --git a/doc/api/applications.md b/doc/api/applications.md new file mode 100644 index 00000000000..933867ed0bb --- /dev/null +++ b/doc/api/applications.md @@ -0,0 +1,37 @@ +# Applications API + +> [Introduced][ce-8160] in GitLab 10.5 + +[ce-8160]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8160 + +## Create a application + +Create a application by posting a JSON payload. + +User must be admin to do that. + +Returns `200` if the request succeeds. + +``` +POST /applications +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `name` | string | yes | The name of the application | +| `redirect_uri` | string | yes | The redirect URI of the application | +| `scopes` | string | yes | The scopes of the application | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "name=MyApplication&redirect_uri=http://redirect.uri&scopes=" https://gitlab.example.com/api/v3/applications +``` + +Example response: + +```json +{ + "application_id": "5832fc6e14300a0d962240a8144466eef4ee93ef0d218477e55f11cf12fc3737", + "secret": "ee1dd64b6adc89cf7e2c23099301ccc2c61b441064e9324d963c46902a85ec34", + "callback_url": "http://redirect.uri" +} +``` diff --git a/lib/api/api.rb b/lib/api/api.rb index ae161efb358..f3f64244589 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -106,6 +106,7 @@ module API # Keep in alphabetical order mount ::API::AccessRequests + mount ::API::Applications mount ::API::AwardEmoji mount ::API::Boards mount ::API::Branches diff --git a/lib/api/applications.rb b/lib/api/applications.rb new file mode 100644 index 00000000000..b122cdefe4e --- /dev/null +++ b/lib/api/applications.rb @@ -0,0 +1,27 @@ +module API + # External applications API + class Applications < Grape::API + before { authenticated_as_admin! } + + resource :applications do + desc 'Create a new application' do + detail 'This feature was introduced in GitLab 10.5' + success Entities::ApplicationWithSecret + end + params do + requires :name, type: String, desc: 'Application name' + requires :redirect_uri, type: String, desc: 'Application redirect URI' + requires :scopes, type: String, desc: 'Application scopes' + end + post do + application = Doorkeeper::Application.new(declared_params) + + if application.save + present application, with: Entities::ApplicationWithSecret + else + render_validation_error! application + end + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 3f4b62dc1b2..5b470bd3479 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -507,7 +507,16 @@ module API expose :work_in_progress?, as: :work_in_progress expose :milestone, using: Entities::Milestone expose :merge_when_pipeline_succeeds - expose :merge_status + + # Ideally we should deprecate `MergeRequest#merge_status` exposure and + # use `MergeRequest#mergeable?` instead (boolean). + # See https://gitlab.com/gitlab-org/gitlab-ce/issues/42344 for more + # information. + expose :merge_status do |merge_request| + # In order to avoid having a breaking change for users, we keep returning the + # expected values from MergeRequest#merge_status state machine. + merge_request.mergeable? ? 'can_be_merged' : 'cannot_be_merged' + end expose :diff_head_sha, as: :sha expose :merge_commit_sha expose :user_notes_count @@ -1157,5 +1166,15 @@ module API pages_domain end end + + class Application < Grape::Entity + expose :uid, as: :application_id + expose :redirect_uri, as: :callback_url + end + + # Use with care, this exposes the secret + class ApplicationWithSecret < Application + expose :secret + end end end diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb new file mode 100644 index 00000000000..f56bc932f40 --- /dev/null +++ b/spec/requests/api/applications_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +describe API::Applications, :api do + include ApiHelpers + + let(:admin_user) { create(:user, admin: true) } + let(:user) { create(:user, admin: false) } + + describe 'POST /applications' do + context 'authenticated and authorized user' do + it 'creates and returns an OAuth application' do + expect do + post api('/applications', admin_user), name: 'application_name', redirect_uri: 'http://application.url', scopes: '' + end.to change { Doorkeeper::Application.count }.by 1 + + application = Doorkeeper::Application.find_by(name: 'application_name', redirect_uri: 'http://application.url') + + expect(response).to have_http_status 201 + expect(json_response).to be_a Hash + expect(json_response['application_id']).to eq application.uid + expect(json_response['secret']).to eq application.secret + expect(json_response['callback_url']).to eq application.redirect_uri + end + + it 'does not allow creating an application with the wrong redirect_uri format' do + expect do + post api('/applications', admin_user), name: 'application_name', redirect_uri: 'wrong_url_format', scopes: '' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 400 + expect(json_response).to be_a Hash + expect(json_response['message']['redirect_uri'][0]).to eq('must be an absolute URI.') + end + + it 'does not allow creating an application without a name' do + expect do + post api('/applications', admin_user), redirect_uri: 'http://application.url', scopes: '' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 400 + expect(json_response).to be_a Hash + expect(json_response['error']).to eq('name is missing') + end + + it 'does not allow creating an application without a redirect_uri' do + expect do + post api('/applications', admin_user), name: 'application_name', scopes: '' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 400 + expect(json_response).to be_a Hash + expect(json_response['error']).to eq('redirect_uri is missing') + end + + it 'does not allow creating an application without scopes' do + expect do + post api('/applications', admin_user), name: 'application_name', redirect_uri: 'http://application.url' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 400 + expect(json_response).to be_a Hash + expect(json_response['error']).to eq('scopes is missing') + end + end + + context 'authorized user without authorization' do + it 'does not create application' do + expect do + post api('/applications', user), name: 'application_name', redirect_uri: 'http://application.url', scopes: '' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 403 + end + end + + context 'non-authenticated user' do + it 'does not create application' do + expect do + post api('/applications'), name: 'application_name', redirect_uri: 'http://application.url' + end.not_to change { Doorkeeper::Application.count } + + expect(response).to have_http_status 401 + end + end + end +end |