Commit 2f47b6d8 authored by 🤖 GitLab Bot 🤖's avatar 🤖 GitLab Bot 🤖

Add latest changes from gitlab-org/[email protected]

parent d15cc268
......@@ -112,9 +112,10 @@
- "Gemfile{,.lock}"
- "Rakefile"
- "config.ru"
# List explicitly all the app/ dirs that aren't backend (i.e. all except app/assets).
# List explicitly all the app/ dirs that are backend (i.e. all except app/assets).
- "{,ee/}{app/channels,app/controllers,app/finders,app/graphql,app/helpers,app/mailers,app/models,app/policies,app/presenters,app/serializers,app/services,app/uploaders,app/validators,app/views,app/workers}/**/*"
- "{,ee/}{bin,cable,config,db,lib}/**/*"
- "{,ee/}spec/**/*.rb"
.db-patterns: &db-patterns
- "{,ee/}{db}/**/*"
......
<script>
import { GlButton } from '@gitlab/ui';
import TagsListRow from './tags_list_row.vue';
import { REMOVE_TAGS_BUTTON_TITLE, TAGS_LIST_TITLE } from '../../constants/index';
export default {
components: {
GlButton,
TagsListRow,
},
props: {
tags: {
type: Array,
required: false,
default: () => [],
},
isDesktop: {
type: Boolean,
default: false,
required: false,
},
},
i18n: {
REMOVE_TAGS_BUTTON_TITLE,
TAGS_LIST_TITLE,
},
data() {
return {
selectedItems: {},
};
},
computed: {
hasSelectedItems() {
return this.tags.some(tag => this.selectedItems[tag.name]);
},
},
methods: {
updateSelectedItems(name) {
this.$set(this.selectedItems, name, !this.selectedItems[name]);
},
},
};
</script>
<template>
<div>
<div class="gl-display-flex gl-justify-content-space-between gl-mb-3">
<h5 data-testid="list-title">
{{ $options.i18n.TAGS_LIST_TITLE }}
</h5>
<gl-button
v-if="isDesktop"
:disabled="!hasSelectedItems"
category="secondary"
variant="danger"
@click="$emit('delete', selectedItems)"
>
{{ $options.i18n.REMOVE_TAGS_BUTTON_TITLE }}
</gl-button>
</div>
<tags-list-row
v-for="(tag, index) in tags"
:key="tag.path"
:tag="tag"
:index="index"
:selected="selectedItems[tag.name]"
:is-desktop="isDesktop"
@select="updateSelectedItems(tag.name)"
@delete="$emit('delete', { [tag.name]: true })"
/>
</div>
</template>
<script>
import { GlFormCheckbox, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DeleteButton from '../delete_button.vue';
import ListItem from '../list_item.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
SHORT_REVISION_LABEL,
CREATED_AT_LABEL,
REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,
} from '../../constants/index';
export default {
components: {
GlSprintf,
GlFormCheckbox,
DeleteButton,
ListItem,
ClipboardButton,
TimeAgoTooltip,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
tag: {
type: Object,
required: true,
},
index: {
type: Number,
required: true,
},
selected: {
type: Boolean,
default: false,
required: false,
},
isDesktop: {
type: Boolean,
default: false,
required: false,
},
},
i18n: {
REMOVE_TAG_BUTTON_TITLE,
SHORT_REVISION_LABEL,
CREATED_AT_LABEL,
REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,
},
computed: {
formattedSize() {
return this.tag.total_size ? numberToHumanSize(this.tag.total_size) : '';
},
layers() {
return this.tag.layers ? n__('%d layer', '%d layers', this.tag.layers) : '';
},
mobileClasses() {
return this.isDesktop ? '' : 'mw-s';
},
},
};
</script>
<template>
<list-item :index="index" :selected="selected">
<template #left-action>
<gl-form-checkbox class="gl-m-0" :checked="selected" @change="$emit('select')" />
</template>
<template #left-primary>
<div class="gl-display-flex gl-align-items-center">
<div
v-gl-tooltip="{ title: tag.name }"
data-testid="name"
class="gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap"
:class="mobileClasses"
>
{{ tag.name }}
</div>
<clipboard-button
v-if="tag.location"
:title="tag.location"
:text="tag.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
</div>
</template>
<template #left-secondary>
<span data-testid="size">
{{ formattedSize }}
<template v-if="formattedSize && layers"
>&middot;</template
>
{{ layers }}
</span>
</template>
<template #right-primary>
<span data-testid="time">
<gl-sprintf :message="$options.i18n.CREATED_AT_LABEL">
<template #timeInfo>
<time-ago-tooltip :time="tag.created_at" />
</template>
</gl-sprintf>
</span>
</template>
<template #right-secondary>
<span data-testid="short-revision">
<gl-sprintf :message="$options.i18n.SHORT_REVISION_LABEL">
<template #imageId>{{ tag.short_revision }}</template>
</gl-sprintf>
</span>
</template>
<template #right-action>
<delete-button
:disabled="!tag.destroy_path"
:title="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:tooltip-title="$options.i18n.REMOVE_TAG_BUTTON_DISABLE_TOOLTIP"
:tooltip-disabled="Boolean(tag.destroy_path)"
data-testid="single-delete-button"
@delete="$emit('delete')"
/>
</template>
</list-item>
</template>
<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">
&middot;
</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>
......@@ -12,12 +12,19 @@ export default {
default: false,
required: false,
},
selected: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
optionalClasses() {
return {
'gl-border-t-solid gl-border-t-1': this.index === 0,
'disabled-content': this.disabled,
'gl-border-gray-200': !this.selected,
'gl-bg-blue-50 gl-border-blue-200': this.selected,
};
},
},
......@@ -26,22 +33,36 @@ export default {
<template>
<div
:class="[
'gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-py-4',
optionalClasses,
]"
class="gl-display-flex gl-align-items-center gl-border-b-solid gl-border-b-1 gl-py-4 gl-px-2"
:class="optionalClasses"
>
<div class="gl-display-flex gl-flex-direction-column">
<div class="gl-display-flex gl-align-items-center">
<slot name="left-primary"></slot>
<div v-if="$slots['left-action']" class="gl-mr-5 gl-display-none gl-display-sm-block">
<slot name="left-action"></slot>
</div>
<div class="gl-display-flex gl-flex-direction-column gl-flex-fill-1">
<div
class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-text-black-normal gl-font-weight-bold"
>
<div>
<slot name="left-primary"></slot>
</div>
<div>
<slot name="right-primary"></slot>
</div>
</div>
<div class="gl-font-sm gl-text-gray-500">
<slot name="left-secondary"></slot>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-font-sm gl-text-gray-500"
>
<div>
<slot name="left-secondary"></slot>
</div>
<div>
<slot name="right-secondary"></slot>
</div>
</div>
</div>
<div>
<slot name="right"></slot>
<div v-if="$slots['right-action']" class="gl-ml-5 gl-display-none gl-display-sm-block">
<slot name="right-action"></slot>
</div>
</div>
</template>
......@@ -106,9 +106,8 @@ export default {
</gl-sprintf>
</span>
</template>
<template #right>
<template #right-action>
<delete-button
class="gl-display-none d-sm-block"
:title="$options.i18n.REMOVE_REPOSITORY_LABEL"
:disabled="disabledDelete"
:tooltip-disabled="Boolean(item.destroy_path)"
......
......@@ -14,12 +14,13 @@ export const DELETE_TAGS_ERROR_MESSAGE = s__(
export const DELETE_TAGS_SUCCESS_MESSAGE = s__(
'ContainerRegistry|Tags successfully marked for deletion.',
);
export const LIST_LABEL_TAG = s__('ContainerRegistry|Tag');
export const LIST_LABEL_IMAGE_ID = s__('ContainerRegistry|Image ID');
export const LIST_LABEL_SIZE = s__('ContainerRegistry|Compressed Size');
export const LIST_LABEL_LAST_UPDATED = s__('ContainerRegistry|Last Updated');
export const TAGS_LIST_TITLE = s__('ContainerRegistry|Image tags');
export const SHORT_REVISION_LABEL = s__('ContainerRegistry|Image ID: %{imageId}');
export const CREATED_AT_LABEL = s__('ContainerRegistry|Published %{timeInfo}');
export const REMOVE_TAG_BUTTON_TITLE = s__('ContainerRegistry|Remove tag');
export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Remove selected tags');
export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Delete selected');
export const REMOVE_TAG_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item}. Are you sure?`,
);
......@@ -36,17 +37,15 @@ export const ADMIN_GARBAGE_COLLECTION_TIP = s__(
'ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.',
);
export const REMOVE_TAG_BUTTON_DISABLE_TOOLTIP = s__(
'ContainerRegistry|Missing or insufficient permission, delete button disabled',
);
// Parameters
export const DEFAULT_PAGE = 1;
export const DEFAULT_PAGE_SIZE = 10;
export const GROUP_PAGE_TYPE = 'groups';
export const LIST_KEY_TAG = 'name';
export const LIST_KEY_IMAGE_ID = 'short_revision';
export const LIST_KEY_SIZE = 'total_size';
export const LIST_KEY_LAST_UPDATED = 'created_at';
export const LIST_KEY_ACTIONS = 'actions';
export const LIST_KEY_CHECKBOX = 'checkbox';
export const ALERT_SUCCESS_TAG = 'success_tag';
export const ALERT_DANGER_TAG = 'danger_tag';
export const ALERT_SUCCESS_TAGS = 'success_tags';
......
......@@ -6,7 +6,7 @@ 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 TagsList from '../components/details_page/tags_list.vue';
import TagsLoader from '../components/details_page/tags_loader.vue';
import EmptyTagsState from '../components/details_page/empty_tags_state.vue';
......@@ -24,7 +24,7 @@ export default {
DetailsHeader,
GlPagination,
DeleteModal,