diff options
Diffstat (limited to 'app')
37 files changed, 955 insertions, 311 deletions
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 0fb06dab635..6b9748bb725 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -12,6 +12,8 @@ import itemStats from './item_stats.vue'; import itemStatsValue from './item_stats_value.vue'; import itemActions from './item_actions.vue'; +import { showLearnGitLabGroupItemPopover } from '~/onboarding_issues'; + export default { directives: { tooltip, @@ -73,6 +75,11 @@ export default { return GROUP_VISIBILITY_TYPE[this.group.visibility]; }, }, + mounted() { + if (this.group.name === 'Learn GitLab') { + showLearnGitLabGroupItemPopover(this.group.id); + } + }, methods: { onClickRowGroup(e) { const NO_EXPAND_CLS = 'no-expand'; diff --git a/app/assets/javascripts/import_projects/components/bitbucket_status_table.vue b/app/assets/javascripts/import_projects/components/bitbucket_status_table.vue new file mode 100644 index 00000000000..1a9974db727 --- /dev/null +++ b/app/assets/javascripts/import_projects/components/bitbucket_status_table.vue @@ -0,0 +1,74 @@ +<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> diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue index f2ed16ba59f..6a467fb8c6a 100644 --- a/app/assets/javascripts/import_projects/components/import_projects_table.vue +++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue @@ -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" diff --git a/app/assets/javascripts/import_projects/index.js b/app/assets/javascripts/import_projects/index.js index 49d38b56e40..68ba04aa9dd 100644 --- a/app/assets/javascripts/import_projects/index.js +++ b/app/assets/javascripts/import_projects/index.js @@ -30,6 +30,7 @@ export function initStoreFromElement(element) { export function initPropsFromElement(element) { return { providerTitle: element.dataset.providerTitle, + filterable: parseBoolean(element.dataset.filterable), }; } diff --git a/app/assets/javascripts/import_projects/store/actions.js b/app/assets/javascripts/import_projects/store/actions.js index 6cf71126882..2422a1ed2e4 100644 --- a/app/assets/javascripts/import_projects/store/actions.js +++ b/app/assets/javascripts/import_projects/store/actions.js @@ -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,14 +37,18 @@ export const fetchRepos = ({ state, dispatch, commit }) => { commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })), ) .then(() => dispatch('fetchJobs')) - .catch(() => { - createFlash( - sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), { - provider, - }), - ); - - commit(types.RECEIVE_REPOS_ERROR); + .catch(e => { + if (hasRedirectInError(e)) { + redirectToUrlInError(e); + } else { + createFlash( + sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), { + provider, + }), + ); + + 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 }, }); diff --git a/app/assets/javascripts/onboarding_issues/index.js b/app/assets/javascripts/onboarding_issues/index.js new file mode 100644 index 00000000000..afc3b0c2685 --- /dev/null +++ b/app/assets/javascripts/onboarding_issues/index.js @@ -0,0 +1,118 @@ +import $ from 'jquery'; +import { parseBoolean, getCookie, setCookie, removeCookie } from '~/lib/utils/common_utils'; +import { __ } from '~/locale'; + +const COOKIE_NAME = 'onboarding_issues_settings'; + +const POPOVER_LOCATIONS = { + GROUPS_SHOW: 'groups#show', + PROJECTS_SHOW: 'projects#show', + ISSUES_INDEX: 'issues#index', +}; + +const removeLearnGitLabCookie = () => { + removeCookie(COOKIE_NAME); +}; + +function disposePopover(event) { + event.preventDefault(); + this.popover('dispose'); + removeLearnGitLabCookie(); +} + +const showPopover = (el, path, footer, options) => { + // Cookie value looks like `{ 'groups#show': true, 'projects#show': true, 'issues#index': true }`. When it doesn't exist, don't show the popover. + const cookie = getCookie(COOKIE_NAME); + if (!cookie) return; + + // When the popover action has already been taken, don't show the popover. + const settings = JSON.parse(cookie); + if (!parseBoolean(settings[path])) return; + + const defaultOptions = { + boundary: 'window', + html: true, + placement: 'top', + template: `<div class="popover blue learn-gitlab d-none d-xl-block" role="tooltip"> + <div class="arrow"></div> + <div class="close cursor-pointer gl-font-base text-white gl-opacity-10 p-2">✕</div> + <div class="popover-body gl-font-base gl-line-height-20 pb-0 px-3"></div> + <div class="bold text-right text-white p-2">${footer}</div> + </div>`, + }; + + // When one of the popovers is dismissed, remove the cookie. + const closeButton = () => document.querySelector('.learn-gitlab.popover .close'); + + // We still have to use jQuery, since Bootstrap's Popover is based on jQuery. + const jQueryEl = $(el); + const clickCloseButton = disposePopover.bind(jQueryEl); + + jQueryEl + .popover({ ...defaultOptions, ...options }) + .on('inserted.bs.popover', () => closeButton().addEventListener('click', clickCloseButton)) + .on('hide.bs.dropdown', () => closeButton().removeEventListener('click', clickCloseButton)) + .popover('show'); + + // The previous popover actions have been taken, don't show those popovers anymore. + Object.keys(settings).forEach(pathSetting => { + if (path !== pathSetting) { + settings[pathSetting] = false; + } else { + setCookie(COOKIE_NAME, settings); + } + }); + + // The final popover action will be taken on click, we then no longer need the cookie. + if (path === POPOVER_LOCATIONS.ISSUES_INDEX) { + el.addEventListener('click', removeLearnGitLabCookie); + } +}; + +export const showLearnGitLabGroupItemPopover = id => { + const el = document.querySelector(`#group-${id} .group-text a`); + + if (!el) return; + + const options = { + content: __( + 'Here are all your projects in your group, including the one you just created. To start, let’s take a look at your personalized learning project which will help you learn about GitLab at your own pace.', + ), + }; + + showPopover(el, POPOVER_LOCATIONS.GROUPS_SHOW, '1 / 2', options); +}; + +export const showLearnGitLabProjectPopover = () => { + // Do not show a popover if we are not viewing the 'Learn GitLab' project. + if (!window.location.pathname.includes('learn-gitlab')) return; + + const el = document.querySelector('a.shortcuts-issues'); + + if (!el) return; + + const options = { + content: __( + 'Go to <strong>Issues</strong> > <strong>Boards</strong> to access your personalized learning issue board.', + ), + }; + + showPopover(el, POPOVER_LOCATIONS.PROJECTS_SHOW, '2 / 2', options); +}; + +export const showLearnGitLabIssuesPopover = () => { + // Do not show a popover if we are not viewing the 'Learn GitLab' project. + if (!window.location.pathname.includes('learn-gitlab')) return; + + const el = document.querySelector('a[data-qa-selector="issue_boards_link"]'); + + if (!el) return; + + const options = { + content: __( + 'Go to <strong>Issues</strong> > <strong>Boards</strong> to access your personalized learning issue board.', + ), + }; + + showPopover(el, POPOVER_LOCATIONS.ISSUES_INDEX, '2 / 2', options); +}; diff --git a/app/assets/javascripts/pages/import/bitbucket/status/index.js b/app/assets/javascripts/pages/import/bitbucket/status/index.js new file mode 100644 index 00000000000..52b5adb79d1 --- /dev/null +++ b/app/assets/javascripts/pages/import/bitbucket/status/index.js @@ -0,0 +1,19 @@ +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 }); + }, + }); +}); diff --git a/app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue b/app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue new file mode 100644 index 00000000000..e01c7b80e1a --- /dev/null +++ b/app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue @@ -0,0 +1,30 @@ +<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> diff --git a/app/assets/javascripts/pages/import/bitbucket_server/status/index.js b/app/assets/javascripts/pages/import/bitbucket_server/status/index.js new file mode 100644 index 00000000000..88455c9b7b9 --- /dev/null +++ b/app/assets/javascripts/pages/import/bitbucket_server/status/index.js @@ -0,0 +1,20 @@ +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 } }); + }, + }); +}); diff --git a/app/assets/javascripts/pages/import/fogbugz/status/index.js b/app/assets/javascripts/pages/import/fogbugz/status/index.js new file mode 100644 index 00000000000..dcd84f0faf9 --- /dev/null +++ b/app/assets/javascripts/pages/import/fogbugz/status/index.js @@ -0,0 +1,7 @@ +import mountImportProjectsTable from '~/import_projects'; + +document.addEventListener('DOMContentLoaded', () => { + const mountElement = document.getElementById('import-projects-mount-element'); + + mountImportProjectsTable(mountElement); +}); diff --git a/app/assets/javascripts/pages/import/gitlab/status/index.js b/app/assets/javascripts/pages/import/gitlab/status/index.js new file mode 100644 index 00000000000..dcd84f0faf9 --- /dev/null +++ b/app/assets/javascripts/pages/import/gitlab/status/index.js @@ -0,0 +1,7 @@ +import mountImportProjectsTable from '~/import_projects'; + +document.addEventListener('DOMContentLoaded', () => { + const mountElement = document.getElementById('import-projects-mount-element'); + + mountImportProjectsTable(mountElement); +}); diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js index e8e0cda2139..a66b665d152 100644 --- a/app/assets/javascripts/pages/projects/issues/index/index.js +++ b/app/assets/javascripts/pages/projects/issues/index/index.js @@ -9,6 +9,7 @@ import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; import initIssuablesList from '~/issuables_list'; import initManualOrdering from '~/manual_ordering'; +import { showLearnGitLabIssuesPopover } from '~/onboarding_issues'; document.addEventListener('DOMContentLoaded', () => { IssuableFilteredSearchTokenKeys.addExtraTokensForIssues(); @@ -24,4 +25,5 @@ document.addEventListener('DOMContentLoaded', () => { initManualOrdering(); initIssuablesList(); + showLearnGitLabIssuesPopover(); }); diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index f29dbd92c46..3c44053e2b2 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -15,6 +15,7 @@ import leaveByUrl from '~/namespaces/leave_by_url'; import Star from '../../../star'; import notificationsDropdown from '../../../notifications_dropdown'; import initNamespaceStorageLimitAlert from '~/namespace_storage_limit_alert'; +import { showLearnGitLabProjectPopover } from '~/onboarding_issues'; document.addEventListener('DOMContentLoaded', () => { initReadMore(); @@ -59,4 +60,6 @@ document.addEventListener('DOMContentLoaded', () => { throw e; }); } + + showLearnGitLabProjectPopover(); }); diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 893b32606f8..0e05f4a4622 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -134,7 +134,7 @@ export default { addMultipleToDiscussionWarning() { return sprintf( __( - '%{icon}You are about to add %{usersTag} people to the discussion. Proceed with caution.', + '%{icon}You are about to add %{usersTag} people to the discussion. They will all receive a notification.', ), { icon: '<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>', diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 44b95c40649..80218d25a1a 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -716,8 +716,8 @@ $accepting-mr-label-color: #69d100; /* * Issues */ -$issues-today-bg: #f3fff2; -$issues-today-border: #e1e8d5; +$issues-today-bg: #f3fff2 !default; +$issues-today-border: #e1e8d5 !default; $compare-display-color: #888; /* diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss index aa145f28093..23487a2d5f7 100644 --- a/app/assets/stylesheets/themes/_dark.scss +++ b/app/assets/stylesheets/themes/_dark.scss @@ -112,6 +112,9 @@ $popover-arrow-outer-color: $gray-800; $secondary: $gray-600; +$issues-today-bg: #333838; +$issues-today-border: #333a40; + .gl-label { filter: brightness(0.9) contrast(1.1); } diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 04919a4b9d0..afdea4f7c9d 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -1,10 +1,86 @@ # 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 diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index c37e799de62..4886aeb5e3f 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -1,6 +1,8 @@ # 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 diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb index 5fb7b5dccc5..9aa8110257d 100644 --- a/app/controllers/import/bitbucket_server_controller.rb +++ b/app/controllers/import/bitbucket_server_controller.rb @@ -1,12 +1,16 @@ # 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 diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb index 4fb6efde7ff..91779a5d6cc 100644 --- a/app/controllers/import/fogbugz_controller.rb +++ b/app/controllers/import/fogbugz_controller.rb @@ -1,6 +1,8 @@ # 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 diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 4e8ceae75bd..097edcd6075 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -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 diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index 5ec8e9e6fc5..a95a67e208c 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -1,6 +1,8 @@ # 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 diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb index fb4fbe57130..5905ec5cb76 100644 --- a/app/serializers/diffs_entity.rb +++ b/app/serializers/diffs_entity.rb @@ -69,11 +69,9 @@ class DiffsEntity < Grape::Entity expose :diff_files do |diffs, options| submodule_links = Gitlab::SubmoduleLinks.new(merge_request.project.repository) - code_navigation_path = - Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs.head_sha) DiffFileEntity.represent(diffs.diff_files, - options.merge(submodule_links: submodule_links, code_navigation_path: code_navigation_path)) + options.merge(submodule_links: submodule_links, code_navigation_path: code_navigation_path(diffs))) end expose :merge_request_diffs, using: MergeRequestDiffEntity, if: -> (_, options) { options[:merge_request_diffs]&.any? } do |diffs| @@ -81,7 +79,7 @@ class DiffsEntity < Grape::Entity end expose :definition_path_prefix, if: -> (diff_file) { Feature.enabled?(:code_navigation, merge_request.project) } do |diffs| - project_blob_path(merge_request.project, diffs.diff_refs.head_sha) + project_blob_path(merge_request.project, diffs.diff_refs&.head_sha) end def merge_request @@ -90,6 +88,12 @@ class DiffsEntity < Grape::Entity private + def code_navigation_path(diffs) + return unless Feature.enabled?(:code_navigation, merge_request.project) + + Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs&.head_sha) + end + def commit_ids @commit_ids ||= merge_request.recent_commits.map(&:id) end diff --git a/app/serializers/import/base_provider_repo_entity.rb b/app/serializers/import/base_provider_repo_entity.rb new file mode 100644 index 00000000000..88a831a1686 --- /dev/null +++ b/app/serializers/import/base_provider_repo_entity.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class Import::BaseProviderRepoEntity < Grape::Entity + expose :id + expose :full_name + expose :sanitized_name + expose :provider_link +end diff --git a/app/serializers/import/bitbucket_provider_repo_entity.rb b/app/serializers/import/bitbucket_provider_repo_entity.rb new file mode 100644 index 00000000000..e8c647d407e --- /dev/null +++ b/app/serializers/import/bitbucket_provider_repo_entity.rb @@ -0,0 +1,15 @@ +# 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 diff --git a/app/serializers/import/bitbucket_server_provider_repo_entity.rb b/app/serializers/import/bitbucket_server_provider_repo_entity.rb new file mode 100644 index 00000000000..d818cac46cd --- /dev/null +++ b/app/serializers/import/bitbucket_server_provider_repo_entity.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Import::BitbucketServerProviderRepoEntity < Import::BitbucketProviderRepoEntity + expose :provider_link, override: true do |repo, options| + repo.browse_url + end +end diff --git a/app/serializers/import/fogbugz_provider_repo_entity.rb b/app/serializers/import/fogbugz_provider_repo_entity.rb new file mode 100644 index 00000000000..d420de141e1 --- /dev/null +++ b/app/serializers/import/fogbugz_provider_repo_entity.rb @@ -0,0 +1,17 @@ +# 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 diff --git a/app/serializers/provider_repo_entity.rb b/app/serializers/import/githubish_provider_repo_entity.rb index d70aaa91324..d3e323053f9 100644 --- a/app/serializers/provider_repo_entity.rb +++ b/app/serializers/import/githubish_provider_repo_entity.rb @@ -1,19 +1,13 @@ # 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 diff --git a/app/serializers/import/gitlab_provider_repo_entity.rb b/app/serializers/import/gitlab_provider_repo_entity.rb new file mode 100644 index 00000000000..5fecd0a1cd3 --- /dev/null +++ b/app/serializers/import/gitlab_provider_repo_entity.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/app/serializers/import/provider_repo_serializer.rb b/app/serializers/import/provider_repo_serializer.rb new file mode 100644 index 00000000000..5a9549d79aa --- /dev/null +++ b/app/serializers/import/provider_repo_serializer.rb @@ -0,0 +1,23 @@ +# 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 diff --git a/app/serializers/paginated_diff_entity.rb b/app/serializers/paginated_diff_entity.rb index a31c9d70d4b..0ab077d9377 100644 --- a/app/serializers/paginated_diff_entity.rb +++ b/app/serializers/paginated_diff_entity.rb @@ -10,11 +10,9 @@ class PaginatedDiffEntity < Grape::Entity expose :diff_files do |diffs, options| submodule_links = Gitlab::SubmoduleLinks.new(merge_request.project.repository) - code_navigation_path = - Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs.head_sha) DiffFileEntity.represent(diffs.diff_files, - options.merge(submodule_links: submodule_links, code_navigation_path: code_navigation_path)) + options.merge(submodule_links: submodule_links, code_navigation_path: code_navigation_path(diffs))) end expose :pagination do @@ -38,6 +36,12 @@ class PaginatedDiffEntity < Grape::Entity private + def code_navigation_path(diffs) + return unless Feature.enabled?(:code_navigation, merge_request.project) + + Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs&.head_sha) + end + %i[current_page next_page total_pages].each do |method| define_method method do pagination_data[method] diff --git a/app/serializers/provider_repo_serializer.rb b/app/serializers/provider_repo_serializer.rb deleted file mode 100644 index 8a73f6fe6df..00000000000 --- a/app/serializers/provider_repo_serializer.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class ProviderRepoSerializer < BaseSerializer - entity ProviderRepoEntity -end diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml index b05c039c85c..9bf1f0c61bb 100644 --- a/app/views/import/_githubish_status.html.haml +++ b/app/views/import/_githubish_status.html.haml @@ -1,4 +1,6 @@ - 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) } diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 7399ff937ce..d405acef75c 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -5,90 +5,93 @@ %i.fa.fa-bitbucket = _('Import projects from Bitbucket') -- if @repos.any? - %p.light - = _('Select projects you want to import.') - %p - - if @incompatible_repos.any? - = button_tag class: 'btn btn-import btn-success js-import-all' do - = _('Import all compatible projects') - = icon('spinner spin', class: 'loading-icon') - - else - = button_tag class: 'btn btn-import btn-success js-import-all' do - = _('Import all projects') - = icon('spinner spin', class: 'loading-icon') +- 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 + - if @incompatible_repos.any? + = button_tag class: 'btn btn-import btn-success js-import-all' do + = _('Import all compatible projects') + = icon('spinner spin', class: 'loading-icon') + - else + = button_tag class: 'btn btn-import btn-success js-import-all' do + = _('Import all projects') + = icon('spinner spin', class: 'loading-icon') -.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 - .border-left - %button{ class: 'btn btn-transparent btn-secondary', 'aria-label': _('Search Button'), type: 'submit' } - %i{ class: 'fa fa-search', 'aria-hidden': true } + .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 + .border-left + %button{ class: 'btn btn-transparent btn-secondary', 'aria-label': _('Search Button'), type: 'submit' } + %i{ class: 'fa fa-search', 'aria-hidden': true } -.table-responsive - %table.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th= _('From Bitbucket') - %th= _('To GitLab') - %th= _('Status') - %tbody - - @already_added_projects.each do |project| - %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } - %td - = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank', rel: 'noopener noreferrer' - %td - = link_to project.full_path, [project.namespace.becomes(Namespace), project] - %td.job-status - - case project.import_status - - when 'finished' - %span - %i.fa.fa-check - = _('done') - - when 'started' - %i.fa.fa-spinner.fa-spin - = _('started') - - else - = project.human_import_status_name + .table-responsive + %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col + %thead + %tr + %th= _('From Bitbucket') + %th= _('To GitLab') + %th= _('Status') + %tbody + - @already_added_projects.each do |project| + %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } + %td + = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank', rel: 'noopener noreferrer' + %td + = link_to project.full_path, [project.namespace.becomes(Namespace), project] + %td.job-status + - case project.import_status + - when 'finished' + %span + %i.fa.fa-check + = _('done') + - when 'started' + %i.fa.fa-spinner.fa-spin + = _('started') + - else + = project.human_import_status_name - - @repos.each do |repo| - %tr{ id: "repo_#{repo.owner}___#{repo.slug}" } - %td - = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer' - %td.import-target - %fieldset.row - .input-group - .project-path.input-group-prepend - - if current_user.can_select_namespace? - - selected = params[:namespace_id] || :current_user - - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner, path: repo.owner) } : {} - = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } - - else - = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true - %span.input-group-prepend - .input-group-text / - = text_field_tag :path, sanitize_project_name(repo.slug), class: "input-mini form-control", tabindex: 2, autofocus: true, required: true - %td.import-actions.job-status - = button_tag class: 'btn btn-import js-add-to-import' do - = _('Import') - = icon('spinner spin', class: 'loading-icon') - - @incompatible_repos.each do |repo| - %tr{ id: "repo_#{repo.owner}___#{repo.slug}" } - %td - = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer' - %td.import-target - %td.import-actions-job-status - = label_tag _('Incompatible Project'), nil, class: 'label badge-danger' + - @repos.each do |repo| + %tr{ id: "repo_#{repo.owner}___#{repo.slug}" } + %td + = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer' + %td.import-target + %fieldset.row + .input-group + .project-path.input-group-prepend + - if current_user.can_select_namespace? + - selected = params[:namespace_id] || :current_user + - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner, path: repo.owner) } : {} + = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } + - else + = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true + %span.input-group-prepend + .input-group-text / + = text_field_tag :path, sanitize_project_name(repo.slug), class: "input-mini form-control", tabindex: 2, autofocus: true, required: true + %td.import-actions.job-status + = button_tag class: 'btn btn-import js-add-to-import' do + = _('Import') + = icon('spinner spin', class: 'loading-icon') + - @incompatible_repos.each do |repo| + %tr{ id: "repo_#{repo.owner}___#{repo.slug}" } + %td + = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer' + %td.import-target + %td.import-actions-job-status + = label_tag _('Incompatible Project'), nil, class: 'label badge-danger' -- 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 } + - 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}" } } diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml index 1aaf5883bf4..7523b8f7b1c 100644 --- a/app/views/import/bitbucket_server/status.html.haml +++ b/app/views/import/bitbucket_server/status.html.haml @@ -5,91 +5,94 @@ %i.fa.fa-bitbucket-square = _('Import projects from Bitbucket Server') -- if @repos.any? - %p.light - = _('Select projects you want to import.') - .btn-group - - if @incompatible_repos.any? - = button_tag class: 'btn btn-import btn-success js-import-all' do - = _('Import all compatible projects') - = icon('spinner spin', class: 'loading-icon') - - else - = button_tag class: 'btn btn-import btn-success js-import-all' do - = _('Import all projects') - = icon('spinner spin', class: 'loading-icon') +- 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 + - if @incompatible_repos.any? + = button_tag class: 'btn btn-import btn-success js-import-all' do + = _('Import all compatible projects') + = icon('spinner spin', class: 'loading-icon') + - else + = button_tag class: 'btn btn-import btn-success js-import-all' do + = _('Import all projects') + = icon('spinner spin', class: 'loading-icon') -.btn-group - = link_to('Reconfigure', configure_import_bitbucket_server_path, class: 'btn btn-primary', method: :post) + .btn-group + = link_to('Reconfigure', configure_import_bitbucket_server_path, class: 'btn btn-primary', method: :post) -.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 + .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.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th= _('From Bitbucket Server') - %th= _('To GitLab') - %th= _('Status') - %tbody - - @already_added_projects.each do |project| - %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } - %td - = link_to project.import_source, project.import_source, target: '_blank', rel: 'noopener noreferrer' - %td - = link_to project.full_path, [project.namespace.becomes(Namespace), project] - %td.job-status - - case project.import_status - - when 'finished' - = icon('check', text: 'Done') - - when 'started' - = icon('spin', text: 'started') - - else - = project.human_import_status_name + .table-responsive.prepend-top-10 + %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col + %thead + %tr + %th= _('From Bitbucket Server') + %th= _('To GitLab') + %th= _('Status') + %tbody + - @already_added_projects.each do |project| + %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } + %td + = link_to project.import_source, project.import_source, target: '_blank', rel: 'noopener noreferrer' + %td + = link_to project.full_path, [project.namespace.becomes(Namespace), project] + %td.job-status + - case project.import_status + - when 'finished' + = icon('check', text: 'Done') + - when 'started' + = icon('spin', text: 'started') + - else + = project.human_import_status_name - - @repos.each do |repo| - %tr{ id: "repo_#{repo.project_key}___#{repo.slug}", data: { project: repo.project_key, repository: repo.slug } } - %td - = link_to repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer' - %td.import-target - %fieldset.row - .input-group - .project-path.input-group-prepend - - if current_user.can_select_namespace? - - selected = params[:namespace_id] || :extra_group - - opts = current_user.can_create_group? ? { extra_group: Group.new(name: sanitize_project_name(repo.project_key), path: sanitize_project_name(repo.project_key)) } : {} - = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } - - else - = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true - %span.input-group-prepend - .input-group-text / - = text_field_tag :path, sanitize_project_name(repo.slug), class: "input-mini form-control", tabindex: 2, required: true - %td.import-actions.job-status - = button_tag class: 'btn btn-import js-add-to-import' do - Import - = icon('spinner spin', class: 'loading-icon') - - @incompatible_repos.each do |repo| - %tr{ id: "repo_#{repo.project_key}___#{repo.slug}" } - %td - = link_to repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer' - %td.import-target - %td.import-actions-job-status - = label_tag 'Incompatible Project', nil, class: 'label badge-danger' + - @repos.each do |repo| + %tr{ id: "repo_#{repo.project_key}___#{repo.slug}", data: { project: repo.project_key, repository: repo.slug } } + %td + = link_to repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer' + %td.import-target + %fieldset.row + .input-group + .project-path.input-group-prepend + - if current_user.can_select_namespace? + - selected = params[:namespace_id] || :extra_group + - opts = current_user.can_create_group? ? { extra_group: Group.new(name: sanitize_project_name(repo.project_key), path: sanitize_project_name(repo.project_key)) } : {} + = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } + - else + = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true + %span.input-group-prepend + .input-group-text / + = text_field_tag :path, sanitize_project_name(repo.slug), class: "input-mini form-control", tabindex: 2, required: true + %td.import-actions.job-status + = button_tag class: 'btn btn-import js-add-to-import' do + Import + = icon('spinner spin', class: 'loading-icon') + - @incompatible_repos.each do |repo| + %tr{ id: "repo_#{repo.project_key}___#{repo.slug}" } + %td + = link_to repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer' + %td.import-target + %td.import-actions-job-status + = label_tag 'Incompatible Project', nil, class: 'label badge-danger' -- 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, - rather than Git. Please convert - = link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview' - and go through the - = link_to 'import flow', status_import_bitbucket_server_path - again. + - 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, + rather than Git. Please convert + = link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview' + and go through the + = 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}" } } diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml index eca67582d6f..75529487aa4 100644 --- a/app/views/import/fogbugz/status.html.haml +++ b/app/views/import/fogbugz/status.html.haml @@ -4,56 +4,63 @@ %i.fa.fa-bug = _('Import projects from FogBugz') -- if @repos.any? - %p.light - = _('Select projects you want to import.') +- 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 - %p - = button_tag class: 'btn btn-import btn-success js-import-all' do - = _('Import all projects') - = icon("spinner spin", class: "loading-icon") + = render 'import/githubish_status', provider: 'fogbugz', filterable: false +- else + - if @repos.any? + %p.light + = _('Select projects you want to import.') + %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 + %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.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th= _("From FogBugz") - %th= _("To GitLab") - %th= _("Status") - %tbody - - @already_added_projects.each do |project| - %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } - %td - = project.import_source - %td - = link_to project.full_path, [project.namespace.becomes(Namespace), project] - %td.job-status - - case project.import_status - - when 'finished' - %span - %i.fa.fa-check - = _("done") - - when 'started' - %i.fa.fa-spinner.fa-spin - = _("started") - - else - = project.human_import_status_name + .table-responsive + %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col + %thead + %tr + %th= _("From FogBugz") + %th= _("To GitLab") + %th= _("Status") + %tbody + - @already_added_projects.each do |project| + %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } + %td + = project.import_source + %td + = link_to project.full_path, [project.namespace.becomes(Namespace), project] + %td.job-status + - case project.import_status + - when 'finished' + %span + %i.fa.fa-check + = _("done") + - when 'started' + %i.fa.fa-spinner.fa-spin + = _("started") + - else + = project.human_import_status_name - - @repos.each do |repo| - %tr{ id: "repo_#{repo.id}" } - %td - = repo.name - %td.import-target - #{current_user.username}/#{repo.name} - %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do - = _("Import") - = icon("spinner spin", class: "loading-icon") + - @repos.each do |repo| + %tr{ id: "repo_#{repo.id}" } + %td + = repo.name + %td.import-target + #{current_user.username}/#{repo.name} + %td.import-actions.job-status + = button_tag class: "btn btn-import js-add-to-import" do + = _("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}" } } diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index a5fa12fe7df..a12b69ae5f9 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -4,52 +4,55 @@ %i.fa.fa-heart = _('Import projects from GitLab.com') -%p.light - = _('Select projects you want to import.') -%hr -%p - = button_tag class: "btn btn-import btn-success js-import-all" do - = _('Import all projects') - = icon("spinner spin", class: "loading-icon") +- 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 + = button_tag class: "btn btn-import btn-success js-import-all" do + = _('Import all projects') + = icon("spinner spin", class: "loading-icon") -.table-responsive - %table.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th= _('From GitLab.com') - %th= _('To this GitLab instance') - %th= _('Status') - %tbody - - @already_added_projects.each do |project| - %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } - %td - = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank" - %td - = link_to project.full_path, [project.namespace.becomes(Namespace), project] - %td.job-status - - case project.import_status - - when 'finished' - %span - %i.fa.fa-check - = _('done') - - when 'started' - %i.fa.fa-spinner.fa-spin - = _('started') - - else - = project.human_import_status_name + .table-responsive + %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col + %thead + %tr + %th= _('From GitLab.com') + %th= _('To this GitLab instance') + %th= _('Status') + %tbody + - @already_added_projects.each do |project| + %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } + %td + = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank" + %td + = link_to project.full_path, [project.namespace.becomes(Namespace), project] + %td.job-status + - case project.import_status + - when 'finished' + %span + %i.fa.fa-check + = _('done') + - when 'started' + %i.fa.fa-spinner.fa-spin + = _('started') + - else + = project.human_import_status_name - - @repos.each do |repo| - %tr{ id: "repo_#{repo["id"]}" } - %td - = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank", rel: 'noopener noreferrer' - %td.import-target - = import_project_target(repo['namespace']['path'], repo['name']) - %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do - = _('Import') - = icon("spinner spin", class: "loading-icon") + - @repos.each do |repo| + %tr{ id: "repo_#{repo["id"]}" } + %td + = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank", rel: 'noopener noreferrer' + %td.import-target + = import_project_target(repo['namespace']['path'], repo['name']) + %td.import-actions.job-status + = button_tag class: "btn btn-import js-add-to-import" do + = _('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}" } } |