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
c8a07385
Commit
c8a07385
authored
Jun 10, 2020
by
Nicolò Maria Mezzopera
Committed by
Mark Florian
Jun 10, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add tags loader component
- new component - unit tests - snapshots
parent
e28e7298
Changes
19
Show whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
843 additions
and
477 deletions
+843
-477
app/assets/javascripts/registry/explorer/components/details_page/empty_tags_state.vue
...try/explorer/components/details_page/empty_tags_state.vue
+33
-0
app/assets/javascripts/registry/explorer/components/details_page/tags_loader.vue
...registry/explorer/components/details_page/tags_loader.vue
+34
-0
app/assets/javascripts/registry/explorer/components/details_page/tags_table.vue
.../registry/explorer/components/details_page/tags_table.vue
+210
-0
app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
...registry/explorer/components/list_page/image_list_row.vue
+1
-1
app/assets/javascripts/registry/explorer/pages/details.vue
app/assets/javascripts/registry/explorer/pages/details.vue
+19
-224
app/assets/javascripts/registry/explorer/stores/getters.js
app/assets/javascripts/registry/explorer/stores/getters.js
+0
-6
spec/features/groups/container_registry_spec.rb
spec/features/groups/container_registry_spec.rb
+1
-1
spec/features/projects/container_registry_spec.rb
spec/features/projects/container_registry_spec.rb
+1
-1
spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap
...nents/details_page/__snapshots__/tags_loader_spec.js.snap
+63
-0
spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js
...try/explorer/components/details_page/delete_alert_spec.js
+5
-0
spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js
...try/explorer/components/details_page/delete_modal_spec.js
+5
-0
spec/frontend/registry/explorer/components/details_page/details_header_spec.js
...y/explorer/components/details_page/details_header_spec.js
+5
-0
spec/frontend/registry/explorer/components/details_page/empty_tags_state.js
...stry/explorer/components/details_page/empty_tags_state.js
+43
-0
spec/frontend/registry/explorer/components/details_page/tags_loader_spec.js
...stry/explorer/components/details_page/tags_loader_spec.js
+49
-0
spec/frontend/registry/explorer/components/details_page/tags_table_spec.js
...istry/explorer/components/details_page/tags_table_spec.js
+287
-0
spec/frontend/registry/explorer/components/list_page/image_list_spec.js
...registry/explorer/components/list_page/image_list_spec.js
+5
-0
spec/frontend/registry/explorer/pages/details_spec.js
spec/frontend/registry/explorer/pages/details_spec.js
+61
-215
spec/frontend/registry/explorer/stores/getters_spec.js
spec/frontend/registry/explorer/stores/getters_spec.js
+0
-29
spec/frontend/registry/explorer/stubs.js
spec/frontend/registry/explorer/stubs.js
+21
-0
No files found.
app/assets/javascripts/registry/explorer/components/details_page/empty_tags_state.vue
0 → 100644
View file @
c8a07385
<
script
>
import
{
GlEmptyState
}
from
'
@gitlab/ui
'
;
import
{
EMPTY_IMAGE_REPOSITORY_TITLE
,
EMPTY_IMAGE_REPOSITORY_MESSAGE
,
}
from
'
../../constants/index
'
;
export
default
{
components
:
{
GlEmptyState
,
},
props
:
{
noContainersImage
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
i18n
:
{
EMPTY_IMAGE_REPOSITORY_TITLE
,
EMPTY_IMAGE_REPOSITORY_MESSAGE
,
},
};
</
script
>
<
template
>
<gl-empty-state
:title=
"$options.i18n.EMPTY_IMAGE_REPOSITORY_TITLE"
:svg-path=
"noContainersImage"
:description=
"$options.i18n.EMPTY_IMAGE_REPOSITORY_MESSAGE"
class=
"gl-mx-auto gl-my-0"
/>
</
template
>
app/assets/javascripts/registry/explorer/components/details_page/tags_loader.vue
0 → 100644
View file @
c8a07385
<
script
>
import
{
GlSkeletonLoader
}
from
'
@gitlab/ui
'
;
export
default
{
components
:
{
GlSkeletonLoader
,
},
loader
:
{
repeat
:
10
,
width
:
1000
,
height
:
40
,
},
};
</
script
>
<
template
>
<div>
<gl-skeleton-loader
v-for=
"index in $options.loader.repeat"
:key=
"index"
:width=
"$options.loader.width"
:height=
"$options.loader.height"
preserve-aspect-ratio=
"xMinYMax meet"
>
<rect
width=
"15"
x=
"0"
y=
"12.5"
height=
"15"
rx=
"4"
/>
<rect
width=
"250"
x=
"25"
y=
"10"
height=
"20"
rx=
"4"
/>
<circle
cx=
"290"
cy=
"20"
r=
"10"
/>
<rect
width=
"100"
x=
"315"
y=
"10"
height=
"20"
rx=
"4"
/>
<rect
width=
"100"
x=
"500"
y=
"10"
height=
"20"
rx=
"4"
/>
<rect
width=
"100"
x=
"630"
y=
"10"
height=
"20"
rx=
"4"
/>
<rect
x=
"960"
y=
"0"
width=
"40"
height=
"40"
rx=
"4"
/>
</gl-skeleton-loader>
</div>
</
template
>
app/assets/javascripts/registry/explorer/components/details_page/tags_table.vue
0 → 100644
View file @
c8a07385
<
script
>
import
{
GlTable
,
GlFormCheckbox
,
GlButton
,
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
{
n__
}
from
'
~/locale
'
;
import
ClipboardButton
from
'
~/vue_shared/components/clipboard_button.vue
'
;
import
{
numberToHumanSize
}
from
'
~/lib/utils/number_utils
'
;
import
timeagoMixin
from
'
~/vue_shared/mixins/timeago
'
;
import
{
LIST_KEY_TAG
,
LIST_KEY_IMAGE_ID
,
LIST_KEY_SIZE
,
LIST_KEY_LAST_UPDATED
,
LIST_KEY_ACTIONS
,
LIST_KEY_CHECKBOX
,
LIST_LABEL_TAG
,
LIST_LABEL_IMAGE_ID
,
LIST_LABEL_SIZE
,
LIST_LABEL_LAST_UPDATED
,
REMOVE_TAGS_BUTTON_TITLE
,
REMOVE_TAG_BUTTON_TITLE
,
}
from
'
../../constants/index
'
;
export
default
{
components
:
{
GlTable
,
GlFormCheckbox
,
GlButton
,
ClipboardButton
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
mixins
:
[
timeagoMixin
],
props
:
{
tags
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
isLoading
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
isDesktop
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
i18n
:
{
REMOVE_TAGS_BUTTON_TITLE
,
REMOVE_TAG_BUTTON_TITLE
,
},
data
()
{
return
{
selectedItems
:
[],
};
},
computed
:
{
fields
()
{
const
tagClass
=
this
.
isDesktop
?
'
w-25
'
:
''
;
const
tagInnerClass
=
this
.
isDesktop
?
'
mw-m
'
:
'
gl-justify-content-end
'
;
return
[
{
key
:
LIST_KEY_CHECKBOX
,
label
:
''
,
class
:
'
gl-w-16
'
},
{
key
:
LIST_KEY_TAG
,
label
:
LIST_LABEL_TAG
,
class
:
`
${
tagClass
}
js-tag-column`
,
innerClass
:
tagInnerClass
,
},
{
key
:
LIST_KEY_IMAGE_ID
,
label
:
LIST_LABEL_IMAGE_ID
},
{
key
:
LIST_KEY_SIZE
,
label
:
LIST_LABEL_SIZE
},
{
key
:
LIST_KEY_LAST_UPDATED
,
label
:
LIST_LABEL_LAST_UPDATED
},
{
key
:
LIST_KEY_ACTIONS
,
label
:
''
},
].
filter
(
f
=>
f
.
key
!==
LIST_KEY_CHECKBOX
||
this
.
isDesktop
);
},
tagsNames
()
{
return
this
.
tags
.
map
(
t
=>
t
.
name
);
},
selectAllChecked
()
{
return
this
.
selectedItems
.
length
===
this
.
tags
.
length
&&
this
.
tags
.
length
>
0
;
},
},
watch
:
{
tagsNames
:
{
immediate
:
false
,
handler
(
tagsNames
)
{
this
.
selectedItems
=
this
.
selectedItems
.
filter
(
t
=>
tagsNames
.
includes
(
t
));
},
},
},
methods
:
{
formatSize
(
size
)
{
return
numberToHumanSize
(
size
);
},
layers
(
layers
)
{
return
layers
?
n__
(
'
%d layer
'
,
'
%d layers
'
,
layers
)
:
''
;
},
onSelectAllChange
()
{
if
(
this
.
selectAllChecked
)
{
this
.
selectedItems
=
[];
}
else
{
this
.
selectedItems
=
this
.
tags
.
map
(
x
=>
x
.
name
);
}
},
updateSelectedItems
(
name
)
{
const
delIndex
=
this
.
selectedItems
.
findIndex
(
x
=>
x
===
name
);
if
(
delIndex
>
-
1
)
{
this
.
selectedItems
.
splice
(
delIndex
,
1
);
}
else
{
this
.
selectedItems
.
push
(
name
);
}
},
},
};
</
script
>
<
template
>
<gl-table
:items=
"tags"
:fields=
"fields"
:stacked=
"!isDesktop"
show-empty
:busy=
"isLoading"
>
<template
v-if=
"isDesktop"
#head
(
checkbox
)
>
<gl-form-checkbox
data-testid=
"mainCheckbox"
:checked=
"selectAllChecked"
@
change=
"onSelectAllChange"
/>
</
template
>
<
template
#head
(
actions
)
>
<span
class=
"gl-display-flex gl-justify-content-end"
>
<gl-button
v-gl-tooltip
data-testid=
"bulkDeleteButton"
:disabled=
"!selectedItems || selectedItems.length === 0"
icon=
"remove"
variant=
"danger"
:title=
"$options.i18n.REMOVE_TAGS_BUTTON_TITLE"
:aria-label=
"$options.i18n.REMOVE_TAGS_BUTTON_TITLE"
@
click=
"$emit('delete', selectedItems)"
/>
</span>
</
template
>
<
template
#cell(checkbox)=
"{item}"
>
<gl-form-checkbox
data-testid=
"rowCheckbox"
:checked=
"selectedItems.includes(item.name)"
@
change=
"updateSelectedItems(item.name)"
/>
</
template
>
<
template
#cell(name)=
"{item, field}"
>
<div
data-testid=
"rowName"
:class=
"[field.innerClass, 'gl-display-flex']"
>
<span
v-gl-tooltip
data-testid=
"rowNameText"
:title=
"item.name"
class=
"gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap"
>
{{
item
.
name
}}
</span>
<clipboard-button
v-if=
"item.location"
data-testid=
"rowClipboardButton"
:title=
"item.location"
:text=
"item.location"
css-class=
"btn-default btn-transparent btn-clipboard"
/>
</div>
</
template
>
<
template
#cell(short_revision)=
"{value}"
>
<span
data-testid=
"rowShortRevision"
>
{{
value
}}
</span>
</
template
>
<
template
#cell(total_size)=
"{item}"
>
<span
data-testid=
"rowSize"
>
{{
formatSize
(
item
.
total_size
)
}}
<template
v-if=
"item.total_size && item.layers"
>
·
</
template
>
{{ layers(item.layers) }}
</span>
</template>
<
template
#cell(created_at)=
"{value}"
>
<span
v-gl-tooltip
data-testid=
"rowTime"
:title=
"tooltipTitle(value)"
>
{{
timeFormatted
(
value
)
}}
</span>
</
template
>
<
template
#cell(actions)=
"{item}"
>
<span
class=
"gl-display-flex gl-justify-content-end"
>
<gl-button
data-testid=
"singleDeleteButton"
:title=
"$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:aria-label=
"$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:disabled=
"!item.destroy_path"
variant=
"danger"
icon=
"remove"
category=
"secondary"
@
click=
"$emit('delete', [item.name])"
/>
</span>
</
template
>
<
template
#empty
>
<slot
name=
"empty"
></slot>
</
template
>
<
template
#table-busy
>
<slot
name=
"loader"
></slot>
</
template
>
</gl-table>
</template>
app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
View file @
c8a07385
...
...
@@ -125,7 +125,7 @@ export default {
:disabled=
"disabledDelete"
:title=
"$options.i18n.REMOVE_REPOSITORY_LABEL"
:aria-label=
"$options.i18n.REMOVE_REPOSITORY_LABEL"
c
lass=
"btn-inverted
"
c
ategory=
"secondary
"
variant=
"danger"
icon=
"remove"
@
click=
"$emit('delete', item)"
...
...
app/assets/javascripts/registry/explorer/pages/details.vue
View file @
c8a07385
<
script
>
import
{
mapState
,
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
{
GlTable
,
GlFormCheckbox
,
GlDeprecatedButton
,
GlIcon
,
GlTooltipDirective
,
GlPagination
,
GlEmptyState
,
GlResizeObserverDirective
,
GlSkeletonLoader
,
}
from
'
@gitlab/ui
'
;
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
{
GlPagination
,
GlResizeObserverDirective
}
from
'
@gitlab/ui
'
;
import
{
GlBreakpointInstance
}
from
'
@gitlab/ui/dist/utils
'
;
import
{
n__
}
from
'
~/locale
'
;
import
ClipboardButton
from
'
~/vue_shared/components/clipboard_button.vue
'
;
import
{
numberToHumanSize
}
from
'
~/lib/utils/number_utils
'
;
import
timeagoMixin
from
'
~/vue_shared/mixins/timeago
'
;
import
Tracking
from
'
~/tracking
'
;
import
DeleteAlert
from
'
../components/details_page/delete_alert.vue
'
;
import
DeleteModal
from
'
../components/details_page/delete_modal.vue
'
;
import
DetailsHeader
from
'
../components/details_page/details_header.vue
'
;
import
TagsTable
from
'
../components/details_page/tags_table.vue
'
;
import
TagsLoader
from
'
../components/details_page/tags_loader.vue
'
;
import
EmptyTagsState
from
'
../components/details_page/empty_tags_state.vue
'
;
import
{
decodeAndParse
}
from
'
../utils
'
;
import
{
LIST_KEY_TAG
,
LIST_KEY_IMAGE_ID
,
LIST_KEY_SIZE
,
LIST_KEY_LAST_UPDATED
,
LIST_KEY_ACTIONS
,
LIST_KEY_CHECKBOX
,
LIST_LABEL_TAG
,
LIST_LABEL_IMAGE_ID
,
LIST_LABEL_SIZE
,
LIST_LABEL_LAST_UPDATED
,
REMOVE_TAGS_BUTTON_TITLE
,
REMOVE_TAG_BUTTON_TITLE
,
EMPTY_IMAGE_REPOSITORY_TITLE
,
EMPTY_IMAGE_REPOSITORY_MESSAGE
,
ALERT_SUCCESS_TAG
,
ALERT_DANGER_TAG
,
ALERT_SUCCESS_TAGS
,
...
...
@@ -46,66 +22,29 @@ export default {
components
:
{
DeleteAlert
,
DetailsHeader
,
GlTable
,
GlFormCheckbox
,
GlDeprecatedButton
,
GlIcon
,
ClipboardButton
,
GlPagination
,
DeleteModal
,
GlSkeletonLoader
,
GlEmptyState
,
TagsTable
,
TagsLoader
,
EmptyTagsState
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
GlResizeObserver
:
GlResizeObserverDirective
,
},
mixins
:
[
timeagoMixin
,
Tracking
.
mixin
()],
loader
:
{
repeat
:
10
,
width
:
1000
,
height
:
40
,
},
i18n
:
{
REMOVE_TAGS_BUTTON_TITLE
,
REMOVE_TAG_BUTTON_TITLE
,
EMPTY_IMAGE_REPOSITORY_TITLE
,
EMPTY_IMAGE_REPOSITORY_MESSAGE
,
},
mixins
:
[
Tracking
.
mixin
()],
data
()
{
return
{
selectedItems
:
[],
itemsToBeDeleted
:
[],
selectAllChecked
:
false
,
modalDescription
:
null
,
isDesktop
:
true
,
deleteAlertType
:
null
,
};
},
computed
:
{
...
mapGetters
([
'
tags
'
]),
...
mapState
([
'
tagsPagination
'
,
'
isLoading
'
,
'
config
'
]),
...
mapState
([
'
tagsPagination
'
,
'
isLoading
'
,
'
config
'
,
'
tags
'
]),
imageName
()
{
const
{
name
}
=
decodeAndParse
(
this
.
$route
.
params
.
id
);
return
name
;
},
fields
()
{
const
tagClass
=
this
.
isDesktop
?
'
w-25
'
:
''
;
const
tagInnerClass
=
this
.
isDesktop
?
'
mw-m
'
:
'
gl-justify-content-end
'
;
return
[
{
key
:
LIST_KEY_CHECKBOX
,
label
:
''
,
class
:
'
gl-w-16
'
},
{
key
:
LIST_KEY_TAG
,
label
:
LIST_LABEL_TAG
,
class
:
`
${
tagClass
}
js-tag-column`
,
innerClass
:
tagInnerClass
,
},
{
key
:
LIST_KEY_IMAGE_ID
,
label
:
LIST_LABEL_IMAGE_ID
},
{
key
:
LIST_KEY_SIZE
,
label
:
LIST_LABEL_SIZE
},
{
key
:
LIST_KEY_LAST_UPDATED
,
label
:
LIST_LABEL_LAST_UPDATED
},
{
key
:
LIST_KEY_ACTIONS
,
label
:
''
},
].
filter
(
f
=>
f
.
key
!==
LIST_KEY_CHECKBOX
||
this
.
isDesktop
);
},
tracking
()
{
return
{
label
:
...
...
@@ -126,48 +65,8 @@ export default {
},
methods
:
{
...
mapActions
([
'
requestTagsList
'
,
'
requestDeleteTag
'
,
'
requestDeleteTags
'
]),
formatSize
(
size
)
{
return
numberToHumanSize
(
size
);
},
layers
(
layers
)
{
return
layers
?
n__
(
'
%d layer
'
,
'
%d layers
'
,
layers
)
:
''
;
},
onSelectAllChange
()
{
if
(
this
.
selectAllChecked
)
{
this
.
deselectAll
();
}
else
{
this
.
selectAll
();
}
},
selectAll
()
{
this
.
selectedItems
=
this
.
tags
.
map
(
x
=>
x
.
name
);
this
.
selectAllChecked
=
true
;
},
deselectAll
()
{
this
.
selectedItems
=
[];
this
.
selectAllChecked
=
false
;
},
updateSelectedItems
(
name
)
{
const
delIndex
=
this
.
selectedItems
.
findIndex
(
x
=>
x
===
name
);
if
(
delIndex
>
-
1
)
{
this
.
selectedItems
.
splice
(
delIndex
,
1
);
this
.
selectAllChecked
=
false
;
}
else
{
this
.
selectedItems
.
push
(
name
);
if
(
this
.
selectedItems
.
length
===
this
.
tags
.
length
)
{
this
.
selectAllChecked
=
true
;
}
}
},
deleteSingleItem
(
name
)
{
this
.
itemsToBeDeleted
=
[{
...
this
.
tags
.
find
(
t
=>
t
.
name
===
name
)
}];
this
.
track
(
'
click_button
'
);
this
.
$refs
.
deleteModal
.
show
();
},
deleteMultipleItems
()
{
this
.
itemsToBeDeleted
=
this
.
selectedItems
.
map
(
name
=>
({
deleteTags
(
toBeDeletedList
)
{
this
.
itemsToBeDeleted
=
toBeDeletedList
.
map
(
name
=>
({
...
this
.
tags
.
find
(
t
=>
t
.
name
===
name
),
}));
this
.
track
(
'
click_button
'
);
...
...
@@ -176,7 +75,6 @@ export default {
handleSingleDelete
()
{
const
[
itemToDelete
]
=
this
.
itemsToBeDeleted
;
this
.
itemsToBeDeleted
=
[];
this
.
selectedItems
=
this
.
selectedItems
.
filter
(
name
=>
name
!==
itemToDelete
.
name
);
return
this
.
requestDeleteTag
({
tag
:
itemToDelete
,
params
:
this
.
$route
.
params
.
id
})
.
then
(()
=>
{
this
.
deleteAlertType
=
ALERT_SUCCESS_TAG
;
...
...
@@ -188,7 +86,6 @@ export default {
handleMultipleDelete
()
{
const
{
itemsToBeDeleted
}
=
this
;
this
.
itemsToBeDeleted
=
[];
this
.
selectedItems
=
[];
return
this
.
requestDeleteTags
({
ids
:
itemsToBeDeleted
.
map
(
x
=>
x
.
name
),
...
...
@@ -227,116 +124,14 @@ export default {
<details-header
:image-name=
"imageName"
/>
<gl-table
:items=
"tags"
:fields=
"fields"
:stacked=
"!isDesktop"
show-empty
>
<template
v-if=
"isDesktop"
#head
(
checkbox
)
>
<gl-form-checkbox
ref=
"mainCheckbox"
:checked=
"selectAllChecked"
@
change=
"onSelectAllChange"
/>
</
template
>
<
template
#head
(
actions
)
>
<gl-deprecated-button
ref=
"bulkDeleteButton"
v-gl-tooltip
:disabled=
"!selectedItems || selectedItems.length === 0"
class=
"float-right"
variant=
"danger"
:title=
"$options.i18n.REMOVE_TAGS_BUTTON_TITLE"
:aria-label=
"$options.i18n.REMOVE_TAGS_BUTTON_TITLE"
@
click=
"deleteMultipleItems()"
>
<gl-icon
name=
"remove"
/>
</gl-deprecated-button>
</
template
>
<
template
#cell(checkbox)=
"{item}"
>
<gl-form-checkbox
ref=
"rowCheckbox"
class=
"js-row-checkbox"
:checked=
"selectedItems.includes(item.name)"
@
change=
"updateSelectedItems(item.name)"
/>
</
template
>
<
template
#cell(name)=
"{item, field}"
>
<div
ref=
"rowName"
:class=
"[field.innerClass, 'gl-display-flex']"
>
<span
v-gl-tooltip
data-testid=
"rowNameText"
:title=
"item.name"
class=
"gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap"
>
{{
item
.
name
}}
</span>
<clipboard-button
v-if=
"item.location"
ref=
"rowClipboardButton"
:title=
"item.location"
:text=
"item.location"
css-class=
"btn-default btn-transparent btn-clipboard"
/>
</div>
</
template
>
<
template
#cell(short_revision)=
"{value}"
>
<span
ref=
"rowShortRevision"
>
{{
value
}}
</span>
</
template
>
<
template
#cell(total_size)=
"{item}"
>
<span
ref=
"rowSize"
>
{{
formatSize
(
item
.
total_size
)
}}
<template
v-if=
"item.total_size && item.layers"
>
·
</
template
>
{{ layers(item.layers) }}
</span>
</template>
<
template
#cell(created_at)=
"{value}"
>
<span
ref=
"rowTime"
v-gl-tooltip
:title=
"tooltipTitle(value)"
>
{{
timeFormatted
(
value
)
}}
</span>
</
template
>
<
template
#cell(actions)=
"{item}"
>
<gl-deprecated-button
ref=
"singleDeleteButton"
:title=
"$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:aria-label=
"$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:disabled=
"!item.destroy_path"
variant=
"danger"
class=
"js-delete-registry float-right btn-inverted btn-border-color btn-icon"
@
click=
"deleteSingleItem(item.name)"
>
<gl-icon
name=
"remove"
/>
</gl-deprecated-button>
</
template
>
<tags-table
:tags=
"tags"
:is-loading=
"isLoading"
:is-desktop=
"isDesktop"
@
delete=
"deleteTags"
>
<template
#empty
>
<template
v-if=
"isLoading"
>
<gl-skeleton-loader
v-for=
"index in $options.loader.repeat"
:key=
"index"
:width=
"$options.loader.width"
:height=
"$options.loader.height"
preserve-aspect-ratio=
"xMinYMax meet"
>
<rect
width=
"15"
x=
"0"
y=
"12.5"
height=
"15"
rx=
"4"
/>
<rect
width=
"250"
x=
"25"
y=
"10"
height=
"20"
rx=
"4"
/>
<circle
cx=
"290"
cy=
"20"
r=
"10"
/>
<rect
width=
"100"
x=
"315"
y=
"10"
height=
"20"
rx=
"4"
/>
<rect
width=
"100"
x=
"500"
y=
"10"
height=
"20"
rx=
"4"
/>
<rect
width=
"100"
x=
"630"
y=
"10"
height=
"20"
rx=
"4"
/>
<rect
x=
"960"
y=
"0"
width=
"40"
height=
"40"
rx=
"4"
/>
</gl-skeleton-loader>
<empty-tags-state
:no-containers-image=
"config.noContainersImage"
/>
</
template
>
<gl-empty-state
v-else
:title=
"$options.i18n.EMPTY_IMAGE_REPOSITORY_TITLE"
:svg-path=
"config.noContainersImage"
:description=
"$options.i18n.EMPTY_IMAGE_REPOSITORY_MESSAGE"
class=
"mx-auto my-0"
/>
<
template
#loader
>
<tags-loader
v-once
/>
</
template
>
</
gl
-table>
</
tags
-table>
<gl-pagination
v-if=
"!isLoading"
...
...
app/assets/javascripts/registry/explorer/stores/getters.js
View file @
c8a07385
export
const
tags
=
state
=>
{
// to show the loader inside the table we need to pass an empty array to gl-table whenever the table is loading
// this is to take in account isLoading = true and state.tags =[1,2,3] during pagination and delete
return
state
.
isLoading
?
[]
:
state
.
tags
;
};
export
const
dockerBuildCommand
=
state
=>
{
/* eslint-disable @gitlab/require-i18n-strings */
return
`docker build -t
${
state
.
config
.
repositoryUrl
}
.`
;
...
...
spec/features/groups/container_registry_spec.rb
View file @
c8a07385
...
...
@@ -75,7 +75,7 @@ describe 'Container Registry', :js do
expect
(
service
).
to
receive
(
:execute
).
with
(
container_repository
)
{
{
status: :success
}
}
expect
(
Projects
::
ContainerRepository
::
DeleteTagsService
).
to
receive
(
:new
).
with
(
container_repository
.
project
,
user
,
tags:
[
'latest'
])
{
service
}
click_on
(
class:
'js-delete-registry'
)
first
(
'[data-testid="singleDeleteButton"]'
).
click
expect
(
find
(
'.modal .modal-title'
)).
to
have_content
_
(
'Remove tag'
)
find
(
'.modal .modal-footer .btn-danger'
).
click
end
...
...
spec/features/projects/container_registry_spec.rb
View file @
c8a07385
...
...
@@ -84,7 +84,7 @@ describe 'Container Registry', :js do
expect
(
service
).
to
receive
(
:execute
).
with
(
container_repository
)
{
{
status: :success
}
}
expect
(
Projects
::
ContainerRepository
::
DeleteTagsService
).
to
receive
(
:new
).
with
(
container_repository
.
project
,
user
,
tags:
[
'1'
])
{
service
}
first
(
'
.js-delete-registry
'
).
click
first
(
'
[data-testid="singleDeleteButton"]
'
).
click
expect
(
find
(
'.modal .modal-title'
)).
to
have_content
_
(
'Remove tag'
)
find
(
'.modal .modal-footer .btn-danger'
).
click
end
...
...
spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap
0 → 100644
View file @
c8a07385
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TagsLoader component has the correct markup 1`] = `
<div>
<div
preserve-aspect-ratio="xMinYMax meet"
>
<rect
height="15"
rx="4"
width="15"
x="0"
y="12.5"
/>
<rect
height="20"
rx="4"
width="250"
x="25"
y="10"
/>
<circle
cx="290"
cy="20"
r="10"
/>
<rect
height="20"
rx="4"
width="100"
x="315"
y="10"
/>
<rect
height="20"
rx="4"
width="100"
x="500"
y="10"
/>
<rect
height="20"
rx="4"
width="100"
x="630"
y="10"
/>
<rect
height="40"
rx="4"
width="40"
x="960"
y="0"
/>
</div>
</div>
`;
spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js
View file @
c8a07385
...
...
@@ -19,6 +19,11 @@ describe('Delete alert', () => {
wrapper
=
shallowMount
(
component
,
{
stubs
:
{
GlSprintf
},
propsData
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
describe
(
'
when deleteAlertType is null
'
,
()
=>
{
it
(
'
does not show the alert
'
,
()
=>
{
mountComponent
();
...
...
spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js
View file @
c8a07385
...
...
@@ -23,6 +23,11 @@ describe('Delete Modal', () => {
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
it
(
'
contains a GlModal
'
,
()
=>
{
mountComponent
();
expect
(
findModal
().
exists
()).
toBe
(
true
);
...
...
spec/frontend/registry/explorer/components/details_page/details_header_spec.js
View file @
c8a07385
...
...
@@ -15,6 +15,11 @@ describe('Details Header', () => {
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
it
(
'
has the correct title
'
,
()
=>
{
mountComponent
();
expect
(
wrapper
.
text
()).
toMatchInterpolatedText
(
DETAILS_PAGE_TITLE
);
...
...
spec/frontend/registry/explorer/components/details_page/empty_tags_state.js
0 → 100644
View file @
c8a07385
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlEmptyState
}
from
'
@gitlab/ui
'
;
import
component
from
'
~/registry/explorer/components/details_page/empty_tags_state.vue
'
;
import
{
EMPTY_IMAGE_REPOSITORY_TITLE
,
EMPTY_IMAGE_REPOSITORY_MESSAGE
,
}
from
'
~/registry/explorer/constants
'
;
describe
(
'
EmptyTagsState component
'
,
()
=>
{
let
wrapper
;
const
findEmptyState
=
()
=>
wrapper
.
find
(
GlEmptyState
);
const
mountComponent
=
()
=>
{
wrapper
=
shallowMount
(
component
,
{
stubs
:
{
GlEmptyState
,
},
propsData
:
{
noContainersImage
:
'
foo
'
,
},
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
it
(
'
contains gl-empty-state
'
,
()
=>
{
mountComponent
();
expect
(
findEmptyState
().
exist
()).
toBe
(
true
);
});
it
(
'
has the correct props
'
,
()
=>
{
mountComponent
();
expect
(
findEmptyState
().
props
()).
toMatchObject
({
title
:
EMPTY_IMAGE_REPOSITORY_TITLE
,
description
:
EMPTY_IMAGE_REPOSITORY_MESSAGE
,
svgPath
:
'
foo
'
,
});
});
});
spec/frontend/registry/explorer/components/details_page/tags_loader_spec.js
0 → 100644
View file @
c8a07385
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
component
from
'
~/registry/explorer/components/details_page/tags_loader.vue
'
;
import
{
GlSkeletonLoader
}
from
'
../../stubs
'
;
describe
(
'
TagsLoader component
'
,
()
=>
{
let
wrapper
;
const
findGlSkeletonLoaders
=
()
=>
wrapper
.
findAll
(
GlSkeletonLoader
);
const
mountComponent
=
()
=>
{
wrapper
=
shallowMount
(
component
,
{
stubs
:
{
GlSkeletonLoader
,
},
// set the repeat to 1 to avoid a long and verbose snapshot
loader
:
{
...
component
.
loader
,
repeat
:
1
,
},
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
it
(
'
produces the correct amount of loaders
'
,
()
=>
{
mountComponent
();
expect
(
findGlSkeletonLoaders
().
length
).
toBe
(
1
);
});
it
(
'
has the correct props
'
,
()
=>
{
mountComponent
();
expect
(
findGlSkeletonLoaders
()
.
at
(
0
)
.
props
(),
).
toMatchObject
({
width
:
component
.
loader
.
width
,
height
:
component
.
loader
.
height
,
});
});
it
(
'
has the correct markup
'
,
()
=>
{
mountComponent
();
expect
(
wrapper
.
element
).
toMatchSnapshot
();
});
});
spec/frontend/registry/explorer/components/details_page/tags_table_spec.js
0 → 100644
View file @
c8a07385
import
{
mount
}
from
'
@vue/test-utils
'
;
import
stubChildren
from
'
helpers/stub_children
'
;
import
component
from
'
~/registry/explorer/components/details_page/tags_table.vue
'
;
import
{
tagsListResponse
}
from
'
../../mock_data
'
;
describe
(
'
tags_table
'
,
()
=>
{
let
wrapper
;
const
tags
=
[...
tagsListResponse
.
data
];
const
findMainCheckbox
=
()
=>
wrapper
.
find
(
'
[data-testid="mainCheckbox"]
'
);
const
findFirstRowItem
=
testid
=>
wrapper
.
find
(
`[data-testid="
${
testid
}
"]`
);
const
findBulkDeleteButton
=
()
=>
wrapper
.
find
(
'
[data-testid="bulkDeleteButton"]
'
);
const
findAllDeleteButtons
=
()
=>
wrapper
.
findAll
(
'
[data-testid="singleDeleteButton"]
'
);
const
findAllCheckboxes
=
()
=>
wrapper
.
findAll
(
'
[data-testid="rowCheckbox"]
'
);
const
findCheckedCheckboxes
=
()
=>
findAllCheckboxes
().
filter
(
c
=>
c
.
attributes
(
'
checked
'
));
const
findFirsTagColumn
=
()
=>
wrapper
.
find
(
'
.js-tag-column
'
);
const
findFirstTagNameText
=
()
=>
wrapper
.
find
(
'
[data-testid="rowNameText"]
'
);
const
findLoaderSlot
=
()
=>
wrapper
.
find
(
'
[data-testid="loaderSlot"]
'
);
const
findEmptySlot
=
()
=>
wrapper
.
find
(
'
[data-testid="emptySlot"]
'
);
const
mountComponent
=
(
propsData
=
{
tags
,
isDesktop
:
true
})
=>
{
wrapper
=
mount
(
component
,
{
stubs
:
{
...
stubChildren
(
component
),
GlTable
:
false
,
},
propsData
,
slots
:
{
loader
:
'
<div data-testid="loaderSlot"></div>
'
,
empty
:
'
<div data-testid="emptySlot"></div>
'
,
},
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
it
.
each
([
'
rowCheckbox
'
,
'
rowName
'
,
'
rowShortRevision
'
,
'
rowSize
'
,
'
rowTime
'
,
'
singleDeleteButton
'
,
])(
'
%s exist in the table
'
,
element
=>
{
mountComponent
();
expect
(
findFirstRowItem
(
element
).
exists
()).
toBe
(
true
);
});
describe
(
'
header checkbox
'
,
()
=>
{
it
(
'
exists
'
,
()
=>
{
mountComponent
();
expect
(
findMainCheckbox
().
exists
()).
toBe
(
true
);
});
it
(
'
if selected selects all the rows
'
,
()
=>
{
mountComponent
();
findMainCheckbox
().
vm
.
$emit
(
'
change
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
findMainCheckbox
().
attributes
(
'
checked
'
)).
toBeTruthy
();
expect
(
findCheckedCheckboxes
()).
toHaveLength
(
tags
.
length
);
});
});
it
(
'
if deselect deselects all the row
'
,
()
=>
{
mountComponent
();
findMainCheckbox
().
vm
.
$emit
(
'
change
'
);
return
wrapper
.
vm
.
$nextTick
()
.
then
(()
=>
{
expect
(
findMainCheckbox
().
attributes
(
'
checked
'
)).
toBeTruthy
();
findMainCheckbox
().
vm
.
$emit
(
'
change
'
);
return
wrapper
.
vm
.
$nextTick
();
})
.
then
(()
=>
{
expect
(
findMainCheckbox
().
attributes
(
'
checked
'
)).
toBe
(
undefined
);
expect
(
findCheckedCheckboxes
()).
toHaveLength
(
0
);
});
});
});
describe
(
'
row checkbox
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
();
});
it
(
'
if selected adds item to selectedItems
'
,
()
=>
{
findFirstRowItem
(
'
rowCheckbox
'
).
vm
.
$emit
(
'
change
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
wrapper
.
vm
.
selectedItems
).
toEqual
([
tags
[
0
].
name
]);
expect
(
findFirstRowItem
(
'
rowCheckbox
'
).
attributes
(
'
checked
'
)).
toBeTruthy
();
});
});
it
(
'
if deselect remove name from selectedItems
'
,
()
=>
{
wrapper
.
setData
({
selectedItems
:
[
tags
[
0
].
name
]
});
findFirstRowItem
(
'
rowCheckbox
'
).
vm
.
$emit
(
'
change
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
wrapper
.
vm
.
selectedItems
.
length
).
toBe
(
0
);
expect
(
findFirstRowItem
(
'
rowCheckbox
'
).
attributes
(
'
checked
'
)).
toBe
(
undefined
);
});
});
});
describe
(
'
header delete button
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
();
});
it
(
'
exists
'
,
()
=>
{
expect
(
findBulkDeleteButton
().
exists
()).
toBe
(
true
);
});
it
(
'
is disabled if no item is selected
'
,
()
=>
{
expect
(
findBulkDeleteButton
().
attributes
(
'
disabled
'
)).
toBe
(
'
true
'
);
});
it
(
'
is enabled if at least one item is selected
'
,
()
=>
{
expect
(
findBulkDeleteButton
().
attributes
(
'
disabled
'
)).
toBe
(
'
true
'
);
findFirstRowItem
(
'
rowCheckbox
'
).
vm
.
$emit
(
'
change
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
findBulkDeleteButton
().
attributes
(
'
disabled
'
)).
toBeFalsy
();
});
});
describe
(
'
on click
'
,
()
=>
{
it
(
'
when one item is selected
'
,
()
=>
{
findFirstRowItem
(
'
rowCheckbox
'
).
vm
.
$emit
(
'
change
'
);
findBulkDeleteButton
().
vm
.
$emit
(
'
click
'
);
expect
(
wrapper
.
emitted
(
'
delete
'
)).
toEqual
([[[
'
centos6
'
]]]);
});
it
(
'
when multiple items are selected
'
,
()
=>
{
findMainCheckbox
().
vm
.
$emit
(
'
change
'
);
findBulkDeleteButton
().
vm
.
$emit
(
'
click
'
);
expect
(
wrapper
.
emitted
(
'
delete
'
)).
toEqual
([[
tags
.
map
(
t
=>
t
.
name
)]]);
});
});
});
describe
(
'
row delete button
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
();
});
it
(
'
exists
'
,
()
=>
{
expect
(
findAllDeleteButtons
()
.
at
(
0
)
.
exists
(),
).
toBe
(
true
);
});
it
(
'
is disabled if the item has no destroy_path
'
,
()
=>
{
expect
(
findAllDeleteButtons
()
.
at
(
1
)
.
attributes
(
'
disabled
'
),
).
toBe
(
'
true
'
);
});
it
(
'
on click
'
,
()
=>
{
findAllDeleteButtons
()
.
at
(
0
)
.
vm
.
$emit
(
'
click
'
);
expect
(
wrapper
.
emitted
(
'
delete
'
)).
toEqual
([[[
'
centos6
'
]]]);
});
});
describe
(
'
name cell
'
,
()
=>
{
it
(
'
tag column has a tooltip with the tag name
'
,
()
=>
{
mountComponent
();
expect
(
findFirstTagNameText
().
attributes
(
'
title
'
)).
toBe
(
tagsListResponse
.
data
[
0
].
name
);
});
describe
(
'
on desktop viewport
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
();
});
it
(
'
table header has class w-25
'
,
()
=>
{
expect
(
findFirsTagColumn
().
classes
()).
toContain
(
'
w-25
'
);
});
it
(
'
tag column has the mw-m class
'
,
()
=>
{
expect
(
findFirstRowItem
(
'
rowName
'
).
classes
()).
toContain
(
'
mw-m
'
);
});
});
describe
(
'
on mobile viewport
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
({
tags
,
isDesktop
:
false
});
});
it
(
'
table header does not have class w-25
'
,
()
=>
{
expect
(
findFirsTagColumn
().
classes
()).
not
.
toContain
(
'
w-25
'
);
});
it
(
'
tag column has the gl-justify-content-end class
'
,
()
=>
{
expect
(
findFirstRowItem
(
'
rowName
'
).
classes
()).
toContain
(
'
gl-justify-content-end
'
);
});
});
});
describe
(
'
last updated cell
'
,
()
=>
{
let
timeCell
;
beforeEach
(()
=>
{
mountComponent
();
timeCell
=
findFirstRowItem
(
'
rowTime
'
);
});
it
(
'
displays the time in string format
'
,
()
=>
{
expect
(
timeCell
.
text
()).
toBe
(
'
2 years ago
'
);
});
it
(
'
has a tooltip timestamp
'
,
()
=>
{
expect
(
timeCell
.
attributes
(
'
title
'
)).
toBe
(
'
Sep 19, 2017 1:45pm GMT+0000
'
);
});
});
describe
(
'
empty state slot
'
,
()
=>
{
describe
(
'
when the table is empty
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
({
tags
:
[],
isDesktop
:
true
});
});
it
(
'
does not show table rows
'
,
()
=>
{
expect
(
findFirstTagNameText
().
exists
()).
toBe
(
false
);
});
it
(
'
has the empty state slot
'
,
()
=>
{
expect
(
findEmptySlot
().
exists
()).
toBe
(
true
);
});
});
describe
(
'
when the table is not empty
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
({
tags
,
isDesktop
:
true
});
});
it
(
'
does show table rows
'
,
()
=>
{
expect
(
findFirstTagNameText
().
exists
()).
toBe
(
true
);
});
it
(
'
does not show the empty state
'
,
()
=>
{
expect
(
findEmptySlot
().
exists
()).
toBe
(
false
);
});
});
});
describe
(
'
loader slot
'
,
()
=>
{
describe
(
'
when the data is loading
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
({
isLoading
:
true
,
tags
});
});
it
(
'
show the loader
'
,
()
=>
{
expect
(
findLoaderSlot
().
exists
()).
toBe
(
true
);
});
it
(
'
does not show the table rows
'
,
()
=>
{
expect
(
findFirstTagNameText
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
when the data is not loading
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
({
isLoading
:
false
,
tags
});
});
it
(
'
does not show the loader
'
,
()
=>
{
expect
(
findLoaderSlot
().
exists
()).
toBe
(
false
);
});
it
(
'
shows the table rows
'
,
()
=>
{
expect
(
findFirstTagNameText
().
exists
()).
toBe
(
true
);
});
});
});
});
spec/frontend/registry/explorer/components/list_page/image_list_spec.js
View file @
c8a07385
...
...
@@ -24,6 +24,11 @@ describe('Image List', () => {
mountComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
describe
(
'
list
'
,
()
=>
{
it
(
'
contains one list element for each image
'
,
()
=>
{
expect
(
findRow
().
length
).
toBe
(
imagesListResponse
.
data
.
length
);
...
...
spec/frontend/registry/explorer/pages/details_spec.js
View file @
c8a07385
import
{
m
ount
}
from
'
@vue/test-utils
'
;
import
{
Gl
Table
,
GlPagination
,
GlSkeletonLoader
}
from
'
@gitlab/ui
'
;
import
{
shallowM
ount
}
from
'
@vue/test-utils
'
;
import
{
Gl
Pagination
}
from
'
@gitlab/ui
'
;
import
Tracking
from
'
~/tracking
'
;
import
stubChildren
from
'
helpers/stub_children
'
;
import
component
from
'
~/registry/explorer/pages/details.vue
'
;
import
DeleteAlert
from
'
~/registry/explorer/components/details_page/delete_alert.vue
'
;
import
DeleteModal
from
'
~/registry/explorer/components/details_page/delete_modal.vue
'
;
import
DetailsHeader
from
'
~/registry/explorer/components/details_page/details_header.vue
'
;
import
TagsLoader
from
'
~/registry/explorer/components/details_page/tags_loader.vue
'
;
import
EmptyTagsState
from
'
~/registry/explorer/components/details_page/empty_tags_state.vue
'
;
import
{
createStore
}
from
'
~/registry/explorer/stores/
'
;
import
{
SET_MAIN_LOADING
,
...
...
@@ -15,7 +15,7 @@ import {
}
from
'
~/registry/explorer/stores/mutation_types/
'
;
import
{
tagsListResponse
}
from
'
../mock_data
'
;
import
{
$toast
}
from
'
../../shared/mock
s
'
;
import
{
TagsTable
,
DeleteModal
}
from
'
../stub
s
'
;
describe
(
'
Details Page
'
,
()
=>
{
let
wrapper
;
...
...
@@ -24,28 +24,19 @@ describe('Details Page', () => {
const
findDeleteModal
=
()
=>
wrapper
.
find
(
DeleteModal
);
const
findPagination
=
()
=>
wrapper
.
find
(
GlPagination
);
const
findSkeletonLoader
=
()
=>
wrapper
.
find
(
GlSkeletonLoader
);
const
findMainCheckbox
=
()
=>
wrapper
.
find
({
ref
:
'
mainCheckbox
'
});
const
findFirstRowItem
=
ref
=>
wrapper
.
find
({
ref
});
const
findBulkDeleteButton
=
()
=>
wrapper
.
find
({
ref
:
'
bulkDeleteButton
'
});
// findAll and refs seems to no work falling back to class
const
findAllDeleteButtons
=
()
=>
wrapper
.
findAll
(
'
.js-delete-registry
'
);
const
findAllCheckboxes
=
()
=>
wrapper
.
findAll
(
'
.js-row-checkbox
'
);
const
findCheckedCheckboxes
=
()
=>
findAllCheckboxes
().
filter
(
c
=>
c
.
attributes
(
'
checked
'
));
const
findFirsTagColumn
=
()
=>
wrapper
.
find
(
'
.js-tag-column
'
);
const
findFirstTagNameText
=
()
=>
wrapper
.
find
(
'
[data-testid="rowNameText"]
'
);
const
findTagsLoader
=
()
=>
wrapper
.
find
(
TagsLoader
);
const
findTagsTable
=
()
=>
wrapper
.
find
(
TagsTable
);
const
findDeleteAlert
=
()
=>
wrapper
.
find
(
DeleteAlert
);
const
findDetailsHeader
=
()
=>
wrapper
.
find
(
DetailsHeader
);
const
findEmptyTagsState
=
()
=>
wrapper
.
find
(
EmptyTagsState
);
const
routeId
=
window
.
btoa
(
JSON
.
stringify
({
name
:
'
foo
'
,
tags_path
:
'
bar
'
}));
const
mountComponent
=
options
=>
{
wrapper
=
m
ount
(
component
,
{
wrapper
=
shallowM
ount
(
component
,
{
store
,
stubs
:
{
...
stubChildren
(
component
),
GlSprintf
:
false
,
GlTable
,
TagsTable
,
DeleteModal
,
},
mocks
:
{
...
...
@@ -54,7 +45,6 @@ describe('Details Page', () => {
id
:
routeId
,
},
},
$toast
,
},
...
options
,
});
...
...
@@ -67,7 +57,6 @@ describe('Details Page', () => {
store
.
commit
(
SET_TAGS_LIST_SUCCESS
,
tagsListResponse
.
data
);
store
.
commit
(
SET_TAGS_PAGINATION
,
tagsListResponse
.
headers
);
jest
.
spyOn
(
Tracking
,
'
event
'
);
jest
.
spyOn
(
DeleteModal
.
methods
,
'
show
'
);
});
afterEach
(()
=>
{
...
...
@@ -78,18 +67,14 @@ describe('Details Page', () => {
describe
(
'
when isLoading is true
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
();
store
.
dispatch
(
'
receiveTagsListSuccess
'
,
{
...
tagsListResponse
,
data
:
[]
});
store
.
commit
(
SET_MAIN_LOADING
,
true
);
return
wrapper
.
vm
.
$nextTick
();
});
after
All
(()
=>
store
.
commit
(
SET_MAIN_LOADING
,
false
));
after
Each
(()
=>
store
.
commit
(
SET_MAIN_LOADING
,
false
));
it
(
'
has a skeleton loader
'
,
()
=>
{
expect
(
findSkeletonLoader
().
exists
()).
toBe
(
true
);
});
it
(
'
does not have list items
'
,
()
=>
{
expect
(
findFirstRowItem
(
'
rowCheckbox
'
).
exists
()).
toBe
(
false
);
it
(
'
binds isLoading to tags-table
'
,
()
=>
{
expect
(
findTagsTable
().
props
(
'
isLoading
'
)).
toBe
(
true
);
});
it
(
'
does not show pagination
'
,
()
=>
{
...
...
@@ -97,204 +82,76 @@ describe('Details Page', () => {
});
});
describe
(
'
table
'
,
()
=>
{
it
.
each
([
'
rowCheckbox
'
,
'
rowName
'
,
'
rowShortRevision
'
,
'
rowSize
'
,
'
rowTime
'
,
'
singleDeleteButton
'
,
])(
'
%s exist in the table
'
,
element
=>
{
mountComponent
();
expect
(
findFirstRowItem
(
element
).
exists
()).
toBe
(
true
);
});
describe
(
'
header checkbox
'
,
()
=>
{
describe
(
'
table slots
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
();
});
it
(
'
exists
'
,
()
=>
{
expect
(
findMainCheckbox
().
exists
()).
toBe
(
true
);
});
it
(
'
if selected set selectedItem and allSelected
'
,
()
=>
{
findMainCheckbox
().
vm
.
$emit
(
'
change
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
findMainCheckbox
().
attributes
(
'
checked
'
)).
toBeTruthy
();
expect
(
findCheckedCheckboxes
()).
toHaveLength
(
store
.
state
.
tags
.
length
);
});
it
(
'
has the empty state
'
,
()
=>
{
expect
(
findEmptyTagsState
().
exists
()).
toBe
(
true
);
});
it
(
'
if deselect unset selectedItem and allSelected
'
,
()
=>
{
wrapper
.
setData
({
selectedItems
:
[
1
,
2
],
selectAllChecked
:
true
});
findMainCheckbox
().
vm
.
$emit
(
'
change
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
findMainCheckbox
().
attributes
(
'
checked
'
)).
toBe
(
undefined
);
expect
(
findCheckedCheckboxes
()).
toHaveLength
(
0
);
});
});
});
describe
(
'
row checkbox
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
();
});
it
(
'
if selected adds item to selectedItems
'
,
()
=>
{
findFirstRowItem
(
'
rowCheckbox
'
).
vm
.
$emit
(
'
change
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
wrapper
.
vm
.
selectedItems
).
toEqual
([
store
.
state
.
tags
[
1
].
name
]);
expect
(
findFirstRowItem
(
'
rowCheckbox
'
).
attributes
(
'
checked
'
)).
toBeTruthy
();
});
});
it
(
'
if deselect remove name from selectedItems
'
,
()
=>
{
wrapper
.
setData
({
selectedItems
:
[
store
.
state
.
tags
[
1
].
name
]
});
findFirstRowItem
(
'
rowCheckbox
'
).
vm
.
$emit
(
'
change
'
);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
wrapper
.
vm
.
selectedItems
.
length
).
toBe
(
0
);
expect
(
findFirstRowItem
(
'
rowCheckbox
'
).
attributes
(
'
checked
'
)).
toBe
(
undefined
);
});
it
(
'
has a skeleton loader
'
,
()
=>
{
expect
(
findTagsLoader
().
exists
()).
toBe
(
true
);
});
});
describe
(
'
header delete button
'
,
()
=>
{
describe
(
'
table
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
();
});
it
(
'
exists
'
,
()
=>
{
mountComponent
();
expect
(
findBulkDeleteButton
().
exists
()).
toBe
(
true
);
});
it
(
'
is disabled if no item is selected
'
,
()
=>
{
mountComponent
();
expect
(
findBulkDeleteButton
().
attributes
(
'
disabled
'
)).
toBe
(
'
true
'
);
expect
(
findTagsTable
().
exists
()).
toBe
(
true
);
});
it
(
'
is enabled if at least one item is selected
'
,
()
=>
{
mountComponent
({
data
:
()
=>
({
selectedItems
:
[
store
.
state
.
tags
[
0
].
name
]
})
});
wrapper
.
setData
({
selectedItems
:
[
1
]
});
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
expect
(
findBulkDeleteButton
().
attributes
(
'
disabled
'
)).
toBeFalsy
();
});
});
describe
(
'
on click
'
,
()
=>
{
it
(
'
when one item is selected
'
,
()
=>
{
mountComponent
({
data
:
()
=>
({
selectedItems
:
[
store
.
state
.
tags
[
0
].
name
]
})
});
jest
.
spyOn
(
wrapper
.
vm
.
$refs
.
deleteModal
,
'
show
'
);
findBulkDeleteButton
().
vm
.
$emit
(
'
click
'
);
expect
(
wrapper
.
vm
.
itemsToBeDeleted
).
toEqual
([
store
.
state
.
tags
[
0
]]);
expect
(
DeleteModal
.
methods
.
show
).
toHaveBeenCalled
();
expect
(
Tracking
.
event
).
toHaveBeenCalledWith
(
undefined
,
'
click_button
'
,
{
label
:
'
registry_tag_delete
'
,
});
it
(
'
has the correct props bound
'
,
()
=>
{
expect
(
findTagsTable
().
props
()).
toMatchObject
({
isDesktop
:
true
,
isLoading
:
false
,
tags
:
store
.
state
.
tags
,
});
it
(
'
when multiple items are selected
'
,
()
=>
{
mountComponent
({
data
:
()
=>
({
selectedItems
:
store
.
state
.
tags
.
map
(
t
=>
t
.
name
)
}),
});
findBulkDeleteButton
().
vm
.
$emit
(
'
click
'
);
expect
(
wrapper
.
vm
.
itemsToBeDeleted
).
toEqual
(
tagsListResponse
.
data
);
expect
(
DeleteModal
.
methods
.
show
).
toHaveBeenCalled
();
expect
(
Tracking
.
event
).
toHaveBeenCalledWith
(
undefined
,
'
click_button
'
,
{
label
:
'
bulk_registry_tag_delete
'
,
});
});
});
});
describe
(
'
row delete button
'
,
()
=>
{
describe
(
'
deleteEvent
'
,
()
=>
{
describe
(
'
single item
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
(
);
findTagsTable
().
vm
.
$emit
(
'
delete
'
,
[
store
.
state
.
tags
[
0
].
name
]
);
});
it
(
'
exists
'
,
()
=>
{
expect
(
findAllDeleteButtons
()
.
at
(
0
)
.
exists
(),
).
toBe
(
true
);
it
(
'
open the modal
'
,
()
=>
{
expect
(
DeleteModal
.
methods
.
show
).
toHaveBeenCalled
();
});
it
(
'
is disabled if the item has no destroy_path
'
,
()
=>
{
expect
(
findAllDeleteButtons
()
.
at
(
1
)
.
attributes
(
'
disabled
'
),
).
toBe
(
'
true
'
);
it
(
'
maps the selection to itemToBeDeleted
'
,
()
=>
{
expect
(
wrapper
.
vm
.
itemsToBeDeleted
).
toEqual
([
store
.
state
.
tags
[
0
]]);
});
it
(
'
on click
'
,
()
=>
{
findAllDeleteButtons
()
.
at
(
0
)
.
vm
.
$emit
(
'
click
'
);
expect
(
DeleteModal
.
methods
.
show
).
toHaveBeenCalled
();
it
(
'
tracks a single delete event
'
,
()
=>
{
expect
(
Tracking
.
event
).
toHaveBeenCalledWith
(
undefined
,
'
click_button
'
,
{
label
:
'
registry_tag_delete
'
,
});
});
});
describe
(
'
name cell
'
,
()
=>
{
it
(
'
tag column has a tooltip with the tag name
'
,
()
=>
{
mountComponent
();
expect
(
findFirstTagNameText
().
attributes
(
'
title
'
)).
toBe
(
tagsListResponse
.
data
[
0
].
name
);
});
describe
(
'
on desktop viewport
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
();
});
it
(
'
table header has class w-25
'
,
()
=>
{
expect
(
findFirsTagColumn
().
classes
()).
toContain
(
'
w-25
'
);
});
it
(
'
tag column has the mw-m class
'
,
()
=>
{
expect
(
findFirstRowItem
(
'
rowName
'
).
classes
()).
toContain
(
'
mw-m
'
);
});
});
describe
(
'
on mobile viewport
'
,
()
=>
{
describe
(
'
multiple items
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
({
data
()
{
return
{
isDesktop
:
false
};
},
});
findTagsTable
().
vm
.
$emit
(
'
delete
'
,
store
.
state
.
tags
.
map
(
t
=>
t
.
name
));
});
it
(
'
table header does not have class w-25
'
,
()
=>
{
expect
(
findFirsTagColumn
().
classes
()).
not
.
toContain
(
'
w-25
'
);
it
(
'
open the modal
'
,
()
=>
{
expect
(
DeleteModal
.
methods
.
show
).
toHaveBeenCalled
(
);
});
it
(
'
tag column has the gl-justify-content-end class
'
,
()
=>
{
expect
(
findFirstRowItem
(
'
rowName
'
).
classes
()).
toContain
(
'
gl-justify-content-end
'
);
});
it
(
'
maps the selection to itemToBeDeleted
'
,
()
=>
{
expect
(
wrapper
.
vm
.
itemsToBeDeleted
).
toEqual
(
store
.
state
.
tags
);
});
});
describe
(
'
last updated cell
'
,
()
=>
{
let
timeCell
;
beforeEach
(
()
=>
{
mountComponent
();
timeCell
=
findFirstRowItem
(
'
rowTime
'
);
it
(
'
tracks a single delete event
'
,
()
=>
{
expect
(
Tracking
.
event
).
toHaveBeenCalledWith
(
undefined
,
'
click_button
'
,
{
label
:
'
bulk_registry_tag_delete
'
,
});
it
(
'
displays the time in string format
'
,
()
=>
{
expect
(
timeCell
.
text
()).
toBe
(
'
2 years ago
'
);
});
it
(
'
has a tooltip timestamp
'
,
()
=>
{
expect
(
timeCell
.
attributes
(
'
title
'
)).
toBe
(
'
Sep 19, 2017 1:45pm GMT+0000
'
);
});
});
});
...
...
@@ -343,44 +200,33 @@ describe('Details Page', () => {
describe
(
'
confirmDelete event
'
,
()
=>
{
describe
(
'
when one item is selected to be deleted
'
,
()
=>
{
const
itemsToBeDeleted
=
[{
name
:
'
foo
'
}];
beforeEach
(()
=>
{
mountComponent
();
findTagsTable
().
vm
.
$emit
(
'
delete
'
,
[
store
.
state
.
tags
[
0
].
name
]);
});
it
(
'
dispatch requestDeleteTag with the right parameters
'
,
()
=>
{
mountComponent
({
data
:
()
=>
({
itemsToBeDeleted
})
});
findDeleteModal
().
vm
.
$emit
(
'
confirmDelete
'
);
expect
(
dispatchSpy
).
toHaveBeenCalledWith
(
'
requestDeleteTag
'
,
{
tag
:
itemsToBeDeleted
[
0
],
tag
:
store
.
state
.
tags
[
0
],
params
:
routeId
,
});
});
it
(
'
remove the deleted item from the selected items
'
,
()
=>
{
mountComponent
({
data
:
()
=>
({
itemsToBeDeleted
,
selectedItems
:
[
'
foo
'
,
'
bar
'
]
})
});
findDeleteModal
().
vm
.
$emit
(
'
confirmDelete
'
);
expect
(
wrapper
.
vm
.
selectedItems
).
toEqual
([
'
bar
'
]);
});
});
describe
(
'
when more than one item is selected to be deleted
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
({
data
:
()
=>
({
itemsToBeDeleted
:
[{
name
:
'
foo
'
},
{
name
:
'
bar
'
}],
selectedItems
:
[
'
foo
'
,
'
bar
'
],
}),
});
mountComponent
();
findTagsTable
().
vm
.
$emit
(
'
delete
'
,
store
.
state
.
tags
.
map
(
t
=>
t
.
name
));
});
it
(
'
dispatch requestDeleteTags with the right parameters
'
,
()
=>
{
findDeleteModal
().
vm
.
$emit
(
'
confirmDelete
'
);
expect
(
dispatchSpy
).
toHaveBeenCalledWith
(
'
requestDeleteTags
'
,
{
ids
:
[
'
foo
'
,
'
bar
'
]
,
ids
:
store
.
state
.
tags
.
map
(
t
=>
t
.
name
)
,
params
:
routeId
,
});
});
it
(
'
clears the selectedItems
'
,
()
=>
{
findDeleteModal
().
vm
.
$emit
(
'
confirmDelete
'
);
expect
(
wrapper
.
vm
.
selectedItems
).
toEqual
([]);
});
});
});
});
...
...
spec/frontend/registry/explorer/stores/getters_spec.js
View file @
c8a07385
...
...
@@ -2,35 +2,6 @@ import * as getters from '~/registry/explorer/stores/getters';
describe
(
'
Getters RegistryExplorer store
'
,
()
=>
{
let
state
;
const
tags
=
[
'
foo
'
,
'
bar
'
];
describe
(
'
tags
'
,
()
=>
{
describe
(
'
when isLoading is false
'
,
()
=>
{
beforeEach
(()
=>
{
state
=
{
tags
,
isLoading
:
false
,
};
});
it
(
'
returns tags
'
,
()
=>
{
expect
(
getters
.
tags
(
state
)).
toEqual
(
state
.
tags
);
});
});
describe
(
'
when isLoading is true
'
,
()
=>
{
beforeEach
(()
=>
{
state
=
{
tags
,
isLoading
:
true
,
};
});
it
(
'
returns empty array
'
,
()
=>
{
expect
(
getters
.
tags
(
state
)).
toEqual
([]);
});
});
});
describe
.
each
`
getter | prefix | configParameter | suffix
...
...
spec/frontend/registry/explorer/stubs.js
View file @
c8a07385
import
RealTagsTable
from
'
~/registry/explorer/components/details_page/tags_table.vue
'
;
import
RealDeleteModal
from
'
~/registry/explorer/components/details_page/delete_modal.vue
'
;
export
const
GlModal
=
{
template
:
'
<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>
'
,
methods
:
{
...
...
@@ -14,3 +17,21 @@ export const RouterLink = {
template
:
`<div><slot></slot></div>`
,
props
:
[
'
to
'
],
};
export
const
TagsTable
=
{
props
:
RealTagsTable
.
props
,
template
:
`<div><slot name="empty"></slot><slot name="loader"></slot></div>`
,
};
export
const
DeleteModal
=
{
template
:
'
<div></div>
'
,
methods
:
{
show
:
jest
.
fn
(),
},
props
:
RealDeleteModal
.
props
,
};
export
const
GlSkeletonLoader
=
{
template
:
`<div><slot></slot></div>`
,
props
:
[
'
width
'
,
'
height
'
],
};
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