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
6a31a37b
Commit
6a31a37b
authored
May 06, 2021
by
Illya Klymov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implement bulk import for all groups on the page
- add Import all button Changelog: added
parent
e0c1d02d
Changes
17
Show whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
415 additions
and
86 deletions
+415
-86
app/assets/javascripts/import_entities/import_groups/components/import_table.vue
...import_entities/import_groups/components/import_table.vue
+70
-8
app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue
...rt_entities/import_groups/components/import_table_row.vue
+33
-5
app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
...s/import_entities/import_groups/graphql/client_factory.js
+71
-25
app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
.../fragments/bulk_import_source_group_item.fragment.graphql
+4
-0
app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql
...s/graphql/mutations/add_validation_error.mutation.graphql
+9
-0
app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
...t_groups/graphql/mutations/import_groups.mutation.graphql
+9
-0
app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql
...raphql/mutations/remove_validation_error.mutation.graphql
+9
-0
app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
...s/import_groups/graphql/services/source_groups_manager.js
+34
-12
app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
...ts/import_entities/import_groups/graphql/typedefs.graphql
+15
-3
app/views/import/bulk_imports/status.html.haml
app/views/import/bulk_imports/status.html.haml
+0
-3
changelogs/unreleased/xanf-bulk-import-all-on-the-page.yml
changelogs/unreleased/xanf-bulk-import-all-on-the-page.yml
+5
-0
locale/gitlab.pot
locale/gitlab.pot
+9
-0
spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
...ntities/import_groups/components/import_table_row_spec.js
+11
-6
spec/frontend/import_entities/import_groups/components/import_table_spec.js
...rt_entities/import_groups/components/import_table_spec.js
+66
-3
spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
...ort_entities/import_groups/graphql/client_factory_spec.js
+50
-11
spec/frontend/import_entities/import_groups/graphql/fixtures.js
...rontend/import_entities/import_groups/graphql/fixtures.js
+1
-0
spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
...ort_groups/graphql/services/source_groups_manager_spec.js
+19
-10
No files found.
app/assets/javascripts/import_entities/import_groups/components/import_table.vue
View file @
6a31a37b
<
script
>
import
{
GlButton
,
GlEmptyState
,
GlDropdown
,
GlDropdownItem
,
...
...
@@ -8,10 +9,13 @@ import {
GlLoadingIcon
,
GlSearchBoxByClick
,
GlSprintf
,
GlSafeHtmlDirective
as
SafeHtml
,
GlTooltip
,
}
from
'
@gitlab/ui
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
{
s__
,
__
,
n__
}
from
'
~/locale
'
;
import
PaginationLinks
from
'
~/vue_shared/components/pagination_links.vue
'
;
import
importGroupMutation
from
'
../graphql/mutations/import_group.mutation.graphql
'
;
import
{
STATUSES
}
from
'
../../constants
'
;
import
importGroupsMutation
from
'
../graphql/mutations/import_groups.mutation.graphql
'
;
import
setNewNameMutation
from
'
../graphql/mutations/set_new_name.mutation.graphql
'
;
import
setTargetNamespaceMutation
from
'
../graphql/mutations/set_target_namespace.mutation.graphql
'
;
import
availableNamespacesQuery
from
'
../graphql/queries/available_namespaces.query.graphql
'
;
...
...
@@ -23,6 +27,7 @@ const DEFAULT_PAGE_SIZE = PAGE_SIZES[0];
export
default
{
components
:
{
GlButton
,
GlEmptyState
,
GlDropdown
,
GlDropdownItem
,
...
...
@@ -31,9 +36,13 @@ export default {
GlLoadingIcon
,
GlSearchBoxByClick
,
GlSprintf
,
GlTooltip
,
ImportTableRow
,
PaginationLinks
,
},
directives
:
{
SafeHtml
,
},
props
:
{
sourceUrl
:
{
...
...
@@ -65,12 +74,28 @@ export default {
},
computed
:
{
groups
()
{
return
this
.
bulkImportSourceGroups
?.
nodes
??
[];
},
hasGroupsWithValidationError
()
{
return
this
.
groups
.
some
((
g
)
=>
g
.
validation_errors
.
length
);
},
availableGroupsForImport
()
{
return
this
.
groups
.
filter
((
g
)
=>
g
.
progress
.
status
===
STATUSES
.
NONE
);
},
isImportAllButtonDisabled
()
{
return
this
.
hasGroupsWithValidationError
||
this
.
availableGroupsForImport
.
length
===
0
;
},
humanizedTotal
()
{
return
this
.
paginationInfo
.
total
>=
1000
?
__
(
'
1000+
'
)
:
this
.
paginationInfo
.
total
;
},
hasGroups
()
{
return
this
.
bulkImportSourceGroups
?.
nodes
?
.
length
>
0
;
return
this
.
groups
.
length
>
0
;
},
hasEmptyFilter
()
{
...
...
@@ -105,6 +130,10 @@ export default {
},
methods
:
{
groupsCount
(
count
)
{
return
n__
(
'
%d group
'
,
'
%d groups
'
,
count
);
},
setPage
(
page
)
{
this
.
page
=
page
;
},
...
...
@@ -123,24 +152,57 @@ export default {
});
},
importGroup
(
sourceGroupId
)
{
importGroup
s
(
sourceGroupIds
)
{
this
.
$apollo
.
mutate
({
mutation
:
importGroupMutation
,
variables
:
{
sourceGroupId
},
mutation
:
importGroup
s
Mutation
,
variables
:
{
sourceGroupId
s
},
});
},
importAllGroups
()
{
this
.
importGroups
(
this
.
availableGroupsForImport
.
map
((
g
)
=>
g
.
id
));
},
setPageSize
(
size
)
{
this
.
perPage
=
size
;
},
},
gitlabLogo
:
window
.
gon
.
gitlab_logo
,
PAGE_SIZES
,
};
</
script
>
<
template
>
<div>
<h1
class=
"gl-my-0 gl-py-4 gl-font-size-h1 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex"
>
<img
:src=
"$options.gitlabLogo"
class=
"gl-w-6 gl-h-6 gl-mb-2 gl-display-inline gl-mr-2"
/>
{{
s__
(
'
BulkImport|Import groups from GitLab
'
)
}}
<div
ref=
"importAllButtonWrapper"
class=
"gl-ml-auto"
>
<gl-button
v-if=
"!$apollo.loading && hasGroups"
:disabled=
"isImportAllButtonDisabled"
variant=
"confirm"
@
click=
"importAllGroups"
>
<gl-sprintf
:message=
"s__('BulkImport|Import %
{groups}')">
<template
#groups
>
{{
groupsCount
(
availableGroupsForImport
.
length
)
}}
</
template
>
</gl-sprintf>
</gl-button>
</div>
<gl-tooltip
v-if=
"isImportAllButtonDisabled"
:target=
"() => $refs.importAllButtonWrapper"
>
<
template
v-if=
"hasGroupsWithValidationError"
>
{{
s__
(
'
BulkImport|One or more groups has validation errors
'
)
}}
</
template
>
<
template
v-else
>
{{
s__
(
'
BulkImport|No groups on this page are available for import
'
)
}}
</
template
>
</gl-tooltip>
</h1>
<div
class=
"gl-py-5 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex"
>
...
...
@@ -153,7 +215,7 @@ export default {
<strong>
{{
paginationInfo
.
end
}}
</strong>
</
template
>
<
template
#total
>
<strong>
{{
n__
(
'
%d group
'
,
'
%d groups
'
,
paginationInfo
.
total
)
}}
</strong>
<strong>
{{
groupsCount
(
paginationInfo
.
total
)
}}
</strong>
</
template
>
<
template
#filter
>
<strong>
{{
filter
}}
</strong>
...
...
@@ -196,7 +258,7 @@ export default {
:group-path-regex=
"groupPathRegex"
@
update-target-namespace=
"updateTargetNamespace(group.id, $event)"
@
update-new-name=
"updateNewName(group.id, $event)"
@
import-group=
"importGroup
(group.id
)"
@
import-group=
"importGroup
s([group.id]
)"
/>
</
template
>
</tbody>
...
...
app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue
View file @
6a31a37b
...
...
@@ -10,8 +10,11 @@ import {
GlFormInput
,
}
from
'
@gitlab/ui
'
;
import
{
joinPaths
}
from
'
~/lib/utils/url_utility
'
;
import
{
s__
}
from
'
~/locale
'
;
import
ImportStatus
from
'
../../components/import_status.vue
'
;
import
{
STATUSES
}
from
'
../../constants
'
;
import
addValidationErrorMutation
from
'
../graphql/mutations/add_validation_error.mutation.graphql
'
;
import
removeValidationErrorMutation
from
'
../graphql/mutations/remove_validation_error.mutation.graphql
'
;
import
groupQuery
from
'
../graphql/queries/group.query.graphql
'
;
const
DEBOUNCE_INTERVAL
=
300
;
...
...
@@ -52,6 +55,27 @@ export default {
fullPath
:
this
.
fullPath
,
};
},
update
({
existingGroup
})
{
const
variables
=
{
field
:
'
new_name
'
,
sourceGroupId
:
this
.
group
.
id
,
};
if
(
!
existingGroup
)
{
this
.
$apollo
.
mutate
({
mutation
:
removeValidationErrorMutation
,
variables
,
});
}
else
{
this
.
$apollo
.
mutate
({
mutation
:
addValidationErrorMutation
,
variables
:
{
...
variables
,
message
:
s__
(
'
BulkImport|Name already exists.
'
),
},
});
}
},
skip
()
{
return
!
this
.
isNameValid
||
this
.
isAlreadyImported
;
},
...
...
@@ -63,8 +87,12 @@ export default {
return
this
.
group
.
import_target
;
},
invalidNameValidationMessage
()
{
return
this
.
group
.
validation_errors
.
find
(({
field
})
=>
field
===
'
new_name
'
)?.
message
;
},
isInvalid
()
{
return
Boolean
(
!
this
.
isNameValid
||
this
.
existingGroup
);
return
Boolean
(
!
this
.
isNameValid
||
this
.
invalidNameValidationMessage
);
},
isNameValid
()
{
...
...
@@ -157,21 +185,21 @@ export default {
<
template
v-if=
"!isNameValid"
>
{{
__
(
'
Please choose a group URL with no special characters.
'
)
}}
</
template
>
<
template
v-else-if=
"
existingGroup
"
>
{{
s__
(
'
BulkImport|Name already exists.
'
)
}}
<
template
v-else-if=
"
invalidNameValidationMessage
"
>
{{
invalidNameValidationMessage
}}
</
template
>
</p>
</div>
</div>
</td>
<td
class=
"gl-p-4 gl-white-space-nowrap"
>
<import-status
:status=
"group.progress.status"
/>
<import-status
:status=
"group.progress.status"
class=
"gl-mt-2"
/>
</td>
<td
class=
"gl-p-4"
>
<gl-button
v-if=
"!isAlreadyImported"
:disabled=
"isInvalid"
variant=
"
success
"
variant=
"
confirm
"
category=
"secondary"
@
click=
"$emit('import-group')"
>
{{ __('Import') }}
</gl-button
...
...
app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
View file @
6a31a37b
...
...
@@ -20,6 +20,7 @@ export const clientTypenames = {
BulkImportPageInfo
:
'
ClientBulkImportPageInfo
'
,
BulkImportTarget
:
'
ClientBulkImportTarget
'
,
BulkImportProgress
:
'
ClientBulkImportProgress
'
,
BulkImportValidationError
:
'
ClientBulkImportValidationError
'
,
};
function
makeGroup
(
data
)
{
...
...
@@ -106,6 +107,7 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
return
makeGroup
({
...
group
,
validation_errors
:
[],
progress
:
{
id
:
jobId
??
localProgressId
(
group
.
id
),
status
:
cachedImportState
?.
status
??
STATUSES
.
NONE
,
...
...
@@ -152,7 +154,7 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
async
setImportProgress
(
_
,
{
sourceGroupId
,
status
,
jobId
})
{
if
(
jobId
)
{
groupsManager
.
saveImportState
(
jobId
,
{
status
}
);
groupsManager
.
updateImportProgress
(
jobId
,
status
);
}
return
makeGroup
({
...
...
@@ -165,7 +167,7 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
},
async
updateImportStatus
(
_
,
{
id
,
status
})
{
groupsManager
.
saveImportState
(
id
,
{
status
}
);
groupsManager
.
updateImportProgress
(
id
,
status
);
return
{
__typename
:
clientTypenames
.
BulkImportProgress
,
...
...
@@ -174,39 +176,81 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
};
},
async
importGroup
(
_
,
{
sourceGroupId
},
{
client
})
{
async
addValidationError
(
_
,
{
sourceGroupId
,
field
,
message
},
{
client
})
{
const
{
data
:
{
bulkImportSourceGroup
:
group
},
data
:
{
bulkImportSourceGroup
:
{
validation_errors
:
validationErrors
,
...
group
},
},
}
=
await
client
.
query
({
query
:
bulkImportSourceGroupQuery
,
variables
:
{
id
:
sourceGroupId
},
});
return
{
...
group
,
validation_errors
:
[
...
validationErrors
.
filter
(({
field
:
f
})
=>
f
!==
field
),
{
__typename
:
clientTypenames
.
BulkImportValidationError
,
field
,
message
,
},
],
};
},
async
removeValidationError
(
_
,
{
sourceGroupId
,
field
},
{
client
})
{
const
{
data
:
{
bulkImportSourceGroup
:
{
validation_errors
:
validationErrors
,
...
group
},
},
}
=
await
client
.
query
({
query
:
bulkImportSourceGroupQuery
,
variables
:
{
id
:
sourceGroupId
},
});
const
GROUP_BEING_SCHEDULED
=
makeGroup
({
return
{
...
group
,
validation_errors
:
validationErrors
.
filter
(({
field
:
f
})
=>
f
!==
field
),
};
},
async
importGroups
(
_
,
{
sourceGroupIds
},
{
client
})
{
const
groups
=
await
Promise
.
all
(
sourceGroupIds
.
map
((
id
)
=>
client
.
query
({
query
:
bulkImportSourceGroupQuery
,
variables
:
{
id
},
})
.
then
(({
data
})
=>
data
.
bulkImportSourceGroup
),
),
);
const
GROUPS_BEING_SCHEDULED
=
sourceGroupIds
.
map
((
sourceGroupId
)
=>
makeGroup
({
id
:
sourceGroupId
,
progress
:
{
id
:
localProgressId
(
sourceGroupId
),
status
:
STATUSES
.
SCHEDULING
,
},
});
}),
);
const
defaultErrorMessage
=
s__
(
'
BulkImport|Importing the group failed
'
);
axios
.
post
(
endpoints
.
createBulkImport
,
{
bulk_import
:
[
{
bulk_import
:
groups
.
map
((
group
)
=>
({
source_type
:
'
group_entity
'
,
source_full_path
:
group
.
full_path
,
destination_namespace
:
group
.
import_target
.
target_namespace
,
destination_name
:
group
.
import_target
.
new_name
,
},
],
})),
})
.
then
(({
data
:
{
id
:
jobId
}
})
=>
{
groupsManager
.
saveImportState
(
jobId
,
{
id
:
group
.
id
,
importTarget
:
group
.
import_target
,
groupsManager
.
createImportState
(
jobId
,
{
status
:
STATUSES
.
CREATED
,
groups
,
});
return
{
status
:
STATUSES
.
CREATED
,
jobId
};
...
...
@@ -217,14 +261,16 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
return
{
status
:
STATUSES
.
NONE
};
})
.
then
((
newStatus
)
=>
sourceGroupIds
.
forEach
((
sourceGroupId
)
=>
client
.
mutate
({
mutation
:
setImportProgressMutation
,
variables
:
{
sourceGroupId
,
...
newStatus
},
}),
),
)
.
catch
(()
=>
createFlash
({
message
:
defaultErrorMessage
}));
return
GROUP_BEING_SCHEDULED
;
return
GROUP
S
_BEING_SCHEDULED
;
},
},
};
...
...
app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
View file @
6a31a37b
...
...
@@ -12,4 +12,8 @@ fragment BulkImportSourceGroupItem on ClientBulkImportSourceGroup {
target_namespace
new_name
}
validation_errors
{
field
message
}
}
app/assets/javascripts/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql
0 → 100644
View file @
6a31a37b
mutation
addValidationError
(
$sourceGroupId
:
String
!,
$field
:
String
!,
$message
:
String
!)
{
addValidationError
(
sourceGroupId
:
$sourceGroupId
,
field
:
$field
,
message
:
$message
)
@client
{
id
validation_errors
{
field
message
}
}
}
app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql
→
app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_group
s
.mutation.graphql
View file @
6a31a37b
mutation
importGroup
(
$sourceGroupId
:
String
!)
{
importGroup
(
sourceGroupId
:
$sourceGroupId
)
@client
{
mutation
importGroup
s
(
$sourceGroupIds
:
[
String
!]
!)
{
importGroup
s
(
sourceGroupIds
:
$sourceGroupIds
)
@client
{
id
progress
{
id
...
...
app/assets/javascripts/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql
0 → 100644
View file @
6a31a37b
mutation
removeValidationError
(
$sourceGroupId
:
String
!,
$field
:
String
!)
{
removeValidationError
(
sourceGroupId
:
$sourceGroupId
,
field
:
$field
)
@client
{
id
validation_errors
{
field
message
}
}
}
app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
View file @
6a31a37b
...
...
@@ -13,25 +13,42 @@ export class SourceGroupsManager {
loadImportStatesFromStorage
()
{
try
{
return
JSON
.
parse
(
this
.
storage
.
getItem
(
KEY
))
??
{};
return
Object
.
fromEntries
(
Object
.
entries
(
JSON
.
parse
(
this
.
storage
.
getItem
(
KEY
))
??
{}).
map
(([
jobId
,
config
])
=>
{
// new format of storage
if
(
config
.
groups
)
{
return
[
jobId
,
config
];
}
return
[
jobId
,
{
status
:
config
.
status
,
groups
:
[{
id
:
config
.
id
,
importTarget
:
config
.
importTarget
}],
},
];
}),
);
}
catch
{
return
{};
}
}
saveImportState
(
importId
,
group
)
{
const
key
=
this
.
getStorageKey
(
importId
);
const
oldState
=
this
.
importStates
[
key
]
??
{};
createImportState
(
importId
,
jobConfig
)
{
this
.
importStates
[
this
.
getStorageKey
(
importId
)]
=
{
status
:
jobConfig
.
status
,
groups
:
jobConfig
.
groups
.
map
((
g
)
=>
({
importTarget
:
g
.
import_target
,
id
:
g
.
id
})),
};
this
.
saveImportStatesToStorage
();
}
if
(
!
oldState
.
id
&&
!
group
.
id
)
{
updateImportProgress
(
importId
,
status
)
{
const
currentState
=
this
.
importStates
[
this
.
getStorageKey
(
importId
)];
if
(
!
currentState
)
{
return
;
}
this
.
importStates
[
key
]
=
{
...
oldState
,
...
group
,
status
:
group
.
status
,
};
currentState
.
status
=
status
;
this
.
saveImportStatesToStorage
();
}
...
...
@@ -39,10 +56,15 @@ export class SourceGroupsManager {
const
PREFIX
=
this
.
getStorageKey
(
''
);
const
[
jobId
,
importState
]
=
Object
.
entries
(
this
.
importStates
).
find
(
([
key
,
group
])
=>
key
.
startsWith
(
PREFIX
)
&&
group
.
id
===
groupId
,
([
key
,
state
])
=>
key
.
startsWith
(
PREFIX
)
&&
state
.
groups
.
some
((
g
)
=>
g
.
id
===
groupId
)
,
)
??
[];
return
{
jobId
,
importState
};
if
(
!
jobId
)
{
return
null
;
}
const
group
=
importState
.
groups
.
find
((
g
)
=>
g
.
id
===
groupId
);
return
{
jobId
,
importState
:
{
...
group
,
status
:
importState
.
status
}
};
}
getStorageKey
(
importId
)
{
...
...
app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
View file @
6a31a37b
...
...
@@ -18,6 +18,11 @@ type ClientBulkImportProgress {
status
:
String
!
}
type
ClientBulkImportValidationError
{
field
:
String
!
message
:
String
!
}
type
ClientBulkImportSourceGroup
{
id
:
ID
!
web_url
:
String
!
...
...
@@ -25,6 +30,7 @@ type ClientBulkImportSourceGroup {
full_name
:
String
!
progress
:
ClientBulkImportProgress
!
import_target
:
ClientBulkImportTarget
!
validation_errors
:
[
ClientBulkImportValidationError
!]!
}
type
ClientBulkImportPageInfo
{
...
...
@@ -45,9 +51,15 @@ extend type Query {
}
extend
type
Mutation
{
setNewName
(
newName
:
String
,
sourceGroupId
:
ID
!):
Client
TargetNamespace
!
setTargetNamespace
(
targetNamespace
:
String
,
sourceGroupId
:
ID
!):
Client
TargetNamespace
!
importGroup
(
id
:
ID
!):
ClientBulkImportSourceGroup
!
setNewName
(
newName
:
String
,
sourceGroupId
:
ID
!):
Client
BulkImportSourceGroup
!
setTargetNamespace
(
targetNamespace
:
String
,
sourceGroupId
:
ID
!):
Client
BulkImportSourceGroup
!
importGroup
s
(
sourceGroupIds
:
[
ID
!]!):
[
ClientBulkImportSourceGroup
!]
!
setImportProgress
(
id
:
ID
,
status
:
String
!):
ClientBulkImportSourceGroup
!
updateImportProgress
(
id
:
ID
,
status
:
String
!):
ClientBulkImportProgress
addValidationError
(
sourceGroupId
:
ID
!
field
:
String
!
message
:
String
!
):
ClientBulkImportSourceGroup
!
removeValidationError
(
sourceGroupId
:
ID
!,
field
:
String
!):
ClientBulkImportSourceGroup
!
}
app/views/import/bulk_imports/status.html.haml
View file @
6a31a37b
...
...
@@ -2,9 +2,6 @@
-
add_page_specific_style
'page_bundles/import'
-
breadcrumb_title
_
(
'Import groups'
)
%h1
.gl-my-0.gl-py-4.gl-font-size-h1.gl-border-solid.gl-border-gray-200.gl-border-0.gl-border-b-1
=
s_
(
'BulkImport|Import groups from GitLab'
)
#import-groups-mount-element
{
data:
{
status_path:
status_import_bulk_imports_path
(
format: :json
),
available_namespaces_path:
import_available_namespaces_path
(
format: :json
),
create_bulk_import_path:
import_bulk_imports_path
(
format: :json
),
...
...
changelogs/unreleased/xanf-bulk-import-all-on-the-page.yml
0 → 100644
View file @
6a31a37b
---
title
:
Implement bulk import for all groups on the page
merge_request
:
61097
author
:
type
:
added
locale/gitlab.pot
View file @
6a31a37b
...
...
@@ -5451,6 +5451,9 @@ msgstr ""
msgid "BulkImport|From source group"
msgstr ""
msgid "BulkImport|Import %{groups}"
msgstr ""
msgid "BulkImport|Import failed: Destination cannot be a subgroup of the source group. Change the destination and try again."
msgstr ""
...
...
@@ -5463,9 +5466,15 @@ msgstr ""
msgid "BulkImport|Name already exists."
msgstr ""
msgid "BulkImport|No groups on this page are available for import"
msgstr ""
msgid "BulkImport|No parent"
msgstr ""
msgid "BulkImport|One or more groups has validation errors"
msgstr ""
msgid "BulkImport|Showing %{start}-%{end} of %{total}"
msgstr ""
...
...
spec/frontend/import_entities/import_groups/components/import_table_row_spec.js
View file @
6a31a37b
...
...
@@ -19,6 +19,7 @@ const getFakeGroup = (status) => ({
new_name
:
'
group1
'
,
},
id
:
1
,
validation_errors
:
[],
progress
:
{
status
},
});
...
...
@@ -187,21 +188,25 @@ describe('import table row', () => {
expect
(
wrapper
.
text
()).
toContain
(
'
Please choose a group URL with no special characters.
'
);
});
it
(
'
Reports invalid group name if group already exists
'
,
async
()
=>
{
it
(
'
Reports invalid group name if relevant validation error exists
'
,
async
()
=>
{
const
FAKE_ERROR_MESSAGE
=
'
fake error
'
;
createComponent
({
group
:
{
...
getFakeGroup
(
STATUSES
.
NONE
),
import_target
:
{
target_namespace
:
EXISTING_GROUP_TARGET_NAMESPACE
,
new_name
:
EXISTING_GROUP_PATH
,
validation_errors
:
[
{
field
:
'
new_name
'
,
message
:
FAKE_ERROR_MESSAGE
,
},
],
},
});
jest
.
runOnlyPendingTimers
();
await
nextTick
();
expect
(
wrapper
.
text
()).
toContain
(
'
Name already exists.
'
);
expect
(
wrapper
.
text
()).
toContain
(
FAKE_ERROR_MESSAGE
);
});
});
});
spec/frontend/import_entities/import_groups/components/import_table_spec.js
View file @
6a31a37b
import
{
GlButton
,
GlEmptyState
,
GlLoadingIcon
,
GlSearchBoxByClick
,
...
...
@@ -14,7 +15,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import
{
STATUSES
}
from
'
~/import_entities/constants
'
;
import
ImportTable
from
'
~/import_entities/import_groups/components/import_table.vue
'
;
import
ImportTableRow
from
'
~/import_entities/import_groups/components/import_table_row.vue
'
;
import
importGroup
Mutation
from
'
~/import_entities/import_groups/graphql/mutations/import_group
.mutation.graphql
'
;
import
importGroup
sMutation
from
'
~/import_entities/import_groups/graphql/mutations/import_groups
.mutation.graphql
'
;
import
setNewNameMutation
from
'
~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql
'
;
import
setTargetNamespaceMutation
from
'
~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql
'
;
import
PaginationLinks
from
'
~/vue_shared/components/pagination_links.vue
'
;
...
...
@@ -40,6 +41,7 @@ describe('import table', () => {
];
const
FAKE_PAGE_INFO
=
{
page
:
1
,
perPage
:
20
,
total
:
40
,
totalPages
:
2
};
const
findImportAllButton
=
()
=>
wrapper
.
find
(
'
h1
'
).
find
(
GlButton
);
const
findPaginationDropdown
=
()
=>
wrapper
.
findComponent
(
GlDropdown
);
const
findPaginationDropdownText
=
()
=>
findPaginationDropdown
().
find
({
ref
:
'
text
'
}).
text
();
...
...
@@ -72,7 +74,6 @@ describe('import table', () => {
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
it
(
'
renders loading icon while performing request
'
,
async
()
=>
{
...
...
@@ -141,7 +142,7 @@ describe('import table', () => {
event | payload | mutation | variables
${
'
update-target-namespace
'
}
|
${
'
new-namespace
'
}
|
${
setTargetNamespaceMutation
}
|
${{
sourceGroupId
:
FAKE_GROUP
.
id
,
targetNamespace
:
'
new-namespace
'
}
}
${
'
update-new-name
'
}
|
${
'
new-name
'
}
|
${
setNewNameMutation
}
|
${{
sourceGroupId
:
FAKE_GROUP
.
id
,
newName
:
'
new-name
'
}
}
${
'
import-group
'
}
|
${
undefined
}
|
${
importGroup
Mutation
}
|
${{
sourceGroupId
:
FAKE_GROUP
.
id
}
}
${
'
import-group
'
}
|
${
undefined
}
|
${
importGroup
sMutation
}
|
${{
sourceGroupIds
:
[
FAKE_GROUP
.
id
]
}
}
`
(
'
correctly maps $event to mutation
'
,
async
({
event
,
payload
,
mutation
,
variables
})
=>
{
jest
.
spyOn
(
apolloProvider
.
defaultClient
,
'
mutate
'
);
wrapper
.
find
(
ImportTableRow
).
vm
.
$emit
(
event
,
payload
);
...
...
@@ -277,4 +278,66 @@ describe('import table', () => {
);
});
});
describe
(
'
import all button
'
,
()
=>
{
it
(
'
does not exists when no groups available
'
,
()
=>
{
createComponent
({
bulkImportSourceGroups
:
()
=>
new
Promise
(()
=>
{}),
});
expect
(
findImportAllButton
().
exists
()).
toBe
(
false
);
});
it
(
'
exists when groups are available for import
'
,
async
()
=>
{
createComponent
({
bulkImportSourceGroups
:
()
=>
({
nodes
:
FAKE_GROUPS
,
pageInfo
:
FAKE_PAGE_INFO
,
}),
});
await
waitForPromises
();
expect
(
findImportAllButton
().
exists
()).
toBe
(
true
);
});
it
(
'
counts only not-imported groups
'
,
async
()
=>
{
const
NEW_GROUPS
=
[
generateFakeEntry
({
id
:
1
,
status
:
STATUSES
.
NONE
}),
generateFakeEntry
({
id
:
2
,
status
:
STATUSES
.
NONE
}),
generateFakeEntry
({
id
:
3
,
status
:
STATUSES
.
FINISHED
}),
];
createComponent
({
bulkImportSourceGroups
:
()
=>
({
nodes
:
NEW_GROUPS
,
pageInfo
:
FAKE_PAGE_INFO
,
}),
});
await
waitForPromises
();
expect
(
findImportAllButton
().
text
()).
toMatchInterpolatedText
(
'
Import 2 groups
'
);
});
it
(
'
disables button when any group has validation errors
'
,
async
()
=>
{
const
NEW_GROUPS
=
[
generateFakeEntry
({
id
:
1
,
status
:
STATUSES
.
NONE
}),
generateFakeEntry
({
id
:
2
,
status
:
STATUSES
.
NONE
,
validation_errors
:
[{
field
:
'
new_name
'
,
message
:
'
test validation error
'
}],
}),
generateFakeEntry
({
id
:
3
,
status
:
STATUSES
.
FINISHED
}),
];
createComponent
({
bulkImportSourceGroups
:
()
=>
({
nodes
:
NEW_GROUPS
,
pageInfo
:
FAKE_PAGE_INFO
,
}),
});
await
waitForPromises
();
expect
(
findImportAllButton
().
props
().
disabled
).
toBe
(
true
);
});
});
});
spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
View file @
6a31a37b
...
...
@@ -8,7 +8,9 @@ import {
clientTypenames
,
createResolvers
,
}
from
'
~/import_entities/import_groups/graphql/client_factory
'
;
import
importGroupMutation
from
'
~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql
'
;
import
addValidationErrorMutation
from
'
~/import_entities/import_groups/graphql/mutations/add_validation_error.mutation.graphql
'
;
import
importGroupsMutation
from
'
~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
'
;
import
removeValidationErrorMutation
from
'
~/import_entities/import_groups/graphql/mutations/remove_validation_error.mutation.graphql
'
;
import
setImportProgressMutation
from
'
~/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql
'
;
import
setNewNameMutation
from
'
~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql
'
;
import
setTargetNamespaceMutation
from
'
~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql
'
;
...
...
@@ -240,6 +242,7 @@ describe('Bulk import resolvers', () => {
target_namespace
:
'
root
'
,
new_name
:
'
group1
'
,
},
validation_errors
:
[],
},
],
pageInfo
:
{
...
...
@@ -294,8 +297,8 @@ describe('Bulk import resolvers', () => {
axiosMockAdapter
.
onPost
(
FAKE_ENDPOINTS
.
createBulkImport
).
reply
(()
=>
new
Promise
(()
=>
{}));
client
.
mutate
({
mutation
:
importGroupMutation
,
variables
:
{
sourceGroupId
:
GROUP_ID
},
mutation
:
importGroup
s
Mutation
,
variables
:
{
sourceGroupId
s
:
[
GROUP_ID
]
},
});
await
waitForPromises
();
...
...
@@ -325,8 +328,8 @@ describe('Bulk import resolvers', () => {
it
(
'
sets import status to CREATED when request completes
'
,
async
()
=>
{
axiosMockAdapter
.
onPost
(
FAKE_ENDPOINTS
.
createBulkImport
).
reply
(
httpStatus
.
OK
,
{
id
:
1
});
await
client
.
mutate
({
mutation
:
importGroupMutation
,
variables
:
{
sourceGroupId
:
GROUP_ID
},
mutation
:
importGroup
s
Mutation
,
variables
:
{
sourceGroupId
s
:
[
GROUP_ID
]
},
});
await
waitForPromises
();
...
...
@@ -340,8 +343,8 @@ describe('Bulk import resolvers', () => {
client
.
mutate
({
mutation
:
importGroupMutation
,
variables
:
{
sourceGroupId
:
GROUP_ID
},
mutation
:
[
importGroupsMutation
]
,
variables
:
{
sourceGroupId
s
:
[
GROUP_ID
]
},
})
.
catch
(()
=>
{});
await
waitForPromises
();
...
...
@@ -357,8 +360,8 @@ describe('Bulk import resolvers', () => {
client
.
mutate
({
mutation
:
importGroupMutation
,
variables
:
{
sourceGroupId
:
GROUP_ID
},
mutation
:
importGroup
s
Mutation
,
variables
:
{
sourceGroupId
s
:
[
GROUP_ID
]
},
})
.
catch
(()
=>
{});
await
waitForPromises
();
...
...
@@ -375,8 +378,8 @@ describe('Bulk import resolvers', () => {
client
.
mutate
({
mutation
:
importGroupMutation
,
variables
:
{
sourceGroupId
:
GROUP_ID
},
mutation
:
importGroup
s
Mutation
,
variables
:
{
sourceGroupId
s
:
[
GROUP_ID
]
},
})
.
catch
(()
=>
{});
await
waitForPromises
();
...
...
@@ -418,5 +421,41 @@ describe('Bulk import resolvers', () => {
status
:
NEW_STATUS
,
});
});
it
(
'
addValidationError adds error to group
'
,
async
()
=>
{
const
FAKE_FIELD
=
'
some-field
'
;
const
FAKE_MESSAGE
=
'
some-message
'
;
const
{
data
:
{
addValidationError
:
{
validation_errors
:
validationErrors
},
},
}
=
await
client
.
mutate
({
mutation
:
addValidationErrorMutation
,
variables
:
{
sourceGroupId
:
GROUP_ID
,
field
:
FAKE_FIELD
,
message
:
FAKE_MESSAGE
},
});
expect
(
validationErrors
).
toMatchObject
([{
field
:
FAKE_FIELD
,
message
:
FAKE_MESSAGE
}]);
});
it
(
'
removeValidationError removes error from group
'
,
async
()
=>
{
const
FAKE_FIELD
=
'
some-field
'
;
const
FAKE_MESSAGE
=
'
some-message
'
;
await
client
.
mutate
({
mutation
:
addValidationErrorMutation
,
variables
:
{
sourceGroupId
:
GROUP_ID
,
field
:
FAKE_FIELD
,
message
:
FAKE_MESSAGE
},
});
const
{
data
:
{
removeValidationError
:
{
validation_errors
:
validationErrors
},
},
}
=
await
client
.
mutate
({
mutation
:
removeValidationErrorMutation
,
variables
:
{
sourceGroupId
:
GROUP_ID
,
field
:
FAKE_FIELD
},
});
expect
(
validationErrors
).
toMatchObject
([]);
});
});
});
spec/frontend/import_entities/import_groups/graphql/fixtures.js
View file @
6a31a37b
...
...
@@ -14,6 +14,7 @@ export const generateFakeEntry = ({ id, status, ...rest }) => ({
id
:
`test-
${
id
}
`
,
status
,
},
validation_errors
:
[],
...
rest
,
});
...
...
spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
View file @
6a31a37b
...
...
@@ -22,33 +22,42 @@ describe('SourceGroupsManager', () => {
const
IMPORT_ID
=
1
;
const
IMPORT_TARGET
=
{
destination_name
:
'
demo
'
,
destination_namespace
:
'
foo
'
};
const
STATUS
=
'
FAKE_STATUS
'
;
const
FAKE_GROUP
=
{
id
:
1
,
import
T
arget
:
IMPORT_TARGET
,
status
:
STATUS
};
const
FAKE_GROUP
=
{
id
:
1
,
import
_t
arget
:
IMPORT_TARGET
,
status
:
STATUS
};
it
(
'
loads state from storage on creation
'
,
()
=>
{
expect
(
storage
.
getItem
).
toHaveBeenCalledWith
(
KEY
);
});
it
(
'
saves to storage when saveImportState is called
'
,
()
=>
{
manager
.
saveImportState
(
IMPORT_ID
,
FAKE_GROUP
);
it
(
'
saves to storage when createImportState is called
'
,
()
=>
{
const
FAKE_STATUS
=
'
fake;
'
;
manager
.
createImportState
(
IMPORT_ID
,
{
status
:
FAKE_STATUS
,
groups
:
[
FAKE_GROUP
]
});
const
storedObject
=
JSON
.
parse
(
storage
.
setItem
.
mock
.
calls
[
0
][
1
]);
expect
(
Object
.
values
(
storedObject
)[
0
]).
toStrictEqual
({
status
:
FAKE_STATUS
,
groups
:
[
{
id
:
FAKE_GROUP
.
id
,
importTarget
:
IMPORT_TARGET
,
status
:
STATUS
,
},
],
});
});
it
(
'
updates storage when previous state is available
'
,
()
=>
{
const
CHANGED_STATUS
=
'
changed
'
;
manager
.
saveImportState
(
IMPORT_ID
,
FAKE_GROUP
);
manager
.
createImportState
(
IMPORT_ID
,
{
status
:
STATUS
,
groups
:
[
FAKE_GROUP
]
}
);
manager
.
saveImportState
(
IMPORT_ID
,
{
status
:
CHANGED_STATUS
}
);
manager
.
updateImportProgress
(
IMPORT_ID
,
CHANGED_STATUS
);
const
storedObject
=
JSON
.
parse
(
storage
.
setItem
.
mock
.
calls
[
1
][
1
]);
expect
(
Object
.
values
(
storedObject
)[
0
]).
toStrictEqual
({
status
:
CHANGED_STATUS
,
groups
:
[
{
id
:
FAKE_GROUP
.
id
,
importTarget
:
IMPORT_TARGET
,
status
:
CHANGED_STATUS
,
},
],
});
});
});
...
...
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