Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
c07eba6c
Commit
c07eba6c
authored
Mar 18, 2022
by
Illya Klymov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implement history for project imports
* Add new history page Changelog: added
parent
a9b4272b
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
515 additions
and
1 deletion
+515
-1
app/assets/javascripts/pages/import/history/components/import_error_details.vue
.../pages/import/history/components/import_error_details.vue
+41
-0
app/assets/javascripts/pages/import/history/components/import_history_app.vue
...ts/pages/import/history/components/import_history_app.vue
+190
-0
app/assets/javascripts/pages/import/history/index.js
app/assets/javascripts/pages/import/history/index.js
+15
-0
app/assets/javascripts/pages/import/history/utils/error_messages.js
.../javascripts/pages/import/history/utils/error_messages.js
+3
-0
app/controllers/import/history_controller.rb
app/controllers/import/history_controller.rb
+5
-0
app/views/import/history/index.html.haml
app/views/import/history/index.html.haml
+4
-0
app/views/projects/_import_project_pane.html.haml
app/views/projects/_import_project_pane.html.haml
+2
-1
config/routes/import.rb
config/routes/import.rb
+2
-0
locale/gitlab.pot
locale/gitlab.pot
+18
-0
spec/frontend/pages/import/history/import_error_details_spec.js
...rontend/pages/import/history/import_error_details_spec.js
+66
-0
spec/frontend/pages/import/history/import_history_app_spec.js
.../frontend/pages/import/history/import_history_app_spec.js
+169
-0
No files found.
app/assets/javascripts/pages/import/history/components/import_error_details.vue
0 → 100644
View file @
c07eba6c
<
script
>
import
{
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
API
from
'
~/api
'
;
import
{
createAlert
}
from
'
~/flash
'
;
import
{
DEFAULT_ERROR
}
from
'
../utils/error_messages
'
;
export
default
{
components
:
{
GlLoadingIcon
,
},
props
:
{
id
:
{
type
:
Number
,
required
:
true
,
},
},
data
()
{
return
{
loading
:
true
,
error
:
null
,
};
},
async
mounted
()
{
try
{
const
{
data
:
{
import_error
:
importError
},
}
=
await
API
.
project
(
this
.
id
);
this
.
error
=
importError
;
}
catch
(
e
)
{
createAlert
({
message
:
DEFAULT_ERROR
});
this
.
error
=
null
;
}
finally
{
this
.
loading
=
false
;
}
},
};
</
script
>
<
template
>
<gl-loading-icon
v-if=
"loading"
size=
"md"
/>
<pre
v-else
>
{{
error
||
s__
(
'
BulkImport|No additional information provided.
'
)
}}
</pre>
</
template
>
app/assets/javascripts/pages/import/history/components/import_history_app.vue
0 → 100644
View file @
c07eba6c
<
script
>
import
{
GlButton
,
GlEmptyState
,
GlIcon
,
GlLink
,
GlLoadingIcon
,
GlTable
}
from
'
@gitlab/ui
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
createFlash
from
'
~/flash
'
;
import
{
parseIntPagination
,
normalizeHeaders
}
from
'
~/lib/utils/common_utils
'
;
import
{
getProjects
}
from
'
~/rest_api
'
;
import
ImportStatus
from
'
~/import_entities/components/import_status.vue
'
;
import
PaginationBar
from
'
~/vue_shared/components/pagination_bar/pagination_bar.vue
'
;
import
TimeAgo
from
'
~/vue_shared/components/time_ago_tooltip.vue
'
;
import
{
DEFAULT_ERROR
}
from
'
../utils/error_messages
'
;
import
ImportErrorDetails
from
'
./import_error_details.vue
'
;
const
DEFAULT_PER_PAGE
=
20
;
const
DEFAULT_TH_CLASSES
=
'
gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-200! gl-border-b-1! gl-p-5!
'
;
const
tableCell
=
(
config
)
=>
({
thClass
:
`
${
DEFAULT_TH_CLASSES
}
`
,
tdClass
:
(
value
,
key
,
item
)
=>
{
return
{
// eslint-disable-next-line no-underscore-dangle
'
gl-border-b-0!
'
:
item
.
_showDetails
,
};
},
...
config
,
});
export
default
{
components
:
{
GlButton
,
GlEmptyState
,
GlIcon
,
GlLink
,
GlLoadingIcon
,
GlTable
,
PaginationBar
,
ImportStatus
,
ImportErrorDetails
,
TimeAgo
,
},
data
()
{
return
{
loading
:
true
,
historyItems
:
[],
paginationConfig
:
{
page
:
1
,
perPage
:
DEFAULT_PER_PAGE
,
},
pageInfo
:
{},
};
},
fields
:
[
tableCell
({
key
:
'
source
'
,
label
:
s__
(
'
BulkImport|Source
'
),
thClass
:
`
${
DEFAULT_TH_CLASSES
}
gl-w-30p`
,
}),
tableCell
({
key
:
'
destination
'
,
label
:
s__
(
'
BulkImport|Destination
'
),
thClass
:
`
${
DEFAULT_TH_CLASSES
}
gl-w-40p`
,
}),
tableCell
({
key
:
'
created_at
'
,
label
:
__
(
'
Date
'
),
}),
tableCell
({
key
:
'
status
'
,
label
:
__
(
'
Status
'
),
tdAttr
:
{
'
data-qa-selector
'
:
'
import_status_indicator
'
},
}),
],
computed
:
{
hasHistoryItems
()
{
return
this
.
historyItems
.
length
>
0
;
},
},
watch
:
{
paginationConfig
:
{
handler
()
{
this
.
loadHistoryItems
();
},
deep
:
true
,
immediate
:
true
,
},
},
methods
:
{
async
loadHistoryItems
()
{
try
{
this
.
loading
=
true
;
const
{
data
:
historyItems
,
headers
}
=
await
getProjects
(
undefined
,
{
imported
:
true
,
simple
:
false
,
page
:
this
.
paginationConfig
.
page
,
per_page
:
this
.
paginationConfig
.
perPage
,
});
this
.
pageInfo
=
parseIntPagination
(
normalizeHeaders
(
headers
));
this
.
historyItems
=
historyItems
;
}
catch
(
e
)
{
createFlash
({
message
:
DEFAULT_ERROR
,
captureError
:
true
,
error
:
e
});
}
finally
{
this
.
loading
=
false
;
}
},
isHTTP
(
url
)
{
try
{
const
parsedUrl
=
new
URL
(
url
);
return
[
'
http:
'
,
'
https:
'
].
includes
(
parsedUrl
.
protocol
);
}
catch
(
e
)
{
return
false
;
}
},
},
gitlabLogo
:
window
.
gon
.
gitlab_logo
,
};
</
script
>
<
template
>
<div>
<div
class=
"gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex gl-align-items-center"
>
<h1
class=
"gl-my-0 gl-py-4 gl-font-size-h1"
>
<img
:src=
"$options.gitlabLogo"
class=
"gl-w-6 gl-h-6 gl-mb-2 gl-display-inline gl-mr-2"
/>
{{
s__
(
'
BulkImport|Project import history
'
)
}}
</h1>
</div>
<gl-loading-icon
v-if=
"loading"
size=
"md"
class=
"gl-mt-5"
/>
<gl-empty-state
v-else-if=
"!hasHistoryItems"
:title=
"s__('BulkImport|No history is available')"
:description=
"s__('BulkImport|Your imported projects will appear here.')"
/>
<template
v-else
>
<gl-table
:fields=
"$options.fields"
:items=
"historyItems"
data-qa-selector=
"import_history_table"
class=
"gl-w-full"
>
<template
#cell(source)=
"
{ item }">
<template
v-if=
"item.import_url"
>
<gl-link
v-if=
"isHTTP(item.import_url)"
:href=
"item.import_url"
target=
"_blank"
>
{{
item
.
import_url
}}
<gl-icon
name=
"external-link"
class=
"gl-vertical-align-middle"
/>
</gl-link>
<span
v-else
>
{{
item
.
import_url
}}
</span>
</
template
>
<span
v-else
>
{{
s__('BulkImport|Template / File-based import / GitLab Migration')
}}
</span>
</template>
<
template
#cell(destination)=
"{ item }"
>
<gl-link
:href=
"item.http_url_to_repo"
>
{{
item
.
path_with_namespace
}}
</gl-link>
</
template
>
<
template
#cell(created_at)=
"{ value }"
>
<time-ago
:time=
"value"
/>
</
template
>
<
template
#cell(status)=
"{ item, toggleDetails, detailsShowing }"
>
<import-status
:status=
"item.import_status"
class=
"gl-display-inline-block gl-w-13"
/>
<gl-button
v-if=
"item.import_status === 'failed'"
class=
"gl-ml-3"
:selected=
"detailsShowing"
@
click=
"toggleDetails"
>
{{
__
(
'
Details
'
)
}}
</gl-button
>
</
template
>
<
template
#row-details=
"{ item }"
>
<import-error-details
:id=
"item.id"
/>
</
template
>
</gl-table>
<pagination-bar
:page-info=
"pageInfo"
class=
"gl-m-0 gl-mt-3"
@
set-page=
"paginationConfig.page = $event"
@
set-page-size=
"paginationConfig.perPage = $event"
/>
</template>
</div>
</template>
app/assets/javascripts/pages/import/history/index.js
0 → 100644
View file @
c07eba6c
import
Vue
from
'
vue
'
;
import
ImportHistoryApp
from
'
./components/import_history_app.vue
'
;
function
mountImportHistoryApp
(
mountElement
)
{
if
(
!
mountElement
)
return
undefined
;
return
new
Vue
({
el
:
mountElement
,
render
(
createElement
)
{
return
createElement
(
ImportHistoryApp
);
},
});
}
mountImportHistoryApp
(
document
.
querySelector
(
'
#import-history-mount-element
'
));
app/assets/javascripts/pages/import/history/utils/error_messages.js
0 → 100644
View file @
c07eba6c
import
{
__
}
from
'
~/locale
'
;
export
const
DEFAULT_ERROR
=
__
(
'
Something went wrong on our end.
'
);
app/controllers/import/history_controller.rb
0 → 100644
View file @
c07eba6c
# frozen_string_literal: true
class
Import::HistoryController
<
ApplicationController
feature_category
:importers
end
app/views/import/history/index.html.haml
0 → 100644
View file @
c07eba6c
-
add_to_breadcrumbs
_
(
'Create a new project'
),
new_project_path
-
page_title
_
(
'Import history'
)
#import-history-mount-element
app/views/projects/_import_project_pane.html.haml
View file @
c07eba6c
...
...
@@ -3,8 +3,9 @@
.project-import
.form-group.import-btn-container.clearfix
%h5
%h5
.gl-display-flex
=
_
(
"Import project from"
)
=
link_to
_
(
'History'
),
import_history_index_path
,
class:
'gl-link gl-ml-auto gl-font-weight-normal'
.import-buttons
-
if
gitlab_project_import_enabled?
.import_gitlab_project.has-tooltip
{
data:
{
container:
'body'
,
qa_selector:
'gitlab_import_button'
}
}
...
...
config/routes/import.rb
View file @
c07eba6c
...
...
@@ -10,6 +10,8 @@ Devise.omniauth_providers.map(&:downcase).each do |provider|
end
namespace
:import
do
resources
:history
,
only:
[
:index
],
controller: :history
resources
:available_namespaces
,
only:
[
:index
],
controller: :available_namespaces
namespace
:url
do
...
...
locale/gitlab.pot
View file @
c07eba6c
...
...
@@ -6321,6 +6321,9 @@ msgstr ""
msgid "BulkImport|%{feature} (require v%{version})"
msgstr ""
msgid "BulkImport|Destination"
msgstr ""
msgid "BulkImport|Existing groups"
msgstr ""
...
...
@@ -6363,12 +6366,18 @@ msgstr ""
msgid "BulkImport|New group"
msgstr ""
msgid "BulkImport|No additional information provided."
msgstr ""
msgid "BulkImport|No history is available"
msgstr ""
msgid "BulkImport|No parent"
msgstr ""
msgid "BulkImport|Project import history"
msgstr ""
msgid "BulkImport|Re-import creates a new group. It does not sync with the existing group."
msgstr ""
...
...
@@ -6381,9 +6390,15 @@ msgstr ""
msgid "BulkImport|Showing %{start}-%{end} of %{total} matching filter \"%{filter}\" from %{link}"
msgstr ""
msgid "BulkImport|Source"
msgstr ""
msgid "BulkImport|Source group"
msgstr ""
msgid "BulkImport|Template / File-based import / GitLab Migration"
msgstr ""
msgid "BulkImport|To new group"
msgstr ""
...
...
@@ -6396,6 +6411,9 @@ msgstr ""
msgid "BulkImport|Your imported groups will appear here."
msgstr ""
msgid "BulkImport|Your imported projects will appear here."
msgstr ""
msgid "BulkImport|expected an associated Group but has an associated Project"
msgstr ""
...
...
spec/frontend/pages/import/history/import_error_details_spec.js
0 → 100644
View file @
c07eba6c
import
{
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
ImportErrorDetails
from
'
~/pages/import/history/components/import_error_details.vue
'
;
describe
(
'
ImportErrorDetails
'
,
()
=>
{
const
FAKE_ID
=
5
;
const
API_URL
=
`/api/v4/projects/
${
FAKE_ID
}
`
;
let
wrapper
;
let
mock
;
function
createComponent
({
shallow
=
true
}
=
{})
{
const
mountFn
=
shallow
?
shallowMount
:
mount
;
wrapper
=
mountFn
(
ImportErrorDetails
,
{
propsData
:
{
id
:
FAKE_ID
,
},
});
}
const
originalApiVersion
=
gon
.
api_version
;
beforeAll
(()
=>
{
gon
.
api_version
=
'
v4
'
;
});
afterAll
(()
=>
{
gon
.
api_version
=
originalApiVersion
;
});
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
});
afterEach
(()
=>
{
mock
.
restore
();
wrapper
.
destroy
();
});
describe
(
'
general behavior
'
,
()
=>
{
it
(
'
renders loading state when loading
'
,
()
=>
{
createComponent
();
expect
(
wrapper
.
find
(
GlLoadingIcon
).
exists
()).
toBe
(
true
);
});
it
(
'
renders import_error if it is available
'
,
async
()
=>
{
const
FAKE_IMPORT_ERROR
=
'
IMPORT ERROR
'
;
mock
.
onGet
(
API_URL
).
reply
(
200
,
{
import_error
:
FAKE_IMPORT_ERROR
});
createComponent
();
await
axios
.
waitForAll
();
expect
(
wrapper
.
find
(
GlLoadingIcon
).
exists
()).
toBe
(
false
);
expect
(
wrapper
.
find
(
'
pre
'
).
text
()).
toBe
(
FAKE_IMPORT_ERROR
);
});
it
(
'
renders default text if error is not available
'
,
async
()
=>
{
mock
.
onGet
(
API_URL
).
reply
(
200
,
{
import_error
:
null
});
createComponent
();
await
axios
.
waitForAll
();
expect
(
wrapper
.
find
(
GlLoadingIcon
).
exists
()).
toBe
(
false
);
expect
(
wrapper
.
find
(
'
pre
'
).
text
()).
toBe
(
'
No additional information provided.
'
);
});
});
});
spec/frontend/pages/import/history/import_history_app_spec.js
0 → 100644
View file @
c07eba6c
import
{
GlEmptyState
,
GlLoadingIcon
,
GlTable
}
from
'
@gitlab/ui
'
;
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
ImportErrorDetails
from
'
~/pages/import/history/components/import_error_details.vue
'
;
import
ImportHistoryApp
from
'
~/pages/import/history/components/import_history_app.vue
'
;
import
PaginationBar
from
'
~/vue_shared/components/pagination_bar/pagination_bar.vue
'
;
import
{
extendedWrapper
}
from
'
helpers/vue_test_utils_helper
'
;
describe
(
'
ImportHistoryApp
'
,
()
=>
{
const
API_URL
=
'
/api/v4/projects.json
'
;
const
DEFAULT_HEADERS
=
{
'
x-page
'
:
1
,
'
x-per-page
'
:
20
,
'
x-next-page
'
:
2
,
'
x-total
'
:
22
,
'
x-total-pages
'
:
2
,
'
x-prev-page
'
:
null
,
};
const
DUMMY_RESPONSE
=
[
{
id
:
1
,
path_with_namespace
:
'
root/imported
'
,
created_at
:
'
2022-03-10T15:10:03.172Z
'
,
import_url
:
null
,
import_type
:
'
gitlab_project
'
,
import_status
:
'
finished
'
,
},
{
id
:
2
,
name_with_namespace
:
'
Administrator / Dummy
'
,
path_with_namespace
:
'
root/dummy
'
,
created_at
:
'
2022-03-09T11:23:04.974Z
'
,
import_url
:
'
https://dummy.github/url
'
,
import_type
:
'
github
'
,
import_status
:
'
failed
'
,
},
{
id
:
2
,
name_with_namespace
:
'
Administrator / Dummy
'
,
path_with_namespace
:
'
root/dummy
'
,
created_at
:
'
2022-03-09T11:23:04.974Z
'
,
import_url
:
'
git://non-http.url
'
,
import_type
:
'
gi
'
,
import_status
:
'
finished
'
,
},
];
let
wrapper
;
let
mock
;
function
createComponent
({
shallow
=
true
}
=
{})
{
const
mountFn
=
shallow
?
shallowMount
:
mount
;
wrapper
=
mountFn
(
ImportHistoryApp
);
}
const
originalApiVersion
=
gon
.
api_version
;
beforeAll
(()
=>
{
gon
.
api_version
=
'
v4
'
;
});
afterAll
(()
=>
{
gon
.
api_version
=
originalApiVersion
;
});
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
});
afterEach
(()
=>
{
mock
.
restore
();
wrapper
.
destroy
();
});
describe
(
'
general behavior
'
,
()
=>
{
it
(
'
renders loading state when loading
'
,
()
=>
{
createComponent
();
expect
(
wrapper
.
find
(
GlLoadingIcon
).
exists
()).
toBe
(
true
);
});
it
(
'
renders empty state when no data is available
'
,
async
()
=>
{
mock
.
onGet
(
API_URL
).
reply
(
200
,
[],
DEFAULT_HEADERS
);
createComponent
();
await
axios
.
waitForAll
();
expect
(
wrapper
.
find
(
GlLoadingIcon
).
exists
()).
toBe
(
false
);
expect
(
wrapper
.
find
(
GlEmptyState
).
exists
()).
toBe
(
true
);
});
it
(
'
renders table with data when history is available
'
,
async
()
=>
{
mock
.
onGet
(
API_URL
).
reply
(
200
,
DUMMY_RESPONSE
,
DEFAULT_HEADERS
);
createComponent
();
await
axios
.
waitForAll
();
const
table
=
wrapper
.
find
(
GlTable
);
expect
(
table
.
exists
()).
toBe
(
true
);
// can't use .props() or .attributes() here
expect
(
table
.
vm
.
$attrs
.
items
).
toHaveLength
(
DUMMY_RESPONSE
.
length
);
});
it
(
'
changes page when requested by pagination bar
'
,
async
()
=>
{
const
NEW_PAGE
=
4
;
mock
.
onGet
(
API_URL
).
reply
(
200
,
DUMMY_RESPONSE
,
DEFAULT_HEADERS
);
createComponent
();
await
axios
.
waitForAll
();
mock
.
resetHistory
();
wrapper
.
findComponent
(
PaginationBar
).
vm
.
$emit
(
'
set-page
'
,
NEW_PAGE
);
await
axios
.
waitForAll
();
expect
(
mock
.
history
.
get
.
length
).
toBe
(
1
);
expect
(
mock
.
history
.
get
[
0
].
params
).
toStrictEqual
(
expect
.
objectContaining
({
page
:
NEW_PAGE
}));
});
});
it
(
'
changes page size when requested by pagination bar
'
,
async
()
=>
{
const
NEW_PAGE_SIZE
=
4
;
mock
.
onGet
(
API_URL
).
reply
(
200
,
DUMMY_RESPONSE
,
DEFAULT_HEADERS
);
createComponent
();
await
axios
.
waitForAll
();
mock
.
resetHistory
();
wrapper
.
findComponent
(
PaginationBar
).
vm
.
$emit
(
'
set-page-size
'
,
NEW_PAGE_SIZE
);
await
axios
.
waitForAll
();
expect
(
mock
.
history
.
get
.
length
).
toBe
(
1
);
expect
(
mock
.
history
.
get
[
0
].
params
).
toStrictEqual
(
expect
.
objectContaining
({
per_page
:
NEW_PAGE_SIZE
}),
);
});
describe
(
'
details button
'
,
()
=>
{
beforeEach
(()
=>
{
mock
.
onGet
(
API_URL
).
reply
(
200
,
DUMMY_RESPONSE
,
DEFAULT_HEADERS
);
createComponent
({
shallow
:
false
});
return
axios
.
waitForAll
();
});
it
(
'
renders details button if relevant item has failed
'
,
async
()
=>
{
expect
(
extendedWrapper
(
wrapper
.
find
(
'
tbody
'
).
findAll
(
'
tr
'
).
at
(
1
)).
findByText
(
'
Details
'
).
exists
(),
).
toBe
(
true
);
});
it
(
'
does not render details button if relevant item does not failed
'
,
()
=>
{
expect
(
extendedWrapper
(
wrapper
.
find
(
'
tbody
'
).
findAll
(
'
tr
'
).
at
(
0
)).
findByText
(
'
Details
'
).
exists
(),
).
toBe
(
false
);
});
it
(
'
expands details when details button is clicked
'
,
async
()
=>
{
const
ORIGINAL_ROW_INDEX
=
1
;
await
extendedWrapper
(
wrapper
.
find
(
'
tbody
'
).
findAll
(
'
tr
'
).
at
(
ORIGINAL_ROW_INDEX
))
.
findByText
(
'
Details
'
)
.
trigger
(
'
click
'
);
const
detailsRowContent
=
wrapper
.
find
(
'
tbody
'
)
.
findAll
(
'
tr
'
)
.
at
(
ORIGINAL_ROW_INDEX
+
1
)
.
findComponent
(
ImportErrorDetails
);
expect
(
detailsRowContent
.
exists
()).
toBe
(
true
);
expect
(
detailsRowContent
.
props
().
id
).
toBe
(
DUMMY_RESPONSE
[
1
].
id
);
});
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment