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
c0ec6ddc
Commit
c0ec6ddc
authored
Feb 05, 2021
by
Nicolò Maria Mezzopera
Committed by
Mark Florian
Feb 05, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Improve delete modal to support image delete
- source - tests
parent
bcb377af
Changes
11
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
455 additions
and
67 deletions
+455
-67
app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
...egistry/explorer/components/details_page/delete_modal.vue
+28
-9
app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
...istry/explorer/components/details_page/details_header.vue
+27
-7
app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue
...egistry/explorer/components/details_page/status_alert.vue
+50
-0
app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
...s/registry/explorer/components/details_page/tags_list.vue
+10
-1
app/assets/javascripts/registry/explorer/pages/details.vue
app/assets/javascripts/registry/explorer/pages/details.vue
+55
-4
changelogs/unreleased/216761-add-image-repository-level-delete-functionality-to-the-image-repos.yml
...ository-level-delete-functionality-to-the-image-repos.yml
+5
-0
spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js
...try/explorer/components/details_page/delete_modal_spec.js
+46
-20
spec/frontend/registry/explorer/components/details_page/details_header_spec.js
...y/explorer/components/details_page/details_header_spec.js
+69
-14
spec/frontend/registry/explorer/components/details_page/status_alert_spec.js
...try/explorer/components/details_page/status_alert_spec.js
+57
-0
spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
...gistry/explorer/components/details_page/tags_list_spec.js
+19
-11
spec/frontend/registry/explorer/pages/details_spec.js
spec/frontend/registry/explorer/pages/details_spec.js
+89
-1
No files found.
app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
View file @
c0ec6ddc
<
script
>
import
{
GlModal
,
GlSprintf
}
from
'
@gitlab/ui
'
;
import
{
n__
}
from
'
~/locale
'
;
import
{
REMOVE_TAG_CONFIRMATION_TEXT
,
REMOVE_TAGS_CONFIRMATION_TEXT
}
from
'
../../constants/index
'
;
import
{
REMOVE_TAG_CONFIRMATION_TEXT
,
REMOVE_TAGS_CONFIRMATION_TEXT
,
DELETE_IMAGE_CONFIRMATION_TITLE
,
DELETE_IMAGE_CONFIRMATION_TEXT
,
}
from
'
../../constants
'
;
export
default
{
components
:
{
...
...
@@ -14,9 +19,17 @@ export default {
required
:
false
,
default
:
()
=>
[],
},
deleteImage
:
{
type
:
Boolean
,
default
:
false
,
required
:
false
,
},
},
computed
:
{
modalAction
()
{
modalTitle
()
{
if
(
this
.
deleteImage
)
{
return
DELETE_IMAGE_CONFIRMATION_TITLE
;
}
return
n__
(
'
ContainerRegistry|Remove tag
'
,
'
ContainerRegistry|Remove tags
'
,
...
...
@@ -24,14 +37,19 @@ export default {
);
},
modalDescription
()
{
if
(
this
.
deleteImage
)
{
return
{
message
:
DELETE_IMAGE_CONFIRMATION_TEXT
,
};
}
if
(
this
.
itemsToBeDeleted
.
length
>
1
)
{
return
{
message
:
REMOVE_TAGS_CONFIRMATION_TEXT
,
item
:
this
.
itemsToBeDeleted
.
length
,
};
}
const
[
first
]
=
this
.
itemsToBeDeleted
;
const
[
first
]
=
this
.
itemsToBeDeleted
;
return
{
message
:
REMOVE_TAG_CONFIRMATION_TEXT
,
item
:
first
?.
path
,
...
...
@@ -51,16 +69,17 @@ export default {
ref=
"deleteModal"
modal-id=
"delete-tag-modal"
ok-variant=
"danger"
@
ok=
"$emit('confirmDelete')"
:action-primary=
"
{ text: __('Confirm'), attributes: { variant: 'danger' } }"
:action-cancel="{ text: __('Cancel') }"
@primary="$emit('confirmDelete')"
@cancel="$emit('cancelDelete')"
>
<template
#modal-title
>
{{
modalAction
}}
</
template
>
<
template
#modal-ok
>
{{
modalAction
}}
</
template
>
<template
#modal-title
>
{{
modalTitle
}}
</
template
>
<p
v-if=
"modalDescription"
data-testid=
"description"
>
<gl-sprintf
:message=
"modalDescription.message"
>
<
template
#item
><b>
{{
modalDescription
.
item
}}
</b></
template
>
<
template
#item
>
<b>
{{
modalDescription
.
item
}}
</b>
</
template
>
</gl-sprintf>
</p>
</gl-modal>
...
...
app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
View file @
c0ec6ddc
<
script
>
import
{
GlSprintf
}
from
'
@gitlab/ui
'
;
import
{
GlSprintf
,
GlButton
}
from
'
@gitlab/ui
'
;
import
{
sprintf
,
n__
}
from
'
~/locale
'
;
import
TitleArea
from
'
~/vue_shared/components/registry/title_area.vue
'
;
import
MetadataItem
from
'
~/vue_shared/components/registry/metadata_item.vue
'
;
...
...
@@ -24,7 +24,7 @@ import {
export
default
{
name
:
'
DetailsHeader
'
,
components
:
{
GlSprintf
,
TitleArea
,
MetadataItem
},
components
:
{
GlSprintf
,
GlButton
,
TitleArea
,
MetadataItem
},
mixins
:
[
timeagoMixin
],
props
:
{
image
:
{
...
...
@@ -36,6 +36,11 @@ export default {
required
:
false
,
default
:
false
,
},
disabled
:
{
type
:
Boolean
,
default
:
false
,
required
:
false
,
},
},
computed
:
{
visibilityIcon
()
{
...
...
@@ -65,6 +70,9 @@ export default {
[
UNFINISHED_STATUS
]:
{
text
:
CLEANUP_UNFINISHED_TEXT
,
tooltip
:
CLEANUP_UNFINISHED_TOOLTIP
},
}[
this
.
image
?.
expirationPolicyCleanupStatus
];
},
deleteButtonDisabled
()
{
return
this
.
disabled
||
!
this
.
image
.
canDelete
;
},
},
i18n
:
{
DETAILS_PAGE_TITLE
,
...
...
@@ -75,11 +83,13 @@ export default {
<
template
>
<title-area
:metadata-loading=
"metadataLoading"
>
<template
#title
>
<span
data-testid=
"title"
>
<gl-sprintf
:message=
"$options.i18n.DETAILS_PAGE_TITLE"
>
<template
#imageName
>
{{
image
.
name
}}
</
template
>
</gl-sprintf>
</span>
</template>
<
template
#metadata-tags-count
>
<metadata-item
icon=
"tag"
:text=
"tagCountText"
data-testid=
"tags-count"
/>
...
...
@@ -103,5 +113,15 @@ export default {
data-testid=
"updated-and-visibility"
/>
</
template
>
<
template
#right-actions
>
<gl-button
v-if=
"!metadataLoading"
variant=
"danger"
:disabled=
"deleteButtonDisabled"
@
click=
"$emit('delete')"
>
{{
__
(
'
Delete
'
)
}}
</gl-button>
</
template
>
</title-area>
</template>
app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue
0 → 100644
View file @
c0ec6ddc
<
script
>
import
{
GlAlert
,
GlLink
,
GlSprintf
}
from
'
@gitlab/ui
'
;
import
{
IMAGE_STATUS_MESSAGES
,
IMAGE_STATUS_TITLES
,
IMAGE_STATUS_ALERT_TYPE
,
PACKAGE_DELETE_HELP_PAGE_PATH
,
}
from
'
../../constants
'
;
export
default
{
components
:
{
GlAlert
,
GlSprintf
,
GlLink
,
},
props
:
{
status
:
{
type
:
String
,
required
:
true
,
},
},
computed
:
{
message
()
{
return
IMAGE_STATUS_MESSAGES
[
this
.
status
];
},
title
()
{
return
IMAGE_STATUS_TITLES
[
this
.
status
];
},
variant
()
{
return
IMAGE_STATUS_ALERT_TYPE
[
this
.
status
];
},
},
links
:
{
PACKAGE_DELETE_HELP_PAGE_PATH
,
},
};
</
script
>
<
template
>
<gl-alert
:title=
"title"
:variant=
"variant"
>
<span
data-testid=
"message"
>
<gl-sprintf
:message=
"message"
>
<template
#link
="
{ content }">
<gl-link
:href=
"$options.links.PACKAGE_DELETE_HELP_PAGE_PATH"
target=
"_blank"
>
{{
content
}}
</gl-link>
</
template
>
</gl-sprintf>
</span>
</gl-alert>
</template>
app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
View file @
c0ec6ddc
...
...
@@ -20,6 +20,11 @@ export default {
default
:
true
,
required
:
false
,
},
disabled
:
{
type
:
Boolean
,
default
:
false
,
required
:
false
,
},
},
i18n
:
{
REMOVE_TAGS_BUTTON_TITLE
,
...
...
@@ -37,6 +42,9 @@ export default {
showMultiDeleteButton
()
{
return
this
.
tags
.
some
((
tag
)
=>
tag
.
canDelete
)
&&
!
this
.
isMobile
;
},
multiDeleteButtonIsDisabled
()
{
return
!
this
.
hasSelectedItems
||
this
.
disabled
;
},
},
methods
:
{
updateSelectedItems
(
name
)
{
...
...
@@ -55,7 +63,7 @@ export default {
<gl-button
v-if=
"showMultiDeleteButton"
:disabled=
"
!hasSelectedItems
"
:disabled=
"
multiDeleteButtonIsDisabled
"
category=
"secondary"
variant=
"danger"
@
click=
"$emit('delete', selectedItems)"
...
...
@@ -70,6 +78,7 @@ export default {
:first=
"index === 0"
:selected=
"selectedItems[tag.name]"
:is-mobile=
"isMobile"
:disabled=
"disabled"
@
select=
"updateSelectedItems(tag.name)"
@
delete=
"$emit('delete',
{ [tag.name]: true })"
/>
...
...
app/assets/javascripts/registry/explorer/pages/details.vue
View file @
c0ec6ddc
...
...
@@ -12,6 +12,8 @@ import DetailsHeader from '../components/details_page/details_header.vue';
import
TagsList
from
'
../components/details_page/tags_list.vue
'
;
import
TagsLoader
from
'
../components/details_page/tags_loader.vue
'
;
import
EmptyState
from
'
../components/details_page/empty_state.vue
'
;
import
StatusAlert
from
'
../components/details_page/status_alert.vue
'
;
import
DeleteImage
from
'
../components/delete_image.vue
'
;
import
getContainerRepositoryDetailsQuery
from
'
../graphql/queries/get_container_repository_details.query.graphql
'
;
import
deleteContainerRepositoryTagsMutation
from
'
../graphql/mutations/delete_container_repository_tags.mutation.graphql
'
;
...
...
@@ -21,6 +23,7 @@ import {
ALERT_DANGER_TAG
,
ALERT_SUCCESS_TAGS
,
ALERT_DANGER_TAGS
,
ALERT_DANGER_IMAGE
,
GRAPHQL_PAGE_SIZE
,
FETCH_IMAGES_LIST_ERROR_MESSAGE
,
UNFINISHED_STATUS
,
...
...
@@ -38,6 +41,8 @@ export default {
TagsList
,
TagsLoader
,
EmptyState
,
StatusAlert
,
DeleteImage
,
},
directives
:
{
GlResizeObserver
:
GlResizeObserverDirective
,
...
...
@@ -71,6 +76,7 @@ export default {
mutationLoading
:
false
,
deleteAlertType
:
null
,
hidePartialCleanupWarning
:
false
,
deleteImageAlert
:
false
,
};
},
computed
:
{
...
...
@@ -105,6 +111,9 @@ export default {
hasNoTags
()
{
return
this
.
tags
.
length
===
0
;
},
pageActionsAreDisabled
()
{
return
Boolean
(
this
.
image
?.
status
);
},
},
methods
:
{
updateBreadcrumb
()
{
...
...
@@ -112,11 +121,19 @@ export default {
this
.
breadCrumbState
.
updateName
(
name
);
},
deleteTags
(
toBeDeleted
)
{
this
.
deleteImageAlert
=
false
;
this
.
itemsToBeDeleted
=
this
.
tags
.
filter
((
tag
)
=>
toBeDeleted
[
tag
.
name
]);
this
.
track
(
'
click_button
'
);
this
.
$refs
.
deleteModal
.
show
();
},
async
handleDelete
()
{
confirmDelete
()
{
if
(
this
.
deleteImageAlert
)
{
this
.
$refs
.
deleteImage
.
doDelete
();
}
else
{
this
.
handleDeleteTag
();
}
},
async
handleDeleteTag
()
{
this
.
track
(
'
confirm_delete
'
);
const
{
itemsToBeDeleted
}
=
this
;
this
.
itemsToBeDeleted
=
[];
...
...
@@ -184,6 +201,18 @@ export default {
feature_name
:
this
.
config
.
userCalloutId
,
});
},
deleteImage
()
{
this
.
deleteImageAlert
=
true
;
this
.
itemsToBeDeleted
=
[{
path
:
this
.
image
.
path
}];
this
.
$refs
.
deleteModal
.
show
();
},
deleteImageError
()
{
this
.
deleteAlertType
=
ALERT_DANGER_IMAGE
;
},
deleteImageIniit
()
{
this
.
itemsToBeDeleted
=
[];
this
.
mutationLoading
=
true
;
},
},
};
</
script
>
...
...
@@ -205,13 +234,25 @@ export default {
@
dismiss=
"dismissPartialCleanupWarning"
/>
<details-header
:image=
"image"
:metadata-loading=
"isLoading"
/>
<status-alert
v-if=
"image.status"
:status=
"image.status"
/>
<details-header
:image=
"image"
:metadata-loading=
"isLoading"
:disabled=
"pageActionsAreDisabled"
@
delete=
"deleteImage"
/>
<tags-loader
v-if=
"isLoading"
/>
<template
v-else
>
<empty-state
v-if=
"hasNoTags"
:no-containers-image=
"config.noContainersImage"
/>
<template
v-else
>
<tags-list
:tags=
"tags"
:is-mobile=
"isMobile"
@
delete=
"deleteTags"
/>
<tags-list
:tags=
"tags"
:is-mobile=
"isMobile"
:disabled=
"pageActionsAreDisabled"
@
delete=
"deleteTags"
/>
<div
class=
"gl-display-flex gl-justify-content-center"
>
<gl-keyset-pagination
v-if=
"showPagination"
...
...
@@ -225,10 +266,20 @@ export default {
</
template
>
</template>
<delete-image
:id=
"image.id"
ref=
"deleteImage"
use-update-fn
@
start=
"deleteImageIniit"
@
error=
"deleteImageError"
@
end=
"mutationLoading = false"
/>
<delete-modal
ref=
"deleteModal"
:items-to-be-deleted=
"itemsToBeDeleted"
@
confirmDelete=
"handleDelete"
:delete-image=
"deleteImageAlert"
@
confirmDelete=
"confirmDelete"
@
cancel=
"track('cancel_delete')"
/>
</template>
...
...
changelogs/unreleased/216761-add-image-repository-level-delete-functionality-to-the-image-repos.yml
0 → 100644
View file @
c0ec6ddc
---
title
:
Add delete functionality to the Image Repository detail view
merge_request
:
51980
author
:
type
:
added
spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js
View file @
c0ec6ddc
...
...
@@ -4,6 +4,8 @@ import component from '~/registry/explorer/components/details_page/delete_modal.
import
{
REMOVE_TAG_CONFIRMATION_TEXT
,
REMOVE_TAGS_CONFIRMATION_TEXT
,
DELETE_IMAGE_CONFIRMATION_TITLE
,
DELETE_IMAGE_CONFIRMATION_TEXT
,
}
from
'
~/registry/explorer/constants
'
;
import
{
GlModal
}
from
'
../../stubs
'
;
...
...
@@ -36,12 +38,12 @@ describe('Delete Modal', () => {
describe
(
'
events
'
,
()
=>
{
it
.
each
`
glEvent | localEvent
${
'
ok
'
}
|
${
'
confirmDelete
'
}
${
'
primary
'
}
|
${
'
confirmDelete
'
}
${
'
cancel
'
}
|
${
'
cancelDelete
'
}
`
(
'
GlModal $glEvent emits $localEvent
'
,
({
glEvent
,
localEvent
})
=>
{
mountComponent
();
findModal
().
vm
.
$emit
(
glEvent
);
expect
(
wrapper
.
emitted
(
localEvent
)).
to
BeTruthy
(
);
expect
(
wrapper
.
emitted
(
localEvent
)).
to
Equal
([[]]
);
});
});
...
...
@@ -53,14 +55,33 @@ describe('Delete Modal', () => {
});
});
describe
(
'
when we are deleting images
'
,
()
=>
{
it
(
'
has the correct title
'
,
()
=>
{
mountComponent
({
deleteImage
:
true
});
expect
(
wrapper
.
text
()).
toContain
(
DELETE_IMAGE_CONFIRMATION_TITLE
);
});
it
(
'
has the correct description
'
,
()
=>
{
mountComponent
({
deleteImage
:
true
});
expect
(
wrapper
.
text
()).
toContain
(
DELETE_IMAGE_CONFIRMATION_TEXT
);
});
});
describe
(
'
when we are deleting tags
'
,
()
=>
{
describe
(
'
itemsToBeDeleted contains one element
'
,
()
=>
{
beforeEach
(()
=>
{
mountComponent
({
itemsToBeDeleted
:
[{
path
:
'
foo
'
}]
});
});
it
(
`has the correct description`
,
()
=>
{
expect
(
findDescription
().
text
()).
toBe
(
REMOVE_TAG_CONFIRMATION_TEXT
.
replace
(
'
%{item}
'
,
'
foo
'
));
expect
(
findDescription
().
text
()).
toBe
(
REMOVE_TAG_CONFIRMATION_TEXT
.
replace
(
'
%{item}
'
,
'
foo
'
),
);
});
it
(
'
has the correct action
'
,
()
=>
{
it
(
'
has the correct title
'
,
()
=>
{
expect
(
wrapper
.
text
()).
toContain
(
'
Remove tag
'
);
});
});
...
...
@@ -69,11 +90,16 @@ describe('Delete Modal', () => {
beforeEach
(()
=>
{
mountComponent
({
itemsToBeDeleted
:
[{
path
:
'
foo
'
},
{
path
:
'
bar
'
}]
});
});
it
(
`has the correct description`
,
()
=>
{
expect
(
findDescription
().
text
()).
toBe
(
REMOVE_TAGS_CONFIRMATION_TEXT
.
replace
(
'
%{item}
'
,
'
2
'
));
expect
(
findDescription
().
text
()).
toBe
(
REMOVE_TAGS_CONFIRMATION_TEXT
.
replace
(
'
%{item}
'
,
'
2
'
),
);
});
it
(
'
has the correct action
'
,
()
=>
{
it
(
'
has the correct title
'
,
()
=>
{
expect
(
wrapper
.
text
()).
toContain
(
'
Remove tags
'
);
});
});
});
});
spec/frontend/registry/explorer/components/details_page/details_header_spec.js
View file @
c0ec6ddc
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlSprintf
}
from
'
@gitlab/ui
'
;
import
{
GlSprintf
,
GlButton
}
from
'
@gitlab/ui
'
;
import
{
useFakeDate
}
from
'
helpers/fake_date
'
;
import
TitleArea
from
'
~/vue_shared/components/registry/title_area.vue
'
;
import
component
from
'
~/registry/explorer/components/details_page/details_header.vue
'
;
...
...
@@ -23,6 +23,7 @@ describe('Details Header', () => {
name
:
'
foo
'
,
updatedAt
:
'
2020-11-03T13:29:21Z
'
,
tagsCount
:
10
,
canDelete
:
true
,
project
:
{
visibility
:
'
public
'
,
containerExpirationPolicy
:
{
...
...
@@ -36,8 +37,10 @@ describe('Details Header', () => {
const
findByTestId
=
(
testId
)
=>
wrapper
.
find
(
`[data-testid="
${
testId
}
"]`
);
const
findLastUpdatedAndVisibility
=
()
=>
findByTestId
(
'
updated-and-visibility
'
);
const
findTitle
=
()
=>
findByTestId
(
'
title
'
);
const
findTagsCount
=
()
=>
findByTestId
(
'
tags-count
'
);
const
findCleanup
=
()
=>
findByTestId
(
'
cleanup
'
);
const
findDeleteButton
=
()
=>
wrapper
.
find
(
GlButton
);
const
waitForMetadataItems
=
async
()
=>
{
// Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
...
...
@@ -45,11 +48,9 @@ describe('Details Header', () => {
await
wrapper
.
vm
.
$nextTick
();
};
const
mountComponent
=
(
image
=
defaultImage
)
=>
{
const
mountComponent
=
(
propsData
=
{
image
:
defaultImage
}
)
=>
{
wrapper
=
shallowMount
(
component
,
{
propsData
:
{
image
,
},
propsData
,
stubs
:
{
GlSprintf
,
TitleArea
,
...
...
@@ -63,13 +64,65 @@ describe('Details Header', () => {
});
it
(
'
has the correct title
'
,
()
=>
{
mountComponent
({
...
defaultImage
,
name
:
''
});
expect
(
wrapper
.
text
()).
toMatchInterpolatedText
(
DETAILS_PAGE_TITLE
);
mountComponent
({
image
:
{
...
defaultImage
,
name
:
''
}
});
expect
(
findTitle
()
.
text
()).
toMatchInterpolatedText
(
DETAILS_PAGE_TITLE
);
});
it
(
'
shows imageName in the title
'
,
()
=>
{
mountComponent
();
expect
(
wrapper
.
text
()).
toContain
(
'
foo
'
);
expect
(
findTitle
().
text
()).
toContain
(
'
foo
'
);
});
describe
(
'
delete button
'
,
()
=>
{
it
(
'
exists
'
,
()
=>
{
mountComponent
();
expect
(
findDeleteButton
().
exists
()).
toBe
(
true
);
});
it
(
'
is hidden while loading
'
,
()
=>
{
mountComponent
({
image
:
defaultImage
,
metadataLoading
:
true
});
expect
(
findDeleteButton
().
exists
()).
toBe
(
false
);
});
it
(
'
has the correct text
'
,
()
=>
{
mountComponent
();
expect
(
findDeleteButton
().
text
()).
toBe
(
'
Delete
'
);
});
it
(
'
has the correct props
'
,
()
=>
{
mountComponent
();
expect
(
findDeleteButton
().
props
()).
toMatchObject
({
variant
:
'
danger
'
,
disabled
:
false
,
});
});
it
(
'
emits the correct event
'
,
()
=>
{
mountComponent
();
findDeleteButton
().
vm
.
$emit
(
'
click
'
);
expect
(
wrapper
.
emitted
(
'
delete
'
)).
toEqual
([[]]);
});
it
.
each
`
canDelete | disabled | isDisabled
${
true
}
|
${
false
}
|
${
false
}
${
true
}
|
${
true
}
|
${
true
}
${
false
}
|
${
false
}
|
${
true
}
${
false
}
|
${
true
}
|
${
true
}
`
(
'
when canDelete is $canDelete and disabled is $disabled is $isDisabled that the button is disabled
'
,
({
canDelete
,
disabled
,
isDisabled
})
=>
{
mountComponent
({
image
:
{
...
defaultImage
,
canDelete
},
disabled
});
expect
(
findDeleteButton
().
props
(
'
disabled
'
)).
toBe
(
isDisabled
);
},
);
});
describe
(
'
metadata items
'
,
()
=>
{
...
...
@@ -82,7 +135,7 @@ describe('Details Header', () => {
});
it
(
'
when there is one tag has the correct text
'
,
async
()
=>
{
mountComponent
({
...
defaultImage
,
tagsCount
:
1
});
mountComponent
({
image
:
{
...
defaultImage
,
tagsCount
:
1
}
});
await
waitForMetadataItems
();
expect
(
findTagsCount
().
props
(
'
text
'
)).
toBe
(
'
1 tag
'
);
...
...
@@ -124,11 +177,13 @@ describe('Details Header', () => {
'
when the status is $status the text is $text and the tooltip is $tooltip
'
,
async
({
status
,
text
,
tooltip
})
=>
{
mountComponent
({
image
:
{
...
defaultImage
,
expirationPolicyCleanupStatus
:
status
,
project
:
{
containerExpirationPolicy
:
{
enabled
:
true
,
nextRunAt
:
'
2021-01-03T14:29:21Z
'
},
},
},
});
await
waitForMetadataItems
();
...
...
@@ -156,7 +211,7 @@ describe('Details Header', () => {
expect
(
findLastUpdatedAndVisibility
().
props
(
'
icon
'
)).
toBe
(
'
eye
'
);
});
it
(
'
shows an eye slashed when the project is not public
'
,
async
()
=>
{
mountComponent
({
...
defaultImage
,
project
:
{
visibility
:
'
private
'
}
});
mountComponent
({
image
:
{
...
defaultImage
,
project
:
{
visibility
:
'
private
'
}
}
});
await
waitForMetadataItems
();
expect
(
findLastUpdatedAndVisibility
().
props
(
'
icon
'
)).
toBe
(
'
eye-slash
'
);
...
...
spec/frontend/registry/explorer/components/details_page/status_alert_spec.js
0 → 100644
View file @
c0ec6ddc
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlLink
,
GlSprintf
,
GlAlert
}
from
'
@gitlab/ui
'
;
import
component
from
'
~/registry/explorer/components/details_page/status_alert.vue
'
;
import
{
DELETE_SCHEDULED
,
DELETE_FAILED
,
PACKAGE_DELETE_HELP_PAGE_PATH
,
SCHEDULED_FOR_DELETION_STATUS_TITLE
,
SCHEDULED_FOR_DELETION_STATUS_MESSAGE
,
FAILED_DELETION_STATUS_TITLE
,
FAILED_DELETION_STATUS_MESSAGE
,
}
from
'
~/registry/explorer/constants
'
;
describe
(
'
Status Alert
'
,
()
=>
{
let
wrapper
;
const
findLink
=
()
=>
wrapper
.
find
(
GlLink
);
const
findAlert
=
()
=>
wrapper
.
find
(
GlAlert
);
const
findMessage
=
()
=>
wrapper
.
find
(
'
[data-testid="message"]
'
);
const
mountComponent
=
(
propsData
)
=>
{
wrapper
=
shallowMount
(
component
,
{
propsData
,
stubs
:
{
GlSprintf
,
},
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
it
.
each
`
status | title | variant | message | link
${
DELETE_SCHEDULED
}
|
${
SCHEDULED_FOR_DELETION_STATUS_TITLE
}
|
${
'
info
'
}
|
${
SCHEDULED_FOR_DELETION_STATUS_MESSAGE
}
|
${
PACKAGE_DELETE_HELP_PAGE_PATH
}
${
DELETE_FAILED
}
|
${
FAILED_DELETION_STATUS_TITLE
}
|
${
'
warning
'
}
|
${
FAILED_DELETION_STATUS_MESSAGE
}
|
${
''
}
`
(
`when the status is $status, title is $title, variant is $variant, message is $message and the link is $link`
,
({
status
,
title
,
variant
,
message
,
link
})
=>
{
mountComponent
({
status
});
expect
(
findMessage
().
text
()).
toMatchInterpolatedText
(
message
);
expect
(
findAlert
().
props
()).
toMatchObject
({
title
,
variant
,
});
if
(
link
)
{
expect
(
findLink
().
attributes
()).
toMatchObject
({
target
:
'
_blank
'
,
href
:
link
,
});
}
},
);
});
spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
View file @
c0ec6ddc
...
...
@@ -70,18 +70,25 @@ describe('Tags List', () => {
});
});
it
(
'
is disabled when no item is selected
'
,
()
=>
{
mountComponent
();
expect
(
findDeleteButton
().
attributes
(
'
disabled
'
)).
toBe
(
'
true
'
);
});
it
.
each
`
disabled | doSelect | buttonDisabled
${
true
}
|
${
false
}
|
${
'
true
'
}
${
true
}
|
${
true
}
|
${
'
true
'
}
${
false
}
|
${
false
}
|
${
'
true
'
}
${
false
}
|
${
true
}
|
${
undefined
}
`
(
'
is $buttonDisabled that the button is disabled when the component disabled state is $disabled and is $doSelect that the user selected a tag
'
,
async
({
disabled
,
buttonDisabled
,
doSelect
})
=>
{
mountComponent
({
tags
,
disabled
,
isMobile
:
false
});
it
(
'
is enabled when at least one item is selected
'
,
async
()
=>
{
mountComponent
();
if
(
doSelect
)
{
findTagsListRow
().
at
(
0
).
vm
.
$emit
(
'
select
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
findDeleteButton
().
attributes
(
'
disabled
'
)).
toBe
(
undefined
);
});
}
expect
(
findDeleteButton
().
attributes
(
'
disabled
'
)).
toBe
(
buttonDisabled
);
},
);
it
(
'
click event emits a deleted event with selected items
'
,
()
=>
{
mountComponent
();
...
...
@@ -100,12 +107,13 @@ describe('Tags List', () => {
});
it
(
'
the correct props are bound to it
'
,
()
=>
{
mountComponent
();
mountComponent
(
{
tags
,
disabled
:
true
}
);
const
rows
=
findTagsListRow
();
expect
(
rows
.
at
(
0
).
attributes
()).
toMatchObject
({
first
:
'
true
'
,
disabled
:
'
true
'
,
});
});
...
...
spec/frontend/registry/explorer/pages/details_spec.js
View file @
c0ec6ddc
...
...
@@ -12,11 +12,17 @@ import DetailsHeader from '~/registry/explorer/components/details_page/details_h
import
TagsLoader
from
'
~/registry/explorer/components/details_page/tags_loader.vue
'
;
import
TagsList
from
'
~/registry/explorer/components/details_page/tags_list.vue
'
;
import
EmptyTagsState
from
'
~/registry/explorer/components/details_page/empty_state.vue
'
;
import
StatusAlert
from
'
~/registry/explorer/components/details_page/status_alert.vue
'
;
import
DeleteImage
from
'
~/registry/explorer/components/delete_image.vue
'
;
import
getContainerRepositoryDetailsQuery
from
'
~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql
'
;
import
deleteContainerRepositoryTagsMutation
from
'
~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql
'
;
import
{
UNFINISHED_STATUS
}
from
'
~/registry/explorer/constants/index
'
;
import
{
UNFINISHED_STATUS
,
DELETE_SCHEDULED
,
ALERT_DANGER_IMAGE
,
}
from
'
~/registry/explorer/constants
'
;
import
{
graphQLImageDetailsMock
,
...
...
@@ -43,6 +49,8 @@ describe('Details Page', () => {
const
findDetailsHeader
=
()
=>
wrapper
.
find
(
DetailsHeader
);
const
findEmptyState
=
()
=>
wrapper
.
find
(
EmptyTagsState
);
const
findPartialCleanupAlert
=
()
=>
wrapper
.
find
(
PartialCleanupAlert
);
const
findStatusAlert
=
()
=>
wrapper
.
find
(
StatusAlert
);
const
findDeleteImage
=
()
=>
wrapper
.
find
(
DeleteImage
);
const
routeId
=
1
;
...
...
@@ -88,6 +96,7 @@ describe('Details Page', () => {
apolloProvider
,
stubs
:
{
DeleteModal
,
DeleteImage
,
},
mocks
:
{
$route
:
{
...
...
@@ -507,4 +516,83 @@ describe('Details Page', () => {
expect
(
breadCrumbState
.
updateName
).
toHaveBeenCalledWith
(
containerRepositoryMock
.
name
);
});
});
describe
(
'
when the image has a status different from null
'
,
()
=>
{
const
resolver
=
jest
.
fn
()
.
mockResolvedValue
(
graphQLImageDetailsMock
({
status
:
DELETE_SCHEDULED
}));
it
(
'
disables all the actions
'
,
async
()
=>
{
mountComponent
({
resolver
});
await
waitForApolloRequestRender
();
expect
(
findDetailsHeader
().
props
(
'
disabled
'
)).
toBe
(
true
);
expect
(
findTagsList
().
props
(
'
disabled
'
)).
toBe
(
true
);
});
it
(
'
shows a status alert
'
,
async
()
=>
{
mountComponent
({
resolver
});
await
waitForApolloRequestRender
();
expect
(
findStatusAlert
().
exists
()).
toBe
(
true
);
expect
(
findStatusAlert
().
props
()).
toMatchObject
({
status
:
DELETE_SCHEDULED
,
});
});
});
describe
(
'
delete the image
'
,
()
=>
{
const
mountComponentAndDeleteImage
=
async
()
=>
{
mountComponent
();
await
waitForApolloRequestRender
();
findDetailsHeader
().
vm
.
$emit
(
'
delete
'
);
await
wrapper
.
vm
.
$nextTick
();
};
it
(
'
on delete event it deletes the image
'
,
async
()
=>
{
await
mountComponentAndDeleteImage
();
findDeleteModal
().
vm
.
$emit
(
'
confirmDelete
'
);
expect
(
findDeleteImage
().
emitted
(
'
start
'
)).
toEqual
([[]]);
});
it
(
'
binds the correct props to the modal
'
,
async
()
=>
{
await
mountComponentAndDeleteImage
();
expect
(
findDeleteModal
().
props
()).
toMatchObject
({
itemsToBeDeleted
:
[{
path
:
'
gitlab-org/gitlab-test/rails-12009
'
}],
deleteImage
:
true
,
});
});
it
(
'
binds correctly to delete-image start and end events
'
,
async
()
=>
{
mountComponent
();
findDeleteImage
().
vm
.
$emit
(
'
start
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
findTagsLoader
().
exists
()).
toBe
(
true
);
findDeleteImage
().
vm
.
$emit
(
'
end
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
findTagsLoader
().
exists
()).
toBe
(
false
);
});
it
(
'
binds correctly to delete-image error event
'
,
async
()
=>
{
mountComponent
();
findDeleteImage
().
vm
.
$emit
(
'
error
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
findDeleteAlert
().
props
(
'
deleteAlertType
'
)).
toBe
(
ALERT_DANGER_IMAGE
);
});
});
});
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