Commit 95c3aee2 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'xanf-use-new-import-ui-for-all-providers-controllers' into 'master'

Implement JSON status for importers behind feature flag

See merge request gitlab-org/gitlab!33980
parents 56e3823d 4c12d33f
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import ImportProjectsTable from './import_projects_table.vue';
export default {
components: {
ImportProjectsTable,
GlAlert,
GlSprintf,
GlLink,
},
props: {
providerTitle: {
type: String,
required: true,
},
},
data() {
return {
isWarningDismissed: false,
};
},
computed: {
currentPage() {
return window.location.href;
},
},
};
</script>
<template>
<import-projects-table provider-title="providerTitle">
<template #actions>
<slot name="actions"></slot>
</template>
<template #incompatible-repos-warning>
<gl-alert
v-if="!isWarningDismissed"
variant="warning"
class="gl-my-2"
@dismiss="isWarningDismissed = true"
>
<gl-sprintf
:message="
__(
'One or more of your %{provider} projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git.',
)
"
>
<template #provider>
{{ providerTitle }}
</template>
</gl-sprintf>
<gl-sprintf
:message="
__(
'Please convert %{linkStart}them to Git%{linkEnd}, and go through the %{linkToImportFlow} again.',
)
"
>
<template #link="{ content }">
<gl-link
href="https://www.atlassian.com/git/tutorials/migrating-overview"
target="_blank"
>{{ content }}</gl-link
>
</template>
<template #linkToImportFlow>
<gl-link :href="currentPage">{{ __('import flow') }}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
</template>
</import-projects-table>
</template>
......@@ -24,6 +24,11 @@ export default {
type: String,
required: true,
},
filterable: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
......@@ -114,7 +119,7 @@ export default {
{{ importAllButtonText }}
</gl-button>
<slot name="actions"></slot>
<form class="gl-ml-auto" novalidate @submit.prevent>
<form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
<input
:value="filter"
data-qa-selector="githubish_import_filter_field"
......
......@@ -30,6 +30,7 @@ export function initStoreFromElement(element) {
export function initPropsFromElement(element) {
return {
providerTitle: element.dataset.providerTitle,
filterable: parseBoolean(element.dataset.filterable),
};
}
......
......@@ -2,6 +2,7 @@ import Visibility from 'visibilityjs';
import * as types from './mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
import { visitUrl } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import { s__, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils';
......@@ -9,6 +10,9 @@ import { jobsPathWithFilter, reposPathWithFilter } from './getters';
let eTagPoll;
const hasRedirectInError = e => e?.response?.data?.error?.redirect;
const redirectToUrlInError = e => visitUrl(e.response.data.error.redirect);
export const clearJobsEtagPoll = () => {
eTagPoll = null;
};
......@@ -33,7 +37,10 @@ export const fetchRepos = ({ state, dispatch, commit }) => {
commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })),
)
.then(() => dispatch('fetchJobs'))
.catch(() => {
.catch(e => {
if (hasRedirectInError(e)) {
redirectToUrlInError(e);
} else {
createFlash(
sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), {
provider,
......@@ -41,6 +48,7 @@ export const fetchRepos = ({ state, dispatch, commit }) => {
);
commit(types.RECEIVE_REPOS_ERROR);
}
});
};
......@@ -87,8 +95,13 @@ export const fetchJobs = ({ state, commit, dispatch }) => {
method: 'fetchJobs',
successCallback: ({ data }) =>
commit(types.RECEIVE_JOBS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })),
errorCallback: () =>
createFlash(s__('ImportProjects|Update of imported projects with realtime changes failed')),
errorCallback: e => {
if (hasRedirectInError(e)) {
redirectToUrlInError(e);
} else {
createFlash(s__('ImportProjects|Update of imported projects with realtime changes failed'));
}
},
data: { filter },
});
......
import Vue from 'vue';
import { initStoreFromElement, initPropsFromElement } from '~/import_projects';
import BitbucketStatusTable from '~/import_projects/components/bitbucket_status_table.vue';
document.addEventListener('DOMContentLoaded', () => {
const mountElement = document.getElementById('import-projects-mount-element');
if (!mountElement) return undefined;
const store = initStoreFromElement(mountElement);
const props = initPropsFromElement(mountElement);
return new Vue({
el: mountElement,
store,
render(createElement) {
return createElement(BitbucketStatusTable, { props });
},
});
});
<script>
import { GlButton } from '@gitlab/ui';
import BitbucketStatusTable from '~/import_projects/components/bitbucket_status_table.vue';
export default {
components: {
BitbucketStatusTable,
GlButton,
},
props: {
providerTitle: {
type: String,
required: true,
},
reconfigurePath: {
type: String,
required: true,
},
},
};
</script>
<template>
<bitbucket-status-table :provider-title="providerTitle">
<template #actions>
<gl-button variant="info" class="gl-ml-3" data-method="post" :href="reconfigurePath">{{
__('Reconfigure')
}}</gl-button>
</template>
</bitbucket-status-table>
</template>
import Vue from 'vue';
import { initStoreFromElement, initPropsFromElement } from '~/import_projects';
import BitbucketServerStatusTable from './components/bitbucket_server_status_table.vue';
document.addEventListener('DOMContentLoaded', () => {
const mountElement = document.getElementById('import-projects-mount-element');
if (!mountElement) return undefined;
const store = initStoreFromElement(mountElement);
const props = initPropsFromElement(mountElement);
const { reconfigurePath } = mountElement.dataset;
return new Vue({
el: mountElement,
store,
render(createElement) {
return createElement(BitbucketServerStatusTable, { props: { ...props, reconfigurePath } });
},
});
});
import mountImportProjectsTable from '~/import_projects';
document.addEventListener('DOMContentLoaded', () => {
const mountElement = document.getElementById('import-projects-mount-element');
mountImportProjectsTable(mountElement);
});
import mountImportProjectsTable from '~/import_projects';
document.addEventListener('DOMContentLoaded', () => {
const mountElement = document.getElementById('import-projects-mount-element');
mountImportProjectsTable(mountElement);
});
# frozen_string_literal: true
class Import::BaseController < ApplicationController
include ActionView::Helpers::SanitizeHelper
before_action :import_rate_limit, only: [:create]
def status
respond_to do |format|
format.json do
render json: { imported_projects: serialized_imported_projects,
provider_repos: serialized_provider_repos,
incompatible_repos: serialized_incompatible_repos,
namespaces: serialized_namespaces }
end
format.html
end
end
def realtime_changes
Gitlab::PollingInterval.set_header(response, interval: 3_000)
render json: already_added_projects.to_json(only: [:id], methods: [:import_status])
end
protected
def importable_repos
raise NotImplementedError
end
def incompatible_repos
[]
end
def provider_name
raise NotImplementedError
end
def provider_url
raise NotImplementedError
end
private
def filter_attribute
:name
end
def sanitized_filter_param
@filter ||= sanitize(params[:filter])
end
def filtered(collection)
return collection unless sanitized_filter_param
collection.select { |item| item[filter_attribute].include?(sanitized_filter_param) }
end
def serialized_provider_repos
Import::ProviderRepoSerializer.new(current_user: current_user).represent(importable_repos, provider: provider_name, provider_url: provider_url)
end
def serialized_incompatible_repos
Import::ProviderRepoSerializer.new(current_user: current_user).represent(incompatible_repos, provider: provider_name, provider_url: provider_url)
end
def serialized_imported_projects
ProjectSerializer.new.represent(already_added_projects, serializer: :import, provider_url: provider_url)
end
def already_added_projects
@already_added_projects ||= filtered(find_already_added_projects(provider_name))
end
def serialized_namespaces
NamespaceSerializer.new.represent(namespaces)
end
def namespaces
current_user.manageable_groups_with_routes
end
# rubocop: disable CodeReuse/ActiveRecord
def find_already_added_projects(import_type)
current_user.created_projects.where(import_type: import_type).with_import_state
......
# frozen_string_literal: true
class Import::BitbucketController < Import::BaseController
extend ::Gitlab::Utils::Override
include ActionView::Helpers::SanitizeHelper
before_action :verify_bitbucket_import_enabled
......@@ -10,7 +12,7 @@ class Import::BitbucketController < Import::BaseController
rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized
def callback
response = client.auth_code.get_token(params[:code], redirect_uri: users_import_bitbucket_callback_url)
response = oauth_client.auth_code.get_token(params[:code], redirect_uri: users_import_bitbucket_callback_url)
session[:bitbucket_token] = response.token
session[:bitbucket_expires_at] = response.expires_at
......@@ -22,9 +24,10 @@ class Import::BitbucketController < Import::BaseController
# rubocop: disable CodeReuse/ActiveRecord
def status
return super if Feature.enabled?(:new_import_ui)
bitbucket_client = Bitbucket::Client.new(credentials)
repos = bitbucket_client.repos(filter: sanitized_filter_param)
@repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
@already_added_projects = find_already_added_projects('bitbucket')
......@@ -38,6 +41,10 @@ class Import::BitbucketController < Import::BaseController
render json: find_jobs('bitbucket')
end
def realtime_changes
super
end
def create
bitbucket_client = Bitbucket::Client.new(credentials)
......@@ -59,7 +66,7 @@ class Import::BitbucketController < Import::BaseController
project = Gitlab::BitbucketImport::ProjectCreator.new(repo, project_name, target_namespace, current_user, credentials).execute
if project.persisted?
render json: ProjectSerializer.new.represent(project)
render json: ProjectSerializer.new.represent(project, serializer: :import)
else
render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
......@@ -68,16 +75,50 @@ class Import::BitbucketController < Import::BaseController
end
end
protected
# rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
already_added_projects_names = already_added_projects.map(&:import_source)
bitbucket_repos.reject { |repo| already_added_projects_names.include?(repo.full_name) || !repo.valid? }
end
# rubocop: enable CodeReuse/ActiveRecord
override :incompatible_repos
def incompatible_repos
bitbucket_repos.reject { |repo| repo.valid? }
end
override :provider_name
def provider_name
:bitbucket
end
override :provider_url
def provider_url
provider.url
end
private
def client
@client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options)
def oauth_client
@oauth_client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options)
end
def provider
Gitlab::Auth::OAuth::Provider.config_for('bitbucket')
end
def client
@client ||= Bitbucket::Client.new(credentials)
end
def bitbucket_repos
@bitbucket_repos ||= client.repos(filter: sanitized_filter_param).to_a
end
def options
OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys
end
......@@ -91,7 +132,7 @@ class Import::BitbucketController < Import::BaseController
end
def go_to_bitbucket_for_permissions
redirect_to client.auth_code.authorize_url(redirect_uri: users_import_bitbucket_callback_url)
redirect_to oauth_client.auth_code.authorize_url(redirect_uri: users_import_bitbucket_callback_url)
end
def bitbucket_unauthorized
......
# frozen_string_literal: true
class Import::BitbucketServerController < Import::BaseController
extend ::Gitlab::Utils::Override
include ActionView::Helpers::SanitizeHelper
before_action :verify_bitbucket_server_import_enabled
before_action :bitbucket_auth, except: [:new, :configure]
before_action :validate_import_params, only: [:create]
rescue_from BitbucketServer::Connection::ConnectionError, with: :bitbucket_connection_error
# As a basic sanity check to prevent URL injection, restrict project
# repository input and repository slugs to allowed characters. For Bitbucket:
#
......@@ -24,7 +28,7 @@ class Import::BitbucketServerController < Import::BaseController
end
def create
repo = bitbucket_client.repo(@project_key, @repo_slug)
repo = client.repo(@project_key, @repo_slug)
unless repo
return render json: { errors: _("Project %{project_repo} could not be found") % { project_repo: "#{@project_key}/#{@repo_slug}" } }, status: :unprocessable_entity
......@@ -38,15 +42,13 @@ class Import::BitbucketServerController < Import::BaseController
project = Gitlab::BitbucketServerImport::ProjectCreator.new(@project_key, @repo_slug, repo, project_name, target_namespace, current_user, credentials).execute
if project.persisted?
render json: ProjectSerializer.new.represent(project)
render json: ProjectSerializer.new.represent(project, serializer: :import)
else
render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
else
render json: { errors: _('This namespace has already been taken! Please choose another one.') }, status: :unprocessable_entity
end
rescue BitbucketServer::Connection::ConnectionError => error
render json: { errors: _("Unable to connect to server: %{error}") % { error: error } }, status: :unprocessable_entity
end
def configure
......@@ -59,7 +61,9 @@ class Import::BitbucketServerController < Import::BaseController
# rubocop: disable CodeReuse/ActiveRecord
def status
@collection = bitbucket_client.repos(page_offset: page_offset, limit: limit_per_page, filter: sanitized_filter_param)
return super if Feature.enabled?(:new_import_ui)
@collection = client.repos(page_offset: page_offset, limit: limit_per_page, filter: sanitized_filter_param)
@repos, @incompatible_repos = @collection.partition { |repo| repo.valid? }
# Use the import URL to filter beyond what BaseService#find_already_added_projects
......@@ -67,10 +71,6 @@ class Import::BitbucketServerController < Import::BaseController
already_added_projects_names = @already_added_projects.pluck(:import_source)
@repos.reject! { |repo| already_added_projects_names.include?(repo.browse_url) }
rescue BitbucketServer::Connection::ConnectionError => error
flash[:alert] = _("Unable to connect to server: %{error}") % { error: error }
clear_session_data
redirect_to new_import_bitbucket_server_path
end
# rubocop: enable CodeReuse/ActiveRecord
......@@ -78,6 +78,38 @@ class Import::BitbucketServerController < Import::BaseController
render json: find_jobs('bitbucket_server')
end
def realtime_changes
super
end
protected
# rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
# Use the import URL to filter beyond what BaseService#find_already_added_projects
already_added_projects = filter_added_projects('bitbucket_server', bitbucket_repos.map(&:browse_url))
already_added_projects_names = already_added_projects.map(&:import_source)
bitbucket_repos.reject { |repo| already_added_projects_names.include?(repo.browse_url) || !repo.valid? }
end
# rubocop: enable CodeReuse/ActiveRecord
override :incompatible_repos
def incompatible_repos
bitbucket_repos.reject { |repo| repo.valid? }
end
override :provider_name
def provider_name
:bitbucket_server
end
override :provider_url
def provider_url
session[bitbucket_server_url_key]
end
private
# rubocop: disable CodeReuse/ActiveRecord
......@@ -86,8 +118,12 @@ class Import::BitbucketServerController < Import::BaseController
end
# rubocop: enable CodeReuse/ActiveRecord
def bitbucket_client
@bitbucket_client ||= BitbucketServer::Client.new(credentials)
def client
@client ||= BitbucketServer::Client.new(credentials)
end
def bitbucket_repos
@bitbucket_repos ||= client.repos(page_offset: page_offset, limit: limit_per_page, filter: sanitized_filter_param).to_a
end
def validate_import_params
......@@ -153,4 +189,23 @@ class Import::BitbucketServerController < Import::BaseController
def sanitized_filter_param
sanitize(params[:filter])
end
def bitbucket_connection_error(error)
flash[:alert] = _("Unable to connect to server: %{error}") % { error: error }
clear_session_data
respond_to do |format|
format.json do
render json: {
error: {
message: _("Unable to connect to server: %{error}") % { error: error },
redirect: new_import_bitbucket_server_path
}
}, status: :unprocessable_entity
end
format.html do
redirect_to new_import_bitbucket_server_path
end
end
end
end
# frozen_string_literal: true
class Import::FogbugzController < Import::BaseController
extend ::Gitlab::Utils::Override
before_action :verify_fogbugz_import_enabled
before_action :user_map, only: [:new_user_map, :create_user_map]
before_action :verify_blocked_uri, only: :callback
......@@ -48,6 +50,8 @@ class Import::FogbugzController < Import::BaseController
return redirect_to new_import_fogbugz_path
end
return super if Feature.enabled?(:new_import_ui)
@repos = client.repos
@already_added_projects = find_already_added_projects('fogbugz')
......@@ -57,6 +61,10 @@ class Import::FogbugzController < Import::BaseController
end
# rubocop: enable CodeReuse/ActiveRecord
def realtime_changes
super
end
def jobs
render json: find_jobs('fogbugz')
end
......@@ -69,12 +77,35 @@ class Import::FogbugzController < Import::BaseController
project = Gitlab::FogbugzImport::ProjectCreator.new(repo, fb_session, current_user.namespace, current_user, umap).execute
if project.persisted?
render json: ProjectSerializer.new.represent(project)
render json: ProjectSerializer.new.represent(project, serializer: :import)
else
render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
end
protected
# rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
repos = client.repos
already_added_projects_names = already_added_projects.map(&:import_source)
repos.reject { |repo| already_added_projects_names.include? repo.name }
end
# rubocop: enable CodeReuse/ActiveRecord
override :provider_name
def provider_name
:fogbugz
end
override :provider_url
def provider_url
session[:fogbugz_uri]
end
private
def client
......
......@@ -76,7 +76,7 @@ class Import::GithubController < Import::BaseController
def serialized_provider_repos
repos = client_repos.reject { |repo| already_added_project_names.include? repo.full_name }
ProviderRepoSerializer.new(current_user: current_user).represent(repos, provider: provider, provider_url: provider_url)
Import::ProviderRepoSerializer.new(current_user: current_user).represent(repos, provider: provider, provider_url: provider_url)
end
def serialized_namespaces
......
# frozen_string_literal: true
class Import::GitlabController < Import::BaseController
extend ::Gitlab::Utils::Override
MAX_PROJECT_PAGES = 15
PER_PAGE_PROJECTS = 100
......@@ -16,6 +18,8 @@ class Import::GitlabController < Import::BaseController
# rubocop: disable CodeReuse/ActiveRecord
def status
return super if Feature.enabled?(:new_import_ui)
@repos = client.projects(starting_page: 1, page_limit: MAX_PROJECT_PAGES, per_page: PER_PAGE_PROJECTS)
@already_added_projects = find_already_added_projects('gitlab')
......@@ -37,7 +41,7 @@ class Import::GitlabController < Import::BaseController
project = Gitlab::GitlabImport::ProjectCreator.new(repo, target_namespace, current_user, access_params).execute
if project.persisted?
render json: ProjectSerializer.new.represent(project)
render json: ProjectSerializer.new.represent(project, serializer: :import)
else
render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
......@@ -46,6 +50,29 @@ class Import::GitlabController < Import::BaseController
end
end
protected
# rubocop: disable CodeReuse/ActiveRecord
override :importable_repos
def importable_repos
repos = client.projects(starting_page: 1, page_limit: MAX_PROJECT_PAGES, per_page: PER_PAGE_PROJECTS)
already_added_projects_names = already_added_projects.map(&:import_source)
repos.reject { |repo| already_added_projects_names.include? repo["path_with_namespace"] }
end
# rubocop: enable CodeReuse/ActiveRecord
override :provider_name
def provider_name
:gitlab
end
override :provider_url
def provider_url
'https://gitlab.com'
end
private
def client
......
# frozen_string_literal: true
class Import::BaseProviderRepoEntity < Grape::Entity
expose :id
expose :full_name
expose :sanitized_name
expose :provider_link
end
# frozen_string_literal: true
class Import::BitbucketProviderRepoEntity < Import::BaseProviderRepoEntity
expose :id, override: true do |repo|
repo.full_name
end
expose :sanitized_name, override: true do |repo|
repo.name.gsub(/[^\s\w.-]/, '')
end
expose :provider_link, override: true do |repo, options|
repo.clone_url
end
end
# frozen_string_literal: true
class Import::BitbucketServerProviderRepoEntity < Import::BitbucketProviderRepoEntity
expose :provider_link, override: true do |repo, options|
repo.browse_url
end
end
# frozen_string_literal: true
class Import::FogbugzProviderRepoEntity < Import::BaseProviderRepoEntity
include ImportHelper
expose :full_name, override: true do |repo|
repo.name
end
expose :sanitized_name, override: true do |repo|
repo.safe_name
end
expose :provider_link, override: true do |repo, options|
provider_project_link_url(options[:provider_url], repo.path)
end
end
# frozen_string_literal: true
class ProviderRepoEntity < Grape::Entity
class Import::GithubishProviderRepoEntity < Import::BaseProviderRepoEntity
include ImportHelper
expose :id
expose :full_name
expose :owner_name do |provider_repo, options|
owner_name(provider_repo, options[:provider])
end
expose :sanitized_name do |provider_repo|
expose :sanitized_name, override: true do |provider_repo|
sanitize_project_name(provider_repo[:name])
end
expose :provider_link do |provider_repo, options|
expose :provider_link, override: true do |provider_repo, options|
provider_project_link_url(options[:provider_url], provider_repo[:full_name])
end
......
# frozen_string_literal: true
class Import::GitlabProviderRepoEntity < Import::BaseProviderRepoEntity
expose :id, override: true do |repo|
repo["id"]
end
expose :full_name, override: true do |repo|
repo["path_with_namespace"]
end
expose :sanitized_name, override: true do |repo|
repo["path"]
end
expose :provider_link, override: true do |repo|
repo["web_url"]
end
end
# frozen_string_literal: true
class Import::ProviderRepoSerializer < BaseSerializer
def represent(repo, opts = {})
entity =
case opts[:provider]
when :fogbugz
Import::FogbugzProviderRepoEntity
when :github, :gitea
Import::GithubishProviderRepoEntity
when :bitbucket
Import::BitbucketProviderRepoEntity
when :bitbucket_server
Import::BitbucketServerProviderRepoEntity
when :gitlab
Import::GitlabProviderRepoEntity
else
raise NotImplementedError
end
super(repo, opts, entity)
end
end
# frozen_string_literal: true
class ProviderRepoSerializer < BaseSerializer
entity ProviderRepoEntity
end
- provider = local_assigns.fetch(:provider)
- extra_data = local_assigns.fetch(:extra_data, {})
- filterable = local_assigns.fetch(:filterable, true)
- provider_title = Gitlab::ImportSources.title(provider)
#import-projects-mount-element{ data: { provider: provider, provider_title: provider_title,
......@@ -6,4 +8,5 @@
ci_cd_only: has_ci_cd_only_params?.to_s,
repos_path: url_for([:status, :import, provider, format: :json]),
jobs_path: url_for([:realtime_changes, :import, provider, format: :json]),
import_path: url_for([:import, provider, format: :json]) } }
import_path: url_for([:import, provider, format: :json]),
filterable: filterable.to_s }.merge(extra_data) }
......@@ -5,7 +5,10 @@
%i.fa.fa-bitbucket
= _('Import projects from Bitbucket')
- if @repos.any?
- if Feature.enabled?(:new_import_ui)
= render 'import/githubish_status', provider: 'bitbucket'
- else
- if @repos.any?
%p.light
= _('Select projects you want to import.')
%p
......@@ -18,7 +21,7 @@
= _('Import all projects')
= icon('spinner spin', class: 'loading-icon')
.position-relative.ms-no-clear.d-flex.flex-fill.float-right.append-bottom-10
.position-relative.ms-no-clear.d-flex.flex-fill.float-right.append-bottom-10
= form_tag status_import_bitbucket_path, method: 'get' do
= text_field_tag :filter, @filter, class: 'form-control pr-5', placeholder: _('Filter projects'), size: 40, autofocus: true, 'aria-label': _('Search')
.position-absolute.position-top-0.d-flex.align-items-center.text-muted.position-right-0.h-100
......@@ -26,7 +29,7 @@
%button{ class: 'btn btn-transparent btn-secondary', 'aria-label': _('Search Button'), type: 'submit' }
%i{ class: 'fa fa-search', 'aria-hidden': true }
.table-responsive
.table-responsive
%table.table.import-jobs
%colgroup.import-jobs-from-col
%colgroup.import-jobs-to-col
......@@ -84,11 +87,11 @@
%td.import-actions-job-status
= label_tag _('Incompatible Project'), nil, class: 'label badge-danger'
- if @incompatible_repos.any?
- if @incompatible_repos.any?
%p
= _("One or more of your Bitbucket projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git.")
- link_to_git = link_to(_('Git'), 'https://www.atlassian.com/git/tutorials/migrating-overview')
- link_to_import_flow = link_to(_('import flow'), status_import_bitbucket_path)
= _("Please convert them to %{link_to_git}, and go through the %{link_to_import_flow} again.").html_safe % { link_to_git: link_to_git, link_to_import_flow: link_to_import_flow }
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } }
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } }
......@@ -5,7 +5,10 @@
%i.fa.fa-bitbucket-square
= _('Import projects from Bitbucket Server')
- if @repos.any?
- if Feature.enabled?(:new_import_ui)
= render 'import/githubish_status', provider: 'bitbucket_server', extra_data: { reconfigure_path: configure_import_bitbucket_server_path }
- else
- if @repos.any?
%p.light
= _('Select projects you want to import.')
.btn-group
......@@ -18,14 +21,14 @@
= _('Import all projects')
= icon('spinner spin', class: 'loading-icon')
.btn-group
.btn-group
= link_to('Reconfigure', configure_import_bitbucket_server_path, class: 'btn btn-primary', method: :post)
.input-btn-group.float-right
.input-btn-group.float-right
= form_tag status_import_bitbucket_server_path, :method => 'get' do
= text_field_tag :filter, sanitize(params[:filter]), class: 'form-control append-bottom-10', placeholder: _('Filter your projects by name'), size: 40, autoFocus: true
.table-responsive.prepend-top-10
.table-responsive.prepend-top-10
%table.table.import-jobs
%colgroup.import-jobs-from-col
%colgroup.import-jobs-to-col
......@@ -80,7 +83,7 @@
%td.import-actions-job-status
= label_tag 'Incompatible Project', nil, class: 'label badge-danger'
- if @incompatible_repos.any?
- if @incompatible_repos.any?
%p
One or more of your Bitbucket Server projects cannot be imported into GitLab
directly because they use Subversion or Mercurial for version control,
......@@ -90,6 +93,6 @@
= link_to 'import flow', status_import_bitbucket_server_path
again.
= paginate_without_count(@collection)
= paginate_without_count(@collection)
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_server_path}", import_path: "#{import_bitbucket_server_path}" } }
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_server_path}", import_path: "#{import_bitbucket_server_path}" } }
......@@ -4,7 +4,14 @@
%i.fa.fa-bug
= _('Import projects from FogBugz')
- if @repos.any?
- if Feature.enabled?(:new_import_ui)
%p.light
- link_to_customize = link_to('customize', new_user_map_import_fogbugz_path)
= _('Optionally, you can %{link_to_customize} how FogBugz email addresses and usernames are imported into GitLab.').html_safe % { link_to_customize: link_to_customize }
%hr
= render 'import/githubish_status', provider: 'fogbugz', filterable: false
- else
- if @repos.any?
%p.light
= _('Select projects you want to import.')
%p.light
......@@ -16,7 +23,7 @@
= _('Import all projects')
= icon("spinner spin", class: "loading-icon")
.table-responsive
.table-responsive
%table.table.import-jobs
%colgroup.import-jobs-from-col
%colgroup.import-jobs-to-col
......@@ -56,4 +63,4 @@
= _("Import")
= icon("spinner spin", class: "loading-icon")
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_fogbugz_path}", import_path: "#{import_fogbugz_path}" } }
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_fogbugz_path}", import_path: "#{import_fogbugz_path}" } }
......@@ -4,15 +4,18 @@
%i.fa.fa-heart
= _('Import projects from GitLab.com')
%p.light
- if Feature.enabled?(:new_import_ui)
= render 'import/githubish_status', provider: 'gitlab', filterable: false
- else
%p.light
= _('Select projects you want to import.')
%hr
%p
%hr
%p
= button_tag class: "btn btn-import btn-success js-import-all" do
= _('Import all projects')
= icon("spinner spin", class: "loading-icon")
.table-responsive
.table-responsive
%table.table.import-jobs
%colgroup.import-jobs-from-col
%colgroup.import-jobs-to-col
......@@ -52,4 +55,4 @@
= _('Import')
= icon("spinner spin", class: "loading-icon")
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_gitlab_path}", import_path: "#{import_gitlab_path}" } }
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_gitlab_path}", import_path: "#{import_gitlab_path}" } }
---
title: Introduce a feature flag for Vue-based UI for all import providers
merge_request: 33980
author:
type: added
......@@ -25,12 +25,14 @@ namespace :import do
get :status
get :callback
get :jobs
get :realtime_changes
end
resource :bitbucket, only: [:create], controller: :bitbucket do
get :status
get :callback
get :jobs
get :realtime_changes
end
resource :bitbucket_server, only: [:create, :new], controller: :bitbucket_server do
......@@ -38,6 +40,7 @@ namespace :import do
get :status
get :callback
get :jobs
get :realtime_changes
end
resource :google_code, only: [:create, :new], controller: :google_code do
......@@ -53,6 +56,7 @@ namespace :import do
get :status
post :callback
get :jobs
get :realtime_changes
get :new_user_map, path: :user_map
post :create_user_map, path: :user_map
......
......@@ -15313,6 +15313,9 @@ msgstr ""
msgid "One or more of you personal access tokens were revoked"
msgstr ""
msgid "One or more of your %{provider} projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git."
msgstr ""
msgid "One or more of your Bitbucket projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git."
msgstr ""
......@@ -16321,6 +16324,9 @@ msgstr ""
msgid "Please complete your profile with email address"
msgstr ""
msgid "Please convert %{linkStart}them to Git%{linkEnd}, and go through the %{linkToImportFlow} again."
msgstr ""
msgid "Please convert them to %{link_to_git}, and go through the %{link_to_import_flow} again."
msgstr ""
......@@ -18229,6 +18235,9 @@ msgstr ""
msgid "Recipe"
msgstr ""
msgid "Reconfigure"
msgstr ""
msgid "Recover hidden stage"
msgstr ""
......
......@@ -56,8 +56,42 @@ RSpec.describe Import::BitbucketController do
describe "GET status" do
before do
@repo = double(slug: 'vim', owner: 'asd', full_name: 'asd/vim', "valid?" => true)
@repo = double(name: 'vim', slug: 'vim', owner: 'asd', full_name: 'asd/vim', clone_url: 'http://test.host/demo/url.git', 'valid?' => true)
@invalid_repo = double(name: 'mercurialrepo', slug: 'mercurialrepo', owner: 'asd', full_name: 'asd/mercurialrepo', clone_url: 'http://test.host/demo/mercurialrepo.git', 'valid?' => false)
assign_session_tokens
stub_feature_flags(new_import_ui: false)
end
it_behaves_like 'import controller with new_import_ui feature flag' do
before do
allow(controller).to receive(:provider_url).and_return('http://demobitbucket.org')
end
let(:repo) { @repo }
let(:repo_id) { @repo.full_name }
let(:import_source) { @repo.full_name }
let(:provider_name) { 'bitbucket' }
let(:client_repos_field) { :repos }
end
context 'with new_import_ui feature flag enabled' do
before do
stub_feature_flags(new_import_ui: true)
allow(controller).to receive(:provider_url).and_return('http://demobitbucket.org')
end
it 'returns invalid repos' do
allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo, @invalid_repo])
get :status, format: :json
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['incompatible_repos'].length).to eq(1)
expect(json_response.dig("incompatible_repos", 0, "id")).to eq(@invalid_repo.full_name)
expect(json_response['provider_repos'].length).to eq(1)
expect(json_response.dig("provider_repos", 0, "id")).to eq(@repo.full_name)
end
end
it "assigns variables" do
......
......@@ -33,7 +33,7 @@ RSpec.describe Import::BitbucketServerController do
let(:project_name) { "my-project_123" }
before do
allow(controller).to receive(:bitbucket_client).and_return(client)
allow(controller).to receive(:client).and_return(client)
repo = double(name: project_name)
allow(client).to receive(:repo).with(project_key, repo_slug).and_return(repo)
assign_session_tokens
......@@ -139,12 +139,39 @@ RSpec.describe Import::BitbucketServerController do
let(:repos) { instance_double(BitbucketServer::Collection) }
before do
allow(controller).to receive(:bitbucket_client).and_return(client)
allow(controller).to receive(:client).and_return(client)
@repo = double(slug: 'vim', project_key: 'asd', full_name: 'asd/vim', "valid?" => true, project_name: 'asd', browse_url: 'http://test', name: 'vim')
@invalid_repo = double(slug: 'invalid', project_key: 'foobar', full_name: 'asd/foobar', "valid?" => false, browse_url: 'http://bad-repo')
@invalid_repo = double(slug: 'invalid', project_key: 'foobar', full_name: 'asd/foobar', "valid?" => false, browse_url: 'http://bad-repo', name: 'invalid')
@created_repo = double(slug: 'created', project_key: 'existing', full_name: 'group/created', "valid?" => true, browse_url: 'http://existing')
assign_session_tokens
stub_feature_flags(new_import_ui: false)
end
context 'with new_import_ui feature flag enabled' do
before do
stub_feature_flags(new_import_ui: true)
end
it 'returns invalid repos' do
allow(client).to receive(:repos).with(filter: nil, limit: 25, page_offset: 0).and_return([@repo, @invalid_repo])
get :status, format: :json
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['incompatible_repos'].length).to eq(1)
expect(json_response.dig("incompatible_repos", 0, "id")).to eq(@invalid_repo.full_name)
expect(json_response['provider_repos'].length).to eq(1)
expect(json_response.dig("provider_repos", 0, "id")).to eq(@repo.full_name)
end
end
it_behaves_like 'import controller with new_import_ui feature flag' do
let(:repo) { @repo }
let(:repo_id) { @repo.full_name }
let(:import_source) { @repo.browse_url }
let(:provider_name) { 'bitbucket_server' }
let(:client_repos_field) { :repos }
end
it 'assigns repository categories' do
......
......@@ -80,8 +80,17 @@ RSpec.describe Import::FogbugzController do
describe 'GET status' do
before do
@repo = OpenStruct.new(name: 'vim')
@repo = OpenStruct.new(id: 'demo', name: 'vim')
stub_client(valid?: true)
stub_feature_flags(new_import_ui: false)
end
it_behaves_like 'import controller with new_import_ui feature flag' do
let(:repo) { @repo }
let(:repo_id) { @repo.id }
let(:import_source) { @repo.name }
let(:provider_name) { 'fogbugz' }
let(:client_repos_field) { :repos }
end
it 'assigns variables' do
......
......@@ -34,8 +34,17 @@ RSpec.describe Import::GitlabController do
describe "GET status" do
before do
@repo = OpenStruct.new(path: 'vim', path_with_namespace: 'asd/vim')
@repo = OpenStruct.new(id: 1, path: 'vim', path_with_namespace: 'asd/vim', web_url: 'https://gitlab.com/asd/vim')
assign_session_token
stub_feature_flags(new_import_ui: false)
end
it_behaves_like 'import controller with new_import_ui feature flag' do
let(:repo) { @repo }
let(:repo_id) { @repo.id }
let(:import_source) { @repo.path_with_namespace }
let(:provider_name) { 'gitlab' }
let(:client_repos_field) { :projects }
end
it "assigns variables" do
......
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
import BitbucketStatusTable from '~/import_projects/components/bitbucket_status_table.vue';
import ImportProjectsTable from '~/import_projects/components/import_projects_table.vue';
const ImportProjectsTableStub = {
name: 'ImportProjectsTable',
template:
'<div><slot name="incompatible-repos-warning"></slot><slot name="actions"></slot></div>',
};
describe('BitbucketStatusTable', () => {
let wrapper;
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
function createComponent(propsData, importProjectsTableStub = true, slots) {
wrapper = shallowMount(BitbucketStatusTable, {
propsData,
stubs: {
ImportProjectsTable: importProjectsTableStub,
},
slots,
});
}
it('renders import table component', () => {
createComponent({ providerTitle: 'Test' });
expect(wrapper.contains(ImportProjectsTable)).toBe(true);
});
it('passes alert in incompatible-repos-warning slot', () => {
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
expect(wrapper.find(GlAlert).exists()).toBe(true);
});
it('passes actions slot to import project table component', () => {
const actionsSlotContent = 'DEMO';
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub, {
actions: actionsSlotContent,
});
expect(wrapper.find(ImportProjectsTable).text()).toBe(actionsSlotContent);
});
it('dismisses alert when requested', async () => {
createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
wrapper.find(GlAlert).vm.$emit('dismiss');
await nextTick();
expect(wrapper.find(GlAlert).exists()).toBe(false);
});
});
......@@ -16,6 +16,9 @@ jest.mock('~/import_projects/event_hub', () => ({
describe('ImportProjectsTable', () => {
let wrapper;
const findFilterField = () =>
wrapper.find('input[data-qa-selector="githubish_import_filter_field"]');
const providerTitle = 'THE PROVIDER';
const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
const importedProject = {
......@@ -32,7 +35,12 @@ describe('ImportProjectsTable', () => {
.filter(w => w.props().variant === 'success')
.at(0);
function createComponent({ state: initialState, getters: customGetters, slots } = {}) {
function createComponent({
state: initialState,
getters: customGetters,
slots,
filterable,
} = {}) {
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -57,6 +65,7 @@ describe('ImportProjectsTable', () => {
store,
propsData: {
providerTitle,
filterable,
},
slots,
});
......@@ -159,9 +168,14 @@ describe('ImportProjectsTable', () => {
expect(findImportAllButton().props().loading).toBe(true);
});
it('renders filtering input field', () => {
it('renders filtering input field by default', () => {
createComponent();
expect(wrapper.contains('input[data-qa-selector="githubish_import_filter_field"]')).toBe(true);
expect(findFilterField().exists()).toBe(true);
});
it('does not render filtering input field when filterable is false', () => {
createComponent({ filterable: false });
expect(findFilterField().exists()).toBe(false);
});
it.each`
......
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import BitbucketServerStatusTable from '~/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue';
import BitbucketStatusTable from '~/import_projects/components/bitbucket_status_table.vue';
const BitbucketStatusTableStub = {
name: 'BitbucketStatusTable',
template: '<div><slot name="actions"></slot></div>',
};
describe('BitbucketServerStatusTable', () => {
let wrapper;
const findReconfigureButton = () =>
wrapper
.findAll(GlButton)
.filter(w => w.props().variant === 'info')
.at(0);
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
function createComponent(bitbucketStatusTableStub = true) {
wrapper = shallowMount(BitbucketServerStatusTable, {
propsData: { providerTitle: 'Test', reconfigurePath: '/reconfigure' },
stubs: {
BitbucketStatusTable: bitbucketStatusTableStub,
},
});
}
it('renders bitbucket status table component', () => {
createComponent();
expect(wrapper.contains(BitbucketStatusTable)).toBe(true);
});
it('renders Reconfigure button', async () => {
createComponent(BitbucketStatusTableStub);
expect(findReconfigureButton().attributes().href).toBe('/reconfigure');
expect(findReconfigureButton().text()).toBe('Reconfigure');
});
});
# frozen_string_literal: true
require 'spec_helper'
describe Import::BitbucketProviderRepoEntity do
let(:repo_data) do
{
'name' => 'repo_name',
'full_name' => 'owner/repo_name',
'links' => {
'clone' => [
{
'href' => 'https://bitbucket.org/owner/repo_name',
'name' => 'https'
}
]
}
}
end
let(:repo) { Bitbucket::Representation::Repo.new(repo_data) }
subject { described_class.new(repo).as_json }
it_behaves_like 'exposes required fields for import entity' do
let(:expected_values) do
{
id: 'owner/repo_name',
full_name: 'owner/repo_name',
sanitized_name: 'repo_name',
provider_link: 'https://bitbucket.org/owner/repo_name'
}
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Import::BitbucketServerProviderRepoEntity do
let(:repo_data) do
{
'name' => 'test',
'project' => {
'name' => 'demo'
},
'links' => {
'self' => [
{
'href' => 'http://local.bitbucket.server/demo/test.git',
'name' => 'http'
}
]
}
}
end
let(:repo) { BitbucketServer::Representation::Repo.new(repo_data) }
subject { described_class.new(repo).as_json }
it_behaves_like 'exposes required fields for import entity' do
let(:expected_values) do
{
id: 'demo/test',
full_name: 'demo/test',
sanitized_name: 'test',
provider_link: 'http://local.bitbucket.server/demo/test.git'
}
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Import::FogbugzProviderRepoEntity do
let(:provider_url) { 'https://demo.fogbugz.com/' }
let(:repo_data) do
{
'ixProject' => 'foo',
'sProject' => 'demo'
}
end
let(:repo) { Gitlab::FogbugzImport::Repository.new(repo_data) }
subject { described_class.represent(repo, { provider_url: provider_url }).as_json }
it_behaves_like 'exposes required fields for import entity' do
let(:expected_values) do
{
id: 'foo',
full_name: 'demo',
sanitized_name: 'demo',
provider_link: 'https://demo.fogbugz.com/demo'
}
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Import::GithubishProviderRepoEntity do
let(:provider_url) { 'https://github.com/' }
let(:repo) do
{
id: 1,
full_name: 'full/name',
name: 'name'
}
end
subject { described_class.represent(repo, { provider_url: provider_url }).as_json }
it_behaves_like 'exposes required fields for import entity' do
let(:expected_values) do
{
id: 1,
full_name: 'full/name',
sanitized_name: 'name',
provider_link: 'https://github.com/full/name'
}
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Import::GitlabProviderRepoEntity do
let(:repo_data) do
{
'id' => 1,
'path_with_namespace' => 'demo/test',
'path' => 'test',
'web_url' => 'https://gitlab.com/demo/test'
}
end
subject { described_class.new(repo_data).as_json }
it_behaves_like 'exposes required fields for import entity' do
let(:expected_values) do
{
id: 1,
full_name: 'demo/test',
sanitized_name: 'test',
provider_link: 'https://gitlab.com/demo/test'
}
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Import::ProviderRepoSerializer do
using RSpec::Parameterized::TableSyntax
describe '#represent' do
where(:provider, :class_name) do
:github | 'Import::GithubishProviderRepoEntity'
:gitea | 'Import::GithubishProviderRepoEntity'
:bitbucket | 'Import::BitbucketProviderRepoEntity'
:bitbucket_server | 'Import::BitbucketServerProviderRepoEntity'
:fogbugz | 'Import::FogbugzProviderRepoEntity'
end
with_them do
it 'uses correct entity class' do
opts = { provider: provider }
expect(class_name.constantize).to receive(:represent)
described_class.new.represent({}, opts)
end
end
it 'raises an error if invalid provider supplied' do
expect { described_class.new.represent({}, { provider: :invalid })}.to raise_error { NotImplementedError }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ProviderRepoEntity do
include ImportHelper
let(:provider_repo) { { id: 1, full_name: 'full/name', name: 'name', owner: { login: 'owner' } } }
let(:provider) { :github }
let(:provider_url) { 'https://github.com' }
let(:entity) { described_class.represent(provider_repo, provider: provider, provider_url: provider_url) }
describe '#as_json' do
subject { entity.as_json }
it 'includes required fields' do
expect(subject[:id]).to eq(provider_repo[:id])
expect(subject[:full_name]).to eq(provider_repo[:full_name])
expect(subject[:owner_name]).to eq(provider_repo[:owner][:login])
expect(subject[:sanitized_name]).to eq(sanitize_project_name(provider_repo[:name]))
expect(subject[:provider_link]).to eq(provider_project_link_url(provider_url, provider_repo[:full_name]))
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'import controller with new_import_ui feature flag' do
include ImportSpecHelper
context 'with new_import_ui feature flag enabled' do
let(:group) { create(:group) }
before do
stub_feature_flags(new_import_ui: true)
group.add_owner(user)
end
it "returns variables for json request" do
project = create(:project, import_type: provider_name, creator_id: user.id)
stub_client(client_repos_field => [repo])
get :status, format: :json
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id)
expect(json_response.dig("namespaces", 0, "id")).to eq(group.id)
end
it "does not show already added project" do
project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source)
stub_client(client_repos_field => [repo])
get :status, format: :json
expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
expect(json_response.dig("provider_repos")).to eq([])
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'exposes required fields for import entity' do
describe 'exposes required fields' do
it 'correctly exposes id' do
expect(subject[:id]).to eql(expected_values[:id])
end
it 'correctly exposes full name' do
expect(subject[:full_name]).to eql(expected_values[:full_name])
end
it 'correctly exposes sanitized name' do
expect(subject[:sanitized_name]).to eql(expected_values[:sanitized_name])
end
it 'correctly exposes provider link' do
expect(subject[:provider_link]).to eql(expected_values[:provider_link])
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment