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
1ca830d3
Commit
1ca830d3
authored
Sep 13, 2021
by
Olena Horal-Koretska
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Styling updates of the protected environments access dropdown
Due to migration to GlDropdown Changelog: changed EE: true
parent
d2167efa
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
598 additions
and
78 deletions
+598
-78
app/assets/javascripts/projects/settings/access_dropdown.js
app/assets/javascripts/projects/settings/access_dropdown.js
+5
-48
app/assets/javascripts/projects/settings/api/access_dropdown_api.js
.../javascripts/projects/settings/api/access_dropdown_api.js
+45
-0
app/assets/javascripts/projects/settings/components/access_dropdown.vue
...ascripts/projects/settings/components/access_dropdown.vue
+292
-0
app/assets/javascripts/projects/settings/init_access_dropdown.js
...ets/javascripts/projects/settings/init_access_dropdown.js
+28
-0
ee/app/assets/javascripts/protected_environments/protected_environment_create.js
...ts/protected_environments/protected_environment_create.js
+19
-17
ee/app/views/projects/protected_environments/_deploy_access_levels_dropdown.html.haml
...ted_environments/_deploy_access_levels_dropdown.html.haml
+0
-6
ee/app/views/projects/protected_environments/_form.html.haml
ee/app/views/projects/protected_environments/_form.html.haml
+2
-2
ee/spec/features/projects/settings/protected_environments_spec.rb
...features/projects/settings/protected_environments_spec.rb
+3
-5
spec/frontend/projects/settings/components/new_access_dropdown_spec.js
.../projects/settings/components/new_access_dropdown_spec.js
+204
-0
No files found.
app/assets/javascripts/projects/settings/access_dropdown.js
View file @
1ca830d3
...
...
@@ -2,8 +2,8 @@
import
{
escape
,
find
,
countBy
}
from
'
lodash
'
;
import
initDeprecatedJQueryDropdown
from
'
~/deprecated_jquery_dropdown
'
;
import
createFlash
from
'
~/flash
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
n__
,
s__
,
__
,
sprintf
}
from
'
~/locale
'
;
import
{
getUsers
,
getGroups
,
getDeployKeys
}
from
'
./api/access_dropdown_api
'
;
import
{
LEVEL_TYPES
,
LEVEL_ID_PROP
,
ACCESS_LEVELS
,
ACCESS_LEVEL_NONE
}
from
'
./constants
'
;
export
default
class
AccessDropdown
{
...
...
@@ -16,9 +16,6 @@ export default class AccessDropdown {
this
.
accessLevelsData
=
accessLevelsData
.
roles
;
this
.
$dropdown
=
$dropdown
;
this
.
$wrap
=
this
.
$dropdown
.
closest
(
`.
${
this
.
accessLevel
}
-container`
);
this
.
usersPath
=
'
/-/autocomplete/users.json
'
;
this
.
groupsPath
=
'
/-/autocomplete/project_groups.json
'
;
this
.
deployKeysPath
=
'
/-/autocomplete/deploy_keys_with_owners.json
'
;
this
.
defaultLabel
=
this
.
$dropdown
.
data
(
'
defaultLabel
'
);
this
.
setSelectedItems
([]);
...
...
@@ -318,9 +315,9 @@ export default class AccessDropdown {
getData
(
query
,
callback
)
{
if
(
this
.
hasLicense
)
{
Promise
.
all
([
this
.
getDeployKeys
(
query
),
this
.
getUsers
(
query
),
this
.
groupsData
?
Promise
.
resolve
(
this
.
groupsData
)
:
this
.
getGroups
(),
getDeployKeys
(
query
),
getUsers
(
query
),
this
.
groupsData
?
Promise
.
resolve
(
this
.
groupsData
)
:
getGroups
(),
])
.
then
(([
deployKeysResponse
,
usersResponse
,
groupsResponse
])
=>
{
this
.
groupsData
=
groupsResponse
;
...
...
@@ -332,7 +329,7 @@ export default class AccessDropdown {
createFlash
({
message
:
__
(
'
Failed to load groups, users and deploy keys.
'
)
});
});
}
else
{
this
.
getDeployKeys
(
query
)
getDeployKeys
(
query
)
.
then
((
deployKeysResponse
)
=>
callback
(
this
.
consolidateData
(
deployKeysResponse
.
data
)))
.
catch
(()
=>
createFlash
({
message
:
__
(
'
Failed to load deploy keys.
'
)
}));
}
...
...
@@ -473,46 +470,6 @@ export default class AccessDropdown {
return
consolidatedData
;
}
getUsers
(
query
)
{
return
axios
.
get
(
this
.
buildUrl
(
gon
.
relative_url_root
,
this
.
usersPath
),
{
params
:
{
search
:
query
,
per_page
:
20
,
active
:
true
,
project_id
:
gon
.
current_project_id
,
push_code
:
true
,
},
});
}
getGroups
()
{
return
axios
.
get
(
this
.
buildUrl
(
gon
.
relative_url_root
,
this
.
groupsPath
),
{
params
:
{
project_id
:
gon
.
current_project_id
,
},
});
}
getDeployKeys
(
query
)
{
return
axios
.
get
(
this
.
buildUrl
(
gon
.
relative_url_root
,
this
.
deployKeysPath
),
{
params
:
{
search
:
query
,
per_page
:
20
,
active
:
true
,
project_id
:
gon
.
current_project_id
,
push_code
:
true
,
},
});
}
buildUrl
(
urlRoot
,
url
)
{
let
newUrl
;
if
(
urlRoot
!=
null
)
{
newUrl
=
urlRoot
.
replace
(
/
\/
$/
,
''
)
+
url
;
}
return
newUrl
;
}
renderRow
(
item
)
{
let
criteria
=
{};
let
groupRowEl
;
...
...
app/assets/javascripts/projects/settings/api/access_dropdown_api.js
0 → 100644
View file @
1ca830d3
import
axios
from
'
~/lib/utils/axios_utils
'
;
const
USERS_PATH
=
'
/-/autocomplete/users.json
'
;
const
GROUPS_PATH
=
'
/-/autocomplete/project_groups.json
'
;
const
DEPLOY_KEYS_PATH
=
'
/-/autocomplete/deploy_keys_with_owners.json
'
;
const
buildUrl
=
(
urlRoot
,
url
)
=>
{
let
newUrl
;
if
(
urlRoot
!=
null
)
{
newUrl
=
urlRoot
.
replace
(
/
\/
$/
,
''
)
+
url
;
}
return
newUrl
;
};
export
const
getUsers
=
(
query
)
=>
{
return
axios
.
get
(
buildUrl
(
gon
.
relative_url_root
||
''
,
USERS_PATH
),
{
params
:
{
search
:
query
,
per_page
:
20
,
active
:
true
,
project_id
:
gon
.
current_project_id
,
push_code
:
true
,
},
});
};
export
const
getGroups
=
()
=>
{
return
axios
.
get
(
buildUrl
(
gon
.
relative_url_root
||
''
,
GROUPS_PATH
),
{
params
:
{
project_id
:
gon
.
current_project_id
,
},
});
};
export
const
getDeployKeys
=
(
query
)
=>
{
return
axios
.
get
(
buildUrl
(
gon
.
relative_url_root
||
''
,
DEPLOY_KEYS_PATH
),
{
params
:
{
search
:
query
,
per_page
:
20
,
active
:
true
,
project_id
:
gon
.
current_project_id
,
push_code
:
true
,
},
});
};
app/assets/javascripts/projects/settings/components/access_dropdown.vue
0 → 100644
View file @
1ca830d3
<
script
>
import
{
GlDropdown
,
GlDropdownItem
,
GlDropdownSectionHeader
,
GlDropdownDivider
,
GlSearchBoxByType
,
GlLoadingIcon
,
GlAvatar
,
GlSprintf
,
}
from
'
@gitlab/ui
'
;
import
{
debounce
}
from
'
lodash
'
;
import
createFlash
from
'
~/flash
'
;
import
{
__
,
s__
,
n__
}
from
'
~/locale
'
;
import
{
getUsers
,
getGroups
,
getDeployKeys
}
from
'
../api/access_dropdown_api
'
;
import
{
LEVEL_TYPES
,
ACCESS_LEVELS
}
from
'
../constants
'
;
export
const
i18n
=
{
selectUsers
:
s__
(
'
ProtectedEnvironment|Select users
'
),
rolesSectionHeader
:
s__
(
'
AccessDropdown|Roles
'
),
groupsSectionHeader
:
s__
(
'
AccessDropdown|Groups
'
),
usersSectionHeader
:
s__
(
'
AccessDropdown|Users
'
),
deployKeysSectionHeader
:
s__
(
'
AccessDropdown|Deploy Keys
'
),
ownedBy
:
__
(
'
Owned by %{image_tag}
'
),
};
export
default
{
i18n
,
components
:
{
GlDropdown
,
GlDropdownItem
,
GlDropdownSectionHeader
,
GlDropdownDivider
,
GlSearchBoxByType
,
GlLoadingIcon
,
GlAvatar
,
GlSprintf
,
},
props
:
{
accessLevelsData
:
{
type
:
Array
,
required
:
true
,
},
accessLevel
:
{
required
:
true
,
type
:
String
,
},
hasLicense
:
{
required
:
false
,
type
:
Boolean
,
default
:
true
,
},
},
data
()
{
return
{
loading
:
false
,
query
:
''
,
users
:
[],
groups
:
[],
roles
:
[],
deployKeys
:
[],
selected
:
{
[
LEVEL_TYPES
.
GROUP
]:
[],
[
LEVEL_TYPES
.
USER
]:
[],
[
LEVEL_TYPES
.
ROLE
]:
[],
[
LEVEL_TYPES
.
DEPLOY_KEY
]:
[],
},
};
},
computed
:
{
showDeployKeys
()
{
return
this
.
accessLevel
===
ACCESS_LEVELS
.
PUSH
&&
this
.
deployKeys
.
length
;
},
toggleLabel
()
{
const
counts
=
Object
.
entries
(
this
.
selected
).
reduce
((
acc
,
[
key
,
value
])
=>
{
acc
[
key
]
=
value
.
length
;
return
acc
;
},
{});
const
isOnlyRoleSelected
=
counts
[
LEVEL_TYPES
.
ROLE
]
===
1
&&
[
counts
[
LEVEL_TYPES
.
USER
],
counts
[
LEVEL_TYPES
.
GROUP
],
counts
[
LEVEL_TYPES
.
DEPLOY_KEY
]].
every
(
(
count
)
=>
count
===
0
,
);
if
(
isOnlyRoleSelected
)
{
return
this
.
selected
[
LEVEL_TYPES
.
ROLE
][
0
].
text
;
}
const
labelPieces
=
[];
if
(
counts
[
LEVEL_TYPES
.
ROLE
]
>
0
)
{
labelPieces
.
push
(
n__
(
'
1 role
'
,
'
%d roles
'
,
counts
[
LEVEL_TYPES
.
ROLE
]));
}
if
(
counts
[
LEVEL_TYPES
.
USER
]
>
0
)
{
labelPieces
.
push
(
n__
(
'
1 user
'
,
'
%d users
'
,
counts
[
LEVEL_TYPES
.
USER
]));
}
if
(
counts
[
LEVEL_TYPES
.
DEPLOY_KEY
]
>
0
)
{
labelPieces
.
push
(
n__
(
'
1 deploy key
'
,
'
%d deploy keys
'
,
counts
[
LEVEL_TYPES
.
DEPLOY_KEY
]));
}
if
(
counts
[
LEVEL_TYPES
.
GROUP
]
>
0
)
{
labelPieces
.
push
(
n__
(
'
1 group
'
,
'
%d groups
'
,
counts
[
LEVEL_TYPES
.
GROUP
]));
}
return
labelPieces
.
join
(
'
,
'
)
||
i18n
.
selectUsers
;
},
toggleClass
()
{
return
this
.
toggleLabel
===
i18n
.
selectUsers
?
'
gl-text-gray-500!
'
:
''
;
},
},
watch
:
{
query
:
debounce
(
function
debouncedSearch
()
{
return
this
.
getData
();
},
500
),
},
created
()
{
this
.
getData
();
},
methods
:
{
focusInput
()
{
this
.
$refs
.
search
.
focusInput
();
},
getData
()
{
this
.
loading
=
true
;
if
(
this
.
hasLicense
)
{
Promise
.
all
([
getDeployKeys
(
this
.
query
),
getUsers
(
this
.
query
),
this
.
groups
.
length
?
Promise
.
resolve
({
data
:
this
.
groups
})
:
getGroups
(),
])
.
then
(([
deployKeysResponse
,
usersResponse
,
groupsResponse
])
=>
this
.
consolidateData
(
deployKeysResponse
.
data
,
usersResponse
.
data
,
groupsResponse
.
data
),
)
.
catch
(()
=>
createFlash
({
message
:
__
(
'
Failed to load groups, users and deploy keys.
'
)
}),
)
.
finally
(()
=>
{
this
.
loading
=
false
;
});
}
else
{
getDeployKeys
(
this
.
query
)
.
then
((
deployKeysResponse
)
=>
this
.
consolidateData
(
deployKeysResponse
.
data
))
.
catch
(()
=>
createFlash
({
message
:
__
(
'
Failed to load deploy keys.
'
)
}))
.
finally
(()
=>
{
this
.
loading
=
false
;
});
}
},
consolidateData
(
deployKeysResponse
,
usersResponse
=
[],
groupsResponse
=
[])
{
// This re-assignment is intentional as level.type property is being used for comparision,
// and accessLevelsData is provided by gon.create_access_levels which doesn't have `type` included.
// See this discussion https://gitlab.com/gitlab-org/gitlab/merge_requests/1629#note_31285823
this
.
roles
=
this
.
accessLevelsData
.
map
((
role
)
=>
({
...
role
,
type
:
LEVEL_TYPES
.
ROLE
}));
if
(
this
.
hasLicense
)
{
this
.
groups
=
groupsResponse
.
map
((
group
)
=>
({
...
group
,
type
:
LEVEL_TYPES
.
GROUP
}));
this
.
users
=
usersResponse
.
map
((
user
)
=>
({
...
user
,
type
:
LEVEL_TYPES
.
USER
}));
}
this
.
deployKeys
=
deployKeysResponse
.
map
((
response
)
=>
{
const
{
id
,
fingerprint
,
title
,
owner
:
{
avatar_url
,
name
,
username
},
}
=
response
;
const
shortFingerprint
=
`(
${
fingerprint
.
substring
(
0
,
14
)}
...)`
;
return
{
id
,
title
:
title
.
concat
(
'
'
,
shortFingerprint
),
avatar_url
,
fullname
:
name
,
username
,
type
:
LEVEL_TYPES
.
DEPLOY_KEY
,
};
});
},
onItemClick
(
item
)
{
this
.
toggleSelection
(
this
.
selected
[
item
.
type
],
item
);
this
.
emitUpdate
();
},
toggleSelection
(
arr
,
item
)
{
const
itemIndex
=
arr
.
indexOf
(
item
);
if
(
itemIndex
>
-
1
)
{
arr
.
splice
(
itemIndex
,
1
);
}
else
arr
.
push
(
item
);
},
isSelected
(
item
)
{
return
this
.
selected
[
item
.
type
].
some
((
selected
)
=>
selected
.
id
===
item
.
id
);
},
emitUpdate
()
{
const
selected
=
Object
.
values
(
this
.
selected
).
flat
();
this
.
$emit
(
'
select
'
,
selected
);
},
},
};
</
script
>
<
template
>
<gl-dropdown
:text=
"toggleLabel"
class=
"gl-display-block"
:toggle-class=
"toggleClass"
aria-labelledby=
"allowed-users-label"
@
shown=
"focusInput"
>
<template
#header
>
<gl-search-box-by-type
ref=
"search"
v-model.trim=
"query"
/>
<gl-loading-icon
v-if=
"loading"
size=
"sm"
/>
</
template
>
<
template
v-if=
"roles.length"
>
<gl-dropdown-section-header>
{{
$options
.
i18n
.
rolesSectionHeader
}}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for=
"role in roles"
:key=
"role.id"
is-check-item
:is-checked=
"isSelected(role)"
@
click.native.capture.stop=
"onItemClick(role)"
>
{{
role
.
text
}}
</gl-dropdown-item>
<gl-dropdown-divider
v-if=
"groups.length || users.length || showDeployKeys"
/>
</
template
>
<
template
v-if=
"groups.length"
>
<gl-dropdown-section-header>
{{
$options
.
i18n
.
groupsSectionHeader
}}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for=
"group in groups"
:key=
"group.id"
:avatar-url=
"group.avatar_url"
is-check-item
:is-checked=
"isSelected(group)"
@
click.native.capture.stop=
"onItemClick(group)"
>
{{
group
.
name
}}
</gl-dropdown-item>
<gl-dropdown-divider
v-if=
"users.length || showDeployKeys"
/>
</
template
>
<
template
v-if=
"users.length"
>
<gl-dropdown-section-header>
{{
$options
.
i18n
.
usersSectionHeader
}}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for=
"user in users"
:key=
"user.id"
:avatar-url=
"user.avatar_url"
:secondary-text=
"user.username"
is-check-item
:is-checked=
"isSelected(user)"
@
click.native.capture.stop=
"onItemClick(user)"
>
{{
user
.
name
}}
</gl-dropdown-item>
<gl-dropdown-divider
v-if=
"showDeployKeys"
/>
</
template
>
<
template
v-if=
"showDeployKeys"
>
<gl-dropdown-section-header>
{{
$options
.
i18n
.
deployKeysSectionHeader
}}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for=
"key in deployKeys"
:key=
"key.id"
is-check-item
:is-checked=
"isSelected(key)"
class=
"gl-text-truncate"
@
click.native.capture.stop=
"onItemClick(key)"
>
<div
class=
"gl-text-truncate gl-font-weight-bold"
>
{{
key
.
title
}}
</div>
<div
class=
"gl-text-gray-700 gl-text-truncate"
>
<gl-sprintf
:message=
"$options.i18n.ownedBy"
>
<template
#image_tag
>
<gl-avatar
:src=
"key.avatar_url"
:size=
"24"
/>
</
template
>
</gl-sprintf
>
{{ key.fullname }} ({{ key.username }})
</div>
</gl-dropdown-item>
</template>
</gl-dropdown>
</template>
app/assets/javascripts/projects/settings/init_access_dropdown.js
0 → 100644
View file @
1ca830d3
import
Vue
from
'
vue
'
;
import
AccessDropdown
from
'
./components/access_dropdown.vue
'
;
export
const
initAccessDropdown
=
(
el
,
options
)
=>
{
if
(
!
el
)
{
return
false
;
}
const
{
accessLevelsData
,
accessLevel
}
=
options
;
return
new
Vue
({
el
,
render
(
createElement
)
{
const
vm
=
this
;
return
createElement
(
AccessDropdown
,
{
props
:
{
accessLevel
,
accessLevelsData
:
accessLevelsData
.
roles
,
},
on
:
{
select
(
selected
)
{
vm
.
$emit
(
'
select
'
,
selected
);
},
},
});
},
});
};
ee/app/assets/javascripts/protected_environments/protected_environment_create.js
View file @
1ca830d3
...
...
@@ -4,7 +4,7 @@ import createFlash from '~/flash';
import
AccessorUtilities
from
'
~/lib/utils/accessor
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
{
__
}
from
'
~/locale
'
;
import
AccessDropdown
from
'
~/projects/settings/
access_dropdown
'
;
import
{
initAccessDropdown
}
from
'
~/projects/settings/init_
access_dropdown
'
;
import
{
ACCESS_LEVELS
,
LEVEL_TYPES
}
from
'
./constants
'
;
const
PROTECTED_ENVIRONMENT_INPUT
=
'
input[name="protected_environment[name]"]
'
;
...
...
@@ -13,9 +13,9 @@ export default class ProtectedEnvironmentCreate {
constructor
()
{
this
.
$form
=
$
(
'
.js-new-protected-environment
'
);
this
.
isLocalStorageAvailable
=
AccessorUtilities
.
canUseLocalStorage
();
this
.
currentProjectUserDefaults
=
{};
this
.
buildDropdowns
();
this
.
bindEvents
();
this
.
selected
=
[];
}
bindEvents
()
{
...
...
@@ -23,17 +23,21 @@ export default class ProtectedEnvironmentCreate {
}
buildDropdowns
()
{
const
$allowedToDeployDropdown
=
this
.
$form
.
find
(
'
.js-allowed-to-deploy
'
);
// Cache callback
this
.
onSelectCallback
=
this
.
onSelect
.
bind
(
this
);
// Allowed to Deploy dropdown
this
[
`
${
ACCESS_LEVELS
.
DEPLOY
}
_dropdown`
]
=
new
AccessDropdown
({
$dropdown
:
$allowedToDeployDropdown
,
accessLevelsData
:
gon
.
deploy_access_levels
,
onSelect
:
this
.
onSelectCallback
,
accessLevel
:
ACCESS_LEVELS
.
DEPLOY
,
const
accessDropdown
=
initAccessDropdown
(
document
.
querySelector
(
'
.js-allowed-to-deploy-dropdown
'
),
{
accessLevelsData
:
gon
.
deploy_access_levels
,
accessLevel
:
ACCESS_LEVELS
.
DEPLOY
,
},
);
accessDropdown
.
$on
(
'
select
'
,
(
selected
)
=>
{
this
.
selected
=
selected
;
this
.
onSelect
();
});
this
.
createItemDropdown
=
new
CreateItemDropdown
({
...
...
@@ -46,10 +50,9 @@ export default class ProtectedEnvironmentCreate {
});
}
// Enable submit button after selecting an option
// Enable submit button after selecting an option
on select
onSelect
()
{
const
$allowedToDeploy
=
this
[
`
${
ACCESS_LEVELS
.
DEPLOY
}
_dropdown`
].
getSelectedItems
();
const
toggle
=
!
(
this
.
$form
.
find
(
PROTECTED_ENVIRONMENT_INPUT
).
val
()
&&
$allowedToDeploy
.
length
);
const
toggle
=
!
(
this
.
$form
.
find
(
PROTECTED_ENVIRONMENT_INPUT
).
val
()
&&
this
.
selected
.
length
);
this
.
$form
.
find
(
'
input[type="submit"]
'
).
attr
(
'
disabled
'
,
toggle
);
}
...
...
@@ -84,21 +87,20 @@ export default class ProtectedEnvironmentCreate {
Object
.
keys
(
ACCESS_LEVELS
).
forEach
((
level
)
=>
{
const
accessLevel
=
ACCESS_LEVELS
[
level
];
const
selectedItems
=
this
[
`
${
accessLevel
}
_dropdown`
].
getSelectedItems
();
const
levelAttributes
=
[];
selectedItems
.
forEach
((
item
)
=>
{
this
.
selected
.
forEach
((
item
)
=>
{
if
(
item
.
type
===
LEVEL_TYPES
.
USER
)
{
levelAttributes
.
push
({
user_id
:
item
.
user_
id
,
user_id
:
item
.
id
,
});
}
else
if
(
item
.
type
===
LEVEL_TYPES
.
ROLE
)
{
levelAttributes
.
push
({
access_level
:
item
.
access_level
,
access_level
:
item
.
id
,
});
}
else
if
(
item
.
type
===
LEVEL_TYPES
.
GROUP
)
{
levelAttributes
.
push
({
group_id
:
item
.
group_
id
,
group_id
:
item
.
id
,
});
}
});
...
...
ee/app/views/projects/protected_environments/_deploy_access_levels_dropdown.html.haml
deleted
100644 → 0
View file @
d2167efa
.deploy_access_levels-container
=
dropdown_tag
(
s_
(
'ProtectedEnvironment|Select users'
),
options:
{
toggle_class:
'js-allowed-to-deploy wide js-multiselect'
,
dropdown_class:
'dropdown-menu-user dropdown-menu-selectable'
,
filter:
true
,
data:
{
field_name:
'protected_environments[deploy_access_levels_attributes][0][access_level]'
,
input_id:
'deploy_access_levels_attributes'
}})
ee/app/views/projects/protected_environments/_form.html.haml
View file @
1ca830d3
...
...
@@ -11,9 +11,9 @@
=
render
partial:
'projects/protected_environments/environments_dropdown'
,
locals:
{
f:
f
,
project:
@project
}
.form-group
=
f
.
label
:deploy_access_levels_attributes
,
class:
'label-bold'
do
%label
#allowed-users-label
.label-bold
=
s_
(
'ProtectedEnvironment|Allowed to deploy'
)
=
render
partial:
'projects/protected_environments/deploy_access_levels_dropdown'
,
locals:
{
f:
f
}
.js-allowed-to-deploy-dropdown
.card-footer
=
f
.
submit
s_
(
'ProtectedEnvironment|Protect'
),
class:
'gl-button btn btn-confirm'
,
disabled:
true
ee/spec/features/projects/settings/protected_environments_spec.rb
View file @
1ca830d3
...
...
@@ -118,12 +118,10 @@ RSpec.describe 'Protected Environments' do
end
def
set_allowed_to_deploy
(
option
)
find
(
'.js-allowed-to-deploy'
).
click
click_button
(
'Select users'
)
within
(
'.dropdown-content'
)
do
Array
(
option
).
each
{
|
opt
|
click_on
(
opt
)
}
within
'.gl-new-dropdown-contents'
do
Array
(
option
).
each
{
|
opt
|
find
(
'.gl-new-dropdown-item'
,
text:
opt
).
click
}
end
find
(
'.js-allowed-to-deploy'
).
click
end
end
spec/frontend/projects/settings/components/new_access_dropdown_spec.js
0 → 100644
View file @
1ca830d3
import
{
GlSprintf
,
GlDropdown
,
GlDropdownItem
,
GlDropdownSectionHeader
,
GlSearchBoxByType
,
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
nextTick
}
from
'
vue
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
getUsers
,
getGroups
,
getDeployKeys
}
from
'
~/projects/settings/api/access_dropdown_api
'
;
import
AccessDropdown
,
{
i18n
}
from
'
~/projects/settings/components/access_dropdown.vue
'
;
import
{
ACCESS_LEVELS
}
from
'
~/projects/settings/constants
'
;
jest
.
mock
(
'
~/projects/settings/api/access_dropdown_api
'
,
()
=>
({
getUsers
:
jest
.
fn
().
mockResolvedValue
({
data
:
[{
id
:
1
},
{
id
:
2
}]
}),
getGroups
:
jest
.
fn
().
mockResolvedValue
({
data
:
[{
id
:
3
},
{
id
:
4
},
{
id
:
5
}]
}),
getDeployKeys
:
jest
.
fn
().
mockResolvedValue
({
data
:
[
{
id
:
6
,
title
:
'
key1
'
,
fingerprint
:
'
abcdefghijklmnop
'
,
owner
:
{
name
:
'
user1
'
}
},
{
id
:
7
,
title
:
'
key1
'
,
fingerprint
:
'
abcdefghijklmnop
'
,
owner
:
{
name
:
'
user2
'
}
},
{
id
:
8
,
title
:
'
key1
'
,
fingerprint
:
'
abcdefghijklmnop
'
,
owner
:
{
name
:
'
user3
'
}
},
{
id
:
9
,
title
:
'
key1
'
,
fingerprint
:
'
abcdefghijklmnop
'
,
owner
:
{
name
:
'
user4
'
}
},
],
}),
}));
describe
(
'
Access Level Dropdown
'
,
()
=>
{
let
wrapper
;
const
mockAccessLevelsData
=
[
{
id
:
42
,
text
:
'
Dummy Role
'
,
},
];
const
createComponent
=
({
accessLevelsData
=
mockAccessLevelsData
,
accessLevel
=
ACCESS_LEVELS
.
PUSH
,
hasLicense
=
true
,
}
=
{})
=>
{
wrapper
=
shallowMount
(
AccessDropdown
,
{
propsData
:
{
accessLevelsData
,
accessLevel
,
hasLicense
,
},
stubs
:
{
GlSprintf
,
GlDropdown
,
},
});
};
const
findDropdown
=
()
=>
wrapper
.
findComponent
(
GlDropdown
);
const
findDropdownToggleLabel
=
()
=>
findDropdown
().
props
(
'
text
'
);
const
findAllDropdownItems
=
()
=>
findDropdown
().
findAllComponents
(
GlDropdownItem
);
const
findAllDropdownHeaders
=
()
=>
findDropdown
().
findAllComponents
(
GlDropdownSectionHeader
);
const
findSearchBox
=
()
=>
wrapper
.
findComponent
(
GlSearchBoxByType
);
describe
(
'
data request
'
,
()
=>
{
it
(
'
should make an api call for users, groups && deployKeys when user has a license
'
,
()
=>
{
createComponent
();
expect
(
getUsers
).
toHaveBeenCalled
();
expect
(
getGroups
).
toHaveBeenCalled
();
expect
(
getDeployKeys
).
toHaveBeenCalled
();
});
it
(
'
should make an api call for deployKeys but not for users or groups when user does not have a license
'
,
()
=>
{
createComponent
({
hasLicense
:
false
});
expect
(
getUsers
).
not
.
toHaveBeenCalled
();
expect
(
getGroups
).
not
.
toHaveBeenCalled
();
expect
(
getDeployKeys
).
toHaveBeenCalled
();
});
it
(
'
should make api calls when search query is updated
'
,
async
()
=>
{
createComponent
();
const
query
=
'
root
'
;
findSearchBox
().
vm
.
$emit
(
'
input
'
,
query
);
await
nextTick
();
expect
(
getUsers
).
toHaveBeenCalledWith
(
query
);
expect
(
getGroups
).
toHaveBeenCalled
();
expect
(
getDeployKeys
).
toHaveBeenCalledWith
(
query
);
});
});
describe
(
'
layout
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createComponent
();
await
waitForPromises
();
});
it
(
'
renders headers for each section
'
,
()
=>
{
expect
(
findAllDropdownHeaders
()).
toHaveLength
(
4
);
});
it
(
'
renders dropdown item for each access level type
'
,
()
=>
{
expect
(
findAllDropdownItems
()).
toHaveLength
(
10
);
});
});
describe
(
'
toggleLabel
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createComponent
();
await
waitForPromises
();
});
const
triggerNthItemClick
=
async
(
n
)
=>
{
findAllDropdownItems
().
at
(
n
).
trigger
(
'
click
'
);
await
nextTick
();
};
it
(
'
when no items selected displays a default label and has default CSS class
'
,
()
=>
{
expect
(
findDropdownToggleLabel
()).
toBe
(
i18n
.
selectUsers
);
expect
(
findDropdown
().
props
(
'
toggleClass
'
)).
toBe
(
'
gl-text-gray-500!
'
);
});
it
(
'
displays a number of selected items for each group level
'
,
async
()
=>
{
findAllDropdownItems
().
wrappers
.
forEach
((
item
)
=>
{
item
.
trigger
(
'
click
'
);
});
await
nextTick
();
expect
(
findDropdownToggleLabel
()).
toBe
(
'
1 role, 2 users, 4 deploy keys, 3 groups
'
);
});
it
(
'
with only role selected displays the role name and has no class applied
'
,
async
()
=>
{
await
triggerNthItemClick
(
0
);
expect
(
findDropdownToggleLabel
()).
toBe
(
'
Dummy Role
'
);
expect
(
findDropdown
().
props
(
'
toggleClass
'
)).
toBe
(
''
);
});
it
(
'
with only groups selected displays the number of selected groups
'
,
async
()
=>
{
await
triggerNthItemClick
(
1
);
await
triggerNthItemClick
(
2
);
await
triggerNthItemClick
(
3
);
expect
(
findDropdownToggleLabel
()).
toBe
(
'
3 groups
'
);
expect
(
findDropdown
().
props
(
'
toggleClass
'
)).
toBe
(
''
);
});
it
(
'
with only users selected displays the number of selected users
'
,
async
()
=>
{
await
triggerNthItemClick
(
4
);
await
triggerNthItemClick
(
5
);
expect
(
findDropdownToggleLabel
()).
toBe
(
'
2 users
'
);
expect
(
findDropdown
().
props
(
'
toggleClass
'
)).
toBe
(
''
);
});
it
(
'
with users and groups selected displays the number of selected users & groups
'
,
async
()
=>
{
await
triggerNthItemClick
(
1
);
await
triggerNthItemClick
(
2
);
await
triggerNthItemClick
(
4
);
await
triggerNthItemClick
(
5
);
expect
(
findDropdownToggleLabel
()).
toBe
(
'
2 users, 2 groups
'
);
expect
(
findDropdown
().
props
(
'
toggleClass
'
)).
toBe
(
''
);
});
it
(
'
with users and deploy keys selected displays the number of selected users & keys
'
,
async
()
=>
{
await
triggerNthItemClick
(
1
);
await
triggerNthItemClick
(
2
);
await
triggerNthItemClick
(
6
);
expect
(
findDropdownToggleLabel
()).
toBe
(
'
1 deploy key, 2 groups
'
);
expect
(
findDropdown
().
props
(
'
toggleClass
'
)).
toBe
(
''
);
});
});
describe
(
'
selecting an item
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createComponent
();
await
waitForPromises
();
});
it
(
'
selects the item on click and deselects on the next click
'
,
async
()
=>
{
const
item
=
findAllDropdownItems
().
at
(
1
);
item
.
trigger
(
'
click
'
);
await
nextTick
();
expect
(
item
.
props
(
'
isChecked
'
)).
toBe
(
true
);
item
.
trigger
(
'
click
'
);
await
nextTick
();
expect
(
item
.
props
(
'
isChecked
'
)).
toBe
(
false
);
});
it
(
'
emits an update on selection
'
,
async
()
=>
{
const
spy
=
jest
.
spyOn
(
wrapper
.
vm
,
'
$emit
'
);
findAllDropdownItems
().
at
(
4
).
trigger
(
'
click
'
);
findAllDropdownItems
().
at
(
3
).
trigger
(
'
click
'
);
await
nextTick
();
expect
(
spy
).
toHaveBeenLastCalledWith
(
'
select
'
,
[
{
id
:
5
,
type
:
'
group
'
},
{
id
:
1
,
type
:
'
user
'
},
]);
});
});
describe
(
'
on dropdown open
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
();
});
it
(
'
should set the search input focus
'
,
()
=>
{
wrapper
.
vm
.
$refs
.
search
.
focusInput
=
jest
.
fn
();
findDropdown
().
vm
.
$emit
(
'
shown
'
);
expect
(
wrapper
.
vm
.
$refs
.
search
.
focusInput
).
toHaveBeenCalled
();
});
});
});
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