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
a54340a9
Commit
a54340a9
authored
Feb 18, 2021
by
Illya Klymov
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Store group import data in localStorage
* persist importState in localStorage
parent
9a319a49
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
227 additions
and
64 deletions
+227
-64
app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
...s/import_entities/import_groups/graphql/client_factory.js
+45
-27
app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
...s/import_groups/graphql/services/source_groups_manager.js
+76
-7
app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
..._entities/import_groups/graphql/services/status_poller.js
+3
-9
app/assets/javascripts/import_entities/import_groups/index.js
...assets/javascripts/import_entities/import_groups/index.js
+1
-0
app/assets/javascripts/vue_shared/components/select2_select.vue
...sets/javascripts/vue_shared/components/select2_select.vue
+6
-0
spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
...ort_entities/import_groups/graphql/client_factory_spec.js
+37
-4
spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
...ort_groups/graphql/services/source_groups_manager_spec.js
+53
-2
spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
...ties/import_groups/graphql/services/status_poller_spec.js
+6
-15
No files found.
app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
View file @
a54340a9
...
...
@@ -15,52 +15,71 @@ export const clientTypenames = {
BulkImportPageInfo
:
'
ClientBulkImportPageInfo
'
,
};
export
function
createResolvers
({
endpoints
})
{
export
function
createResolvers
({
endpoints
,
sourceUrl
,
GroupsManager
=
SourceGroupsManager
})
{
let
statusPoller
;
let
sourceGroupManager
;
const
getGroupsManager
=
(
client
)
=>
{
if
(
!
sourceGroupManager
)
{
sourceGroupManager
=
new
GroupsManager
({
client
,
sourceUrl
});
}
return
sourceGroupManager
;
};
return
{
Query
:
{
async
bulkImportSourceGroups
(
_
,
vars
,
{
client
})
{
const
{
data
:
{
availableNamespaces
},
}
=
await
client
.
query
({
query
:
availableNamespacesQuery
});
if
(
!
statusPoller
)
{
statusPoller
=
new
StatusPoller
({
client
,
groupManager
:
getGroupsManager
(
client
)
,
pollPath
:
endpoints
.
jobs
,
});
statusPoller
.
startPolling
();
}
return
axios
.
get
(
endpoints
.
status
,
{
const
groupsManager
=
getGroupsManager
(
client
);
return
Promise
.
all
([
axios
.
get
(
endpoints
.
status
,
{
params
:
{
page
:
vars
.
page
,
per_page
:
vars
.
perPage
,
filter
:
vars
.
filter
,
},
})
.
then
(({
headers
,
data
})
=>
{
}),
client
.
query
({
query
:
availableNamespacesQuery
}),
]).
then
(
([
{
headers
,
data
},
{
data
:
{
availableNamespaces
},
},
])
=>
{
const
pagination
=
parseIntPagination
(
normalizeHeaders
(
headers
));
return
{
__typename
:
clientTypenames
.
BulkImportSourceGroupConnection
,
nodes
:
data
.
importable_data
.
map
((
group
)
=>
({
__typename
:
clientTypenames
.
BulkImportSourceGroup
,
...
group
,
status
:
STATUSES
.
NONE
,
import_target
:
{
new_name
:
group
.
full_path
,
target_namespace
:
availableNamespaces
[
0
]?.
full_path
??
''
,
},
})),
nodes
:
data
.
importable_data
.
map
((
group
)
=>
{
const
cachedImportState
=
groupsManager
.
getImportStateFromStorageByGroupId
(
group
.
id
,
);
return
{
__typename
:
clientTypenames
.
BulkImportSourceGroup
,
...
group
,
status
:
cachedImportState
?.
status
??
STATUSES
.
NONE
,
import_target
:
cachedImportState
?.
importTarget
??
{
new_name
:
group
.
full_path
,
target_namespace
:
availableNamespaces
[
0
]?.
full_path
??
''
,
},
};
}),
pageInfo
:
{
__typename
:
clientTypenames
.
BulkImportPageInfo
,
...
pagination
,
},
};
});
},
);
},
availableNamespaces
:
()
=>
...
...
@@ -73,21 +92,21 @@ export function createResolvers({ endpoints }) {
},
Mutation
:
{
setTargetNamespace
(
_
,
{
targetNamespace
,
sourceGroupId
},
{
client
})
{
new
SourceGroupsManager
({
client
}
).
updateById
(
sourceGroupId
,
(
sourceGroup
)
=>
{
getGroupsManager
(
client
).
updateById
(
sourceGroupId
,
(
sourceGroup
)
=>
{
// eslint-disable-next-line no-param-reassign
sourceGroup
.
import_target
.
target_namespace
=
targetNamespace
;
});
},
setNewName
(
_
,
{
newName
,
sourceGroupId
},
{
client
})
{
new
SourceGroupsManager
({
client
}
).
updateById
(
sourceGroupId
,
(
sourceGroup
)
=>
{
getGroupsManager
(
client
).
updateById
(
sourceGroupId
,
(
sourceGroup
)
=>
{
// eslint-disable-next-line no-param-reassign
sourceGroup
.
import_target
.
new_name
=
newName
;
});
},
async
importGroup
(
_
,
{
sourceGroupId
},
{
client
})
{
const
groupManager
=
new
SourceGroupsManager
({
client
}
);
const
groupManager
=
getGroupsManager
(
client
);
const
group
=
groupManager
.
findById
(
sourceGroupId
);
groupManager
.
setImportStatus
(
group
,
STATUSES
.
SCHEDULING
);
try
{
...
...
@@ -101,8 +120,7 @@ export function createResolvers({ endpoints }) {
},
],
});
groupManager
.
setImportStatus
(
group
,
STATUSES
.
STARTED
);
SourceGroupsManager
.
attachImportId
(
group
,
response
.
data
.
id
);
groupManager
.
startImport
({
group
,
importId
:
response
.
data
.
id
});
}
catch
(
e
)
{
createFlash
({
message
:
s__
(
'
BulkImport|Importing the group failed
'
),
...
...
@@ -116,5 +134,5 @@ export function createResolvers({ endpoints }) {
};
}
export
const
createApolloClient
=
({
endpoints
})
=>
createDefaultClient
(
createResolvers
({
endpoints
}),
{
assumeImmutableResults
:
true
});
export
const
createApolloClient
=
({
sourceUrl
,
endpoints
})
=>
createDefaultClient
(
createResolvers
({
sourceUrl
,
endpoints
}),
{
assumeImmutableResults
:
true
});
app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
View file @
a54340a9
import
{
defaultDataIdFromObject
}
from
'
apollo-cache-inmemory
'
;
import
produce
from
'
immer
'
;
import
{
debounce
,
merge
}
from
'
lodash
'
;
import
{
STATUSES
}
from
'
../../../constants
'
;
import
ImportSourceGroupFragment
from
'
../fragments/bulk_import_source_group_item.fragment.graphql
'
;
function
extractTypeConditionFromFragment
(
fragment
)
{
...
...
@@ -13,15 +15,24 @@ function generateGroupId(id) {
});
}
export
const
KEY
=
'
gl-bulk-imports-import-state
'
;
export
const
DEBOUNCE_INTERVAL
=
200
;
export
class
SourceGroupsManager
{
static
importMap
=
new
Map
();
constructor
({
client
,
sourceUrl
,
storage
=
window
.
localStorage
})
{
this
.
client
=
client
;
this
.
sourceUrl
=
sourceUrl
;
static
attachImportId
(
group
,
importId
)
{
SourceGroupsManager
.
importMap
.
set
(
importId
,
group
.
id
);
this
.
storage
=
storage
;
this
.
importStates
=
this
.
loadImportStatesFromStorage
(
);
}
constructor
({
client
})
{
this
.
client
=
client
;
loadImportStatesFromStorage
()
{
try
{
return
JSON
.
parse
(
this
.
storage
.
getItem
(
KEY
))
??
{};
}
catch
{
return
{};
}
}
findById
(
id
)
{
...
...
@@ -42,8 +53,48 @@ export class SourceGroupsManager {
this
.
update
(
group
,
fn
);
}
findByImportId
(
importId
)
{
return
this
.
findById
(
SourceGroupsManager
.
importMap
.
get
(
importId
));
saveImportState
(
importId
,
group
)
{
this
.
importStates
[
this
.
getStorageKey
(
importId
)]
=
{
id
:
group
.
id
,
importTarget
:
group
.
import_target
,
status
:
group
.
status
,
};
this
.
saveImportStatesToStorage
();
}
getImportStateFromStorage
(
importId
)
{
return
this
.
importStates
[
this
.
getStorageKey
(
importId
)];
}
getImportStateFromStorageByGroupId
(
groupId
)
{
const
PREFIX
=
this
.
getStorageKey
(
''
);
const
[,
importState
]
=
Object
.
entries
(
this
.
importStates
).
find
(
([
key
,
group
])
=>
key
.
startsWith
(
PREFIX
)
&&
group
.
id
===
groupId
,
)
??
[];
return
importState
;
}
getStorageKey
(
importId
)
{
return
`
${
this
.
sourceUrl
}
|
${
importId
}
`
;
}
saveImportStatesToStorage
=
debounce
(()
=>
{
try
{
// storage might be changed in other tab so fetch first
this
.
storage
.
setItem
(
KEY
,
JSON
.
stringify
(
merge
({},
this
.
loadImportStatesFromStorage
(),
this
.
importStates
)),
);
}
catch
{
// empty catch intentional: storage might be unavailable or full
}
},
DEBOUNCE_INTERVAL
);
startImport
({
group
,
importId
})
{
this
.
saveImportState
(
importId
,
group
);
this
.
setImportStatus
(
group
,
STATUSES
.
STARTED
);
}
setImportStatus
(
group
,
status
)
{
...
...
@@ -52,4 +103,22 @@ export class SourceGroupsManager {
sourceGroup
.
status
=
status
;
});
}
setImportStatusByImportId
(
importId
,
status
)
{
const
importState
=
this
.
getImportStateFromStorage
(
importId
);
if
(
!
importState
)
{
return
;
}
if
(
importState
.
status
!==
status
)
{
importState
.
status
=
status
;
}
const
group
=
this
.
findById
(
importState
.
id
);
if
(
group
?.
id
)
{
this
.
setImportStatus
(
group
,
status
);
}
this
.
saveImportStatesToStorage
();
}
}
app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js
View file @
a54340a9
...
...
@@ -3,12 +3,9 @@ import createFlash from '~/flash';
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
Poll
from
'
~/lib/utils/poll
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
SourceGroupsManager
}
from
'
./source_groups_manager
'
;
export
class
StatusPoller
{
constructor
({
client
,
pollPath
})
{
this
.
client
=
client
;
constructor
({
groupManager
,
pollPath
})
{
this
.
eTagPoll
=
new
Poll
({
resource
:
{
fetchJobs
:
()
=>
axios
.
get
(
pollPath
),
...
...
@@ -29,7 +26,7 @@ export class StatusPoller {
}
});
this
.
groupManager
=
new
SourceGroupsManager
({
client
})
;
this
.
groupManager
=
groupManager
;
}
startPolling
()
{
...
...
@@ -38,10 +35,7 @@ export class StatusPoller {
async
updateImportsStatuses
(
importStatuses
)
{
importStatuses
.
forEach
(({
id
,
status_name
:
statusName
})
=>
{
const
group
=
this
.
groupManager
.
findByImportId
(
id
);
if
(
group
.
id
)
{
this
.
groupManager
.
setImportStatus
(
group
,
statusName
);
}
this
.
groupManager
.
setImportStatusByImportId
(
id
,
statusName
);
});
}
}
app/assets/javascripts/import_entities/import_groups/index.js
View file @
a54340a9
...
...
@@ -21,6 +21,7 @@ export function mountImportGroupsApp(mountElement) {
}
=
mountElement
.
dataset
;
const
apolloProvider
=
new
VueApollo
({
defaultClient
:
createApolloClient
({
sourceUrl
,
endpoints
:
{
status
:
statusPath
,
availableNamespaces
:
availableNamespacesPath
,
...
...
app/assets/javascripts/vue_shared/components/select2_select.vue
View file @
a54340a9
...
...
@@ -20,6 +20,12 @@ export default {
},
},
watch
:
{
value
()
{
$
(
this
.
$refs
.
dropdownInput
).
val
(
this
.
value
).
trigger
(
'
change
'
);
},
},
mounted
()
{
loadCSSFile
(
gon
.
select2_css_path
)
.
then
(()
=>
{
...
...
spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
View file @
a54340a9
...
...
@@ -35,15 +35,19 @@ describe('Bulk import resolvers', () => {
let
axiosMockAdapter
;
let
client
;
beforeEach
(()
=>
{
axiosMockAdapter
=
new
MockAdapter
(
axios
);
client
=
createMockClient
({
const
createClient
=
(
extraResolverArgs
)
=>
{
return
createMockClient
({
cache
:
new
InMemoryCache
({
fragmentMatcher
:
{
match
:
()
=>
true
},
addTypename
:
false
,
}),
resolvers
:
createResolvers
({
endpoints
:
FAKE_ENDPOINTS
}),
resolvers
:
createResolvers
({
endpoints
:
FAKE_ENDPOINTS
,
...
extraResolverArgs
}),
});
};
beforeEach
(()
=>
{
axiosMockAdapter
=
new
MockAdapter
(
axios
);
client
=
createClient
();
});
afterEach
(()
=>
{
...
...
@@ -82,6 +86,35 @@ describe('Bulk import resolvers', () => {
.
reply
(
httpStatus
.
OK
,
availableNamespacesFixture
);
});
it
(
'
respects cached import state when provided by group manager
'
,
async
()
=>
{
const
FAKE_STATUS
=
'
DEMO_STATUS
'
;
const
FAKE_IMPORT_TARGET
=
{};
const
TARGET_INDEX
=
0
;
const
clientWithMockedManager
=
createClient
({
GroupsManager
:
jest
.
fn
().
mockImplementation
(()
=>
({
getImportStateFromStorageByGroupId
(
groupId
)
{
if
(
groupId
===
statusEndpointFixture
.
importable_data
[
TARGET_INDEX
].
id
)
{
return
{
status
:
FAKE_STATUS
,
importTarget
:
FAKE_IMPORT_TARGET
,
};
}
return
null
;
},
})),
});
const
clientResponse
=
await
clientWithMockedManager
.
query
({
query
:
bulkImportSourceGroupsQuery
,
});
const
clientResults
=
clientResponse
.
data
.
bulkImportSourceGroups
.
nodes
;
expect
(
clientResults
[
TARGET_INDEX
].
import_target
).
toBe
(
FAKE_IMPORT_TARGET
);
expect
(
clientResults
[
TARGET_INDEX
].
status
).
toBe
(
FAKE_STATUS
);
});
it
(
'
populates each result instance with empty import_target when there are no available namespaces
'
,
async
()
=>
{
axiosMockAdapter
.
onGet
(
FAKE_ENDPOINTS
.
availableNamespaces
).
reply
(
httpStatus
.
OK
,
[]);
...
...
spec/frontend/import_entities/import_groups/graphql/services/source_groups_manager_spec.js
View file @
a54340a9
import
{
defaultDataIdFromObject
}
from
'
apollo-cache-inmemory
'
;
import
{
clientTypenames
}
from
'
~/import_entities/import_groups/graphql/client_factory
'
;
import
ImportSourceGroupFragment
from
'
~/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
'
;
import
{
SourceGroupsManager
}
from
'
~/import_entities/import_groups/graphql/services/source_groups_manager
'
;
import
{
KEY
,
SourceGroupsManager
,
}
from
'
~/import_entities/import_groups/graphql/services/source_groups_manager
'
;
const
FAKE_SOURCE_URL
=
'
http://demo.host
'
;
describe
(
'
SourceGroupsManager
'
,
()
=>
{
let
manager
;
let
client
;
let
storage
;
const
getFakeGroup
=
()
=>
({
__typename
:
clientTypenames
.
BulkImportSourceGroup
,
...
...
@@ -17,8 +23,53 @@ describe('SourceGroupsManager', () => {
readFragment
:
jest
.
fn
(),
writeFragment
:
jest
.
fn
(),
};
storage
=
{
getItem
:
jest
.
fn
(),
setItem
:
jest
.
fn
(),
};
manager
=
new
SourceGroupsManager
({
client
,
storage
,
sourceUrl
:
FAKE_SOURCE_URL
});
});
describe
(
'
storage management
'
,
()
=>
{
const
IMPORT_ID
=
1
;
const
IMPORT_TARGET
=
{
destination_name
:
'
demo
'
,
destination_namespace
:
'
foo
'
};
const
STATUS
=
'
FAKE_STATUS
'
;
const
FAKE_GROUP
=
{
id
:
1
,
import_target
:
IMPORT_TARGET
,
status
:
STATUS
};
it
(
'
loads state from storage on creation
'
,
()
=>
{
expect
(
storage
.
getItem
).
toHaveBeenCalledWith
(
KEY
);
});
it
(
'
saves to storage when import is starting
'
,
()
=>
{
manager
.
startImport
({
importId
:
IMPORT_ID
,
group
:
FAKE_GROUP
,
});
const
storedObject
=
JSON
.
parse
(
storage
.
setItem
.
mock
.
calls
[
0
][
1
]);
expect
(
Object
.
values
(
storedObject
)[
0
]).
toStrictEqual
({
id
:
FAKE_GROUP
.
id
,
importTarget
:
IMPORT_TARGET
,
status
:
STATUS
,
});
});
manager
=
new
SourceGroupsManager
({
client
});
it
(
'
saves to storage when import status is updated
'
,
()
=>
{
const
CHANGED_STATUS
=
'
changed
'
;
manager
.
startImport
({
importId
:
IMPORT_ID
,
group
:
FAKE_GROUP
,
});
manager
.
setImportStatusByImportId
(
IMPORT_ID
,
CHANGED_STATUS
);
const
storedObject
=
JSON
.
parse
(
storage
.
setItem
.
mock
.
calls
[
1
][
1
]);
expect
(
Object
.
values
(
storedObject
)[
0
]).
toStrictEqual
({
id
:
FAKE_GROUP
.
id
,
importTarget
:
IMPORT_TARGET
,
status
:
CHANGED_STATUS
,
});
});
});
it
(
'
finds item by group id
'
,
()
=>
{
...
...
spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
View file @
a54340a9
...
...
@@ -2,7 +2,6 @@ import MockAdapter from 'axios-mock-adapter';
import
Visibility
from
'
visibilityjs
'
;
import
createFlash
from
'
~/flash
'
;
import
{
STATUSES
}
from
'
~/import_entities/constants
'
;
import
{
SourceGroupsManager
}
from
'
~/import_entities/import_groups/graphql/services/source_groups_manager
'
;
import
{
StatusPoller
}
from
'
~/import_entities/import_groups/graphql/services/status_poller
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
Poll
from
'
~/lib/utils/poll
'
;
...
...
@@ -18,24 +17,21 @@ jest.mock('~/import_entities/import_groups/graphql/services/source_groups_manage
}));
const
FAKE_POLL_PATH
=
'
/fake/poll/path
'
;
const
CLIENT_MOCK
=
{};
describe
(
'
Bulk import status poller
'
,
()
=>
{
let
poller
;
let
mockAdapter
;
let
groupManager
;
const
getPollHistory
=
()
=>
mockAdapter
.
history
.
get
.
filter
((
x
)
=>
x
.
url
===
FAKE_POLL_PATH
);
beforeEach
(()
=>
{
mockAdapter
=
new
MockAdapter
(
axios
);
mockAdapter
.
onGet
(
FAKE_POLL_PATH
).
reply
(
200
,
{});
poller
=
new
StatusPoller
({
client
:
CLIENT_MOCK
,
pollPath
:
FAKE_POLL_PATH
});
});
it
(
'
creates source group manager with proper client
'
,
()
=>
{
expect
(
SourceGroupsManager
.
mock
.
calls
).
toHaveLength
(
1
);
const
[[{
client
}]]
=
SourceGroupsManager
.
mock
.
calls
;
expect
(
client
).
toBe
(
CLIENT_MOCK
);
groupManager
=
{
setImportStatusByImportId
:
jest
.
fn
(),
};
poller
=
new
StatusPoller
({
groupManager
,
pollPath
:
FAKE_POLL_PATH
});
});
it
(
'
creates poller with proper config
'
,
()
=>
{
...
...
@@ -100,14 +96,9 @@ describe('Bulk import status poller', () => {
it
(
'
when success response arrives updates relevant group status
'
,
()
=>
{
const
FAKE_ID
=
5
;
const
[[
pollConfig
]]
=
Poll
.
mock
.
calls
;
const
[
managerInstance
]
=
SourceGroupsManager
.
mock
.
instances
;
managerInstance
.
findByImportId
.
mockReturnValue
({
id
:
FAKE_ID
});
pollConfig
.
successCallback
({
data
:
[{
id
:
FAKE_ID
,
status_name
:
STATUSES
.
FINISHED
}]
});
expect
(
managerInstance
.
setImportStatus
).
toHaveBeenCalledWith
(
expect
.
objectContaining
({
id
:
FAKE_ID
}),
STATUSES
.
FINISHED
,
);
expect
(
groupManager
.
setImportStatusByImportId
).
toHaveBeenCalledWith
(
FAKE_ID
,
STATUSES
.
FINISHED
);
});
});
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