Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-02-13 15:08:49 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-13 15:08:49 +0300
commit1308dc5eb484ab0f8064989fc551ebdb4b1a7976 (patch)
tree614a93d9bf8df34ecfc25c02648329987fb21dde /app/assets
parentf0707f413ce49b5712fca236b950acbec029be1e (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/code_navigation/store/actions.js3
-rw-r--r--app/assets/javascripts/diffs/components/app.vue2
-rw-r--r--app/assets/javascripts/flash.js2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue12
-rw-r--r--app/assets/javascripts/ide/components/file_row_extra.vue15
-rw-r--r--app/assets/javascripts/ide/stores/actions.js9
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js4
-rw-r--r--app/assets/javascripts/registry/explorer/components/group_empty_state.vue39
-rw-r--r--app/assets/javascripts/registry/explorer/components/project_empty_state.vue113
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue329
-rw-r--r--app/assets/javascripts/registry/explorer/pages/list.vue211
-rw-r--r--app/assets/javascripts/registry/explorer/stores/actions.js4
-rw-r--r--app/assets/javascripts/registry/explorer/stores/mutations.js1
13 files changed, 703 insertions, 41 deletions
diff --git a/app/assets/javascripts/code_navigation/store/actions.js b/app/assets/javascripts/code_navigation/store/actions.js
index cb6d30c775d..2c52074e362 100644
--- a/app/assets/javascripts/code_navigation/store/actions.js
+++ b/app/assets/javascripts/code_navigation/store/actions.js
@@ -1,6 +1,4 @@
import api from '~/api';
-import { __ } from '~/locale';
-import createFlash from '~/flash';
import * as types from './mutation_types';
import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils';
@@ -10,7 +8,6 @@ export default {
},
requestDataError({ commit }) {
commit(types.REQUEST_DATA_ERROR);
- createFlash(__('An error occurred loading code navigation'));
},
fetchData({ commit, dispatch, state }) {
commit(types.REQUEST_DATA);
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 878b54f7d53..f9d3d31e152 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -354,7 +354,7 @@ export default {
<template>
<div v-show="shouldShow">
- <div v-if="isLoading" class="loading"><gl-loading-icon /></div>
+ <div v-if="isLoading" class="loading"><gl-loading-icon size="lg" /></div>
<div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane">
<compare-versions
:merge-request-diffs="mergeRequestDiffs"
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 347f7b450ff..4d62ec6e385 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -11,7 +11,7 @@ const FLASH_TYPES = {
const hideFlash = (flashEl, fadeTransition = true) => {
if (fadeTransition) {
Object.assign(flashEl.style, {
- transition: 'opacity .3s',
+ transition: 'opacity 0.15s',
opacity: '0',
});
}
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 491814bb408..9777f365557 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -6,7 +6,6 @@ import CommitMessageField from './message_field.vue';
import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -15,7 +14,6 @@ export default {
CommitMessageField,
SuccessMessage,
},
- mixins: [glFeatureFlagsMixin()],
data() {
return {
isCompact: true,
@@ -29,13 +27,9 @@ export default {
...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']),
overviewText() {
return sprintf(
- this.glFeatures.stageAllByDefault
- ? __(
- '<strong>%{stagedFilesLength} staged</strong> and <strong>%{changedFilesLength} unstaged</strong> changes',
- )
- : __(
- '<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
- ),
+ __(
+ '<strong>%{stagedFilesLength} staged</strong> and <strong>%{changedFilesLength} unstaged</strong> changes',
+ ),
{
stagedFilesLength: this.stagedFiles.length,
changedFilesLength: this.changedFiles.length,
diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue
index 33098eb1af0..3ef7d863bd5 100644
--- a/app/assets/javascripts/ide/components/file_row_extra.vue
+++ b/app/assets/javascripts/ide/components/file_row_extra.vue
@@ -6,7 +6,6 @@ import Icon from '~/vue_shared/components/icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
import NewDropdown from './new_dropdown/index.vue';
import MrFileIcon from './mr_file_icon.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'FileRowExtra',
@@ -19,7 +18,6 @@ export default {
ChangedFileIcon,
MrFileIcon,
},
- mixins: [glFeatureFlagsMixin()],
props: {
file: {
type: Object,
@@ -57,15 +55,10 @@ export default {
return n__('%d staged change', '%d staged changes', this.folderStagedCount);
}
- return sprintf(
- this.glFeatures.stageAllByDefault
- ? __('%{staged} staged and %{unstaged} unstaged changes')
- : __('%{unstaged} unstaged and %{staged} staged changes'),
- {
- unstaged: this.folderUnstagedCount,
- staged: this.folderStagedCount,
- },
- );
+ return sprintf(__('%{staged} staged and %{unstaged} unstaged changes'), {
+ unstaged: this.folderUnstagedCount,
+ staged: this.folderStagedCount,
+ });
},
showTreeChangesCount() {
return this.isTree && this.changesCount > 0 && !this.file.opened;
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 9bc008c0dd5..ddc0925efb9 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -79,10 +79,7 @@ export const createTempEntry = (
if (type === 'blob') {
commit(types.TOGGLE_FILE_OPEN, file.path);
-
- if (gon.features?.stageAllByDefault)
- commit(types.STAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) });
- else commit(types.ADD_FILE_TO_CHANGED, file.path);
+ commit(types.STAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) });
dispatch('setFileActive', file.path);
dispatch('triggerFilesChange');
@@ -250,9 +247,7 @@ export const renameEntry = ({ dispatch, commit, state, getters }, { path, name,
if (isReset) {
commit(types.REMOVE_FILE_FROM_STAGED_AND_CHANGED, newEntry);
} else if (!isInChanges) {
- if (gon.features?.stageAllByDefault)
- commit(types.STAGE_CHANGE, { path: newPath, diffInfo: getters.getDiffInfo(newPath) });
- else commit(types.ADD_FILE_TO_CHANGED, newPath);
+ commit(types.STAGE_CHANGE, { path: newPath, diffInfo: getters.getDiffInfo(newPath) });
}
if (!newEntry.tempFile) {
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 052120059c4..da7d4a44bde 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -158,9 +158,7 @@ export const changeFileContent = ({ commit, state, getters }, { path, content })
const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path);
if (file.changed && indexOfChangedFile === -1) {
- if (gon.features?.stageAllByDefault)
- commit(types.STAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) });
- else commit(types.ADD_FILE_TO_CHANGED, path);
+ commit(types.STAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) });
} else if (!file.changed && !file.tempFile && indexOfChangedFile !== -1) {
commit(types.REMOVE_FILE_FROM_CHANGED, path);
}
diff --git a/app/assets/javascripts/registry/explorer/components/group_empty_state.vue b/app/assets/javascripts/registry/explorer/components/group_empty_state.vue
new file mode 100644
index 00000000000..a29a9bd23c3
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/group_empty_state.vue
@@ -0,0 +1,39 @@
+<script>
+import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
+import { mapState } from 'vuex';
+
+export default {
+ name: 'GroupEmptyState',
+ components: {
+ GlEmptyState,
+ GlSprintf,
+ GlLink,
+ },
+ computed: {
+ ...mapState(['config']),
+ },
+};
+</script>
+<template>
+ <gl-empty-state
+ :title="s__('ContainerRegistry|There are no container images available in this group')"
+ :svg-path="config.noContainersImage"
+ class="container-message"
+ >
+ <template #description>
+ <p class="js-no-container-images-text">
+ <gl-sprintf
+ :message="
+ s__(
+ `ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here. %{docLinkStart}More Information%{docLinkEnd}`,
+ )
+ "
+ >
+ <template #docLink="{content}">
+ <gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/project_empty_state.vue b/app/assets/javascripts/registry/explorer/components/project_empty_state.vue
new file mode 100644
index 00000000000..53853b4b9fb
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/project_empty_state.vue
@@ -0,0 +1,113 @@
+<script>
+import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { mapState } from 'vuex';
+
+export default {
+ name: 'ProjectEmptyState',
+ components: {
+ ClipboardButton,
+ GlEmptyState,
+ GlSprintf,
+ GlLink,
+ },
+ computed: {
+ ...mapState(['config']),
+ dockerBuildCommand() {
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+ return `docker build -t ${this.config.repositoryUrl} .`;
+ },
+ dockerPushCommand() {
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+ return `docker push ${this.config.repositoryUrl}`;
+ },
+ dockerLoginCommand() {
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+ return `docker login ${this.config.registryHostUrlWithPort}`;
+ },
+ },
+};
+</script>
+<template>
+ <gl-empty-state
+ :title="s__('ContainerRegistry|There are no container images stored for this project')"
+ :svg-path="config.noContainersImage"
+ class="container-message"
+ >
+ <template #description>
+ <p class="js-no-container-images-text">
+ <gl-sprintf
+ :message="
+ s__(`ContainerRegistry|With the Container Registry, every project can have its own space to
+ store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`)
+ "
+ >
+ <template #docLink="{content}">
+ <gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
+ <p class="js-not-logged-in-to-registry-text">
+ <gl-sprintf
+ :message="
+ s__(`ContainerRegistry|If you are not already logged in, you need to authenticate to
+ the Container Registry by using your GitLab username and password. If you have
+ %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a
+ %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd}
+ instead of a password.`)
+ "
+ >
+ <template #twofaDocLink="{content}">
+ <gl-link :href="config.twoFactorAuthHelpLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ <template #personalAccessTokensDocLink="{content}">
+ <gl-link :href="config.personalAccessTokensHelpLink" target="_blank">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <div class="input-group append-bottom-10">
+ <input :value="dockerLoginCommand" type="text" class="form-control monospace" readonly />
+ <span class="input-group-append">
+ <clipboard-button
+ :text="dockerLoginCommand"
+ :title="s__('ContainerRegistry|Copy login command')"
+ class="input-group-text"
+ />
+ </span>
+ </div>
+ <p></p>
+ <p>
+ {{
+ s__(
+ 'ContainerRegistry|You can add an image to this registry with the following commands:',
+ )
+ }}
+ </p>
+
+ <div class="input-group append-bottom-10">
+ <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly />
+ <span class="input-group-append">
+ <clipboard-button
+ :text="dockerBuildCommand"
+ :title="s__('ContainerRegistry|Copy build command')"
+ class="input-group-text"
+ />
+ </span>
+ </div>
+
+ <div class="input-group">
+ <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly />
+ <span class="input-group-append">
+ <clipboard-button
+ :text="dockerPushCommand"
+ :title="s__('ContainerRegistry|Copy push command')"
+ class="input-group-text"
+ />
+ </span>
+ </div>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index 6d32ba41eae..bff67bb8376 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -1,7 +1,332 @@
<script>
-export default {};
+import { mapState, mapActions } from 'vuex';
+import {
+ GlTable,
+ GlFormCheckbox,
+ GlButton,
+ GlIcon,
+ GlTooltipDirective,
+ GlPagination,
+ GlModal,
+ GlLoadingIcon,
+ GlSprintf,
+ GlEmptyState,
+ GlResizeObserverDirective,
+} from '@gitlab/ui';
+import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
+import { n__, s__ } 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 {
+ 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,
+} from '../constants';
+
+export default {
+ components: {
+ GlTable,
+ GlFormCheckbox,
+ GlButton,
+ GlIcon,
+ ClipboardButton,
+ GlPagination,
+ GlModal,
+ GlLoadingIcon,
+ GlSprintf,
+ GlEmptyState,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ GlResizeObserver: GlResizeObserverDirective,
+ },
+ mixins: [timeagoMixin, Tracking.mixin()],
+ data() {
+ return {
+ selectedItems: [],
+ itemsToBeDeleted: [],
+ selectAllChecked: false,
+ modalDescription: null,
+ isDesktop: true,
+ };
+ },
+ computed: {
+ ...mapState(['tags', 'tagsPagination', 'isLoading', 'config']),
+ imageName() {
+ const { name } = JSON.parse(window.atob(this.$route.params.id));
+ return name;
+ },
+ fields() {
+ return [
+ { key: LIST_KEY_CHECKBOX, label: '' },
+ { key: LIST_KEY_TAG, label: LIST_LABEL_TAG },
+ { 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);
+ },
+ isMultiDelete() {
+ return this.itemsToBeDeleted.length > 1;
+ },
+ tracking() {
+ return {
+ label: this.isMultiDelete ? 'bulk_registry_tag_delete' : 'registry_tag_delete',
+ };
+ },
+ modalAction() {
+ return n__(
+ 'ContainerRegistry|Remove tag',
+ 'ContainerRegistry|Remove tags',
+ this.isMultiDelete ? this.itemsToBeDeleted.length : 1,
+ );
+ },
+ currentPage: {
+ get() {
+ return this.tagsPagination.page;
+ },
+ set(page) {
+ this.requestTagsList({ pagination: { page }, id: this.$route.params.id });
+ },
+ },
+ },
+ methods: {
+ ...mapActions(['requestTagsList', 'requestDeleteTag', 'requestDeleteTags']),
+ setModalDescription(itemIndex = -1) {
+ if (itemIndex === -1) {
+ this.modalDescription = {
+ message: s__(`ContainerRegistry|You are about to remove %{item} tags. Are you sure?`),
+ item: this.itemsToBeDeleted.length,
+ };
+ } else {
+ const { path } = this.tags[itemIndex];
+
+ this.modalDescription = {
+ message: s__(`ContainerRegistry|You are about to remove %{item}. Are you sure?`),
+ item: path,
+ };
+ }
+ },
+ 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, index) => index);
+ this.selectAllChecked = true;
+ },
+ deselectAll() {
+ this.selectedItems = [];
+ this.selectAllChecked = false;
+ },
+ updateSelectedItems(index) {
+ const delIndex = this.selectedItems.findIndex(x => x === index);
+
+ if (delIndex > -1) {
+ this.selectedItems.splice(delIndex, 1);
+ this.selectAllChecked = false;
+ } else {
+ this.selectedItems.push(index);
+
+ if (this.selectedItems.length === this.tags.length) {
+ this.selectAllChecked = true;
+ }
+ }
+ },
+ deleteSingleItem(index) {
+ this.setModalDescription(index);
+ this.itemsToBeDeleted = [index];
+ this.track('click_button');
+ this.$refs.deleteModal.show();
+ },
+ deleteMultipleItems() {
+ this.itemsToBeDeleted = [...this.selectedItems];
+ if (this.selectedItems.length === 1) {
+ this.setModalDescription(this.itemsToBeDeleted[0]);
+ } else if (this.selectedItems.length > 1) {
+ this.setModalDescription();
+ }
+ this.track('click_button');
+ this.$refs.deleteModal.show();
+ },
+ handleSingleDelete(itemToDelete) {
+ this.itemsToBeDeleted = [];
+ this.requestDeleteTag({ tag: itemToDelete, imageId: this.$route.params.id });
+ },
+ handleMultipleDelete() {
+ const { itemsToBeDeleted } = this;
+ this.itemsToBeDeleted = [];
+ this.selectedItems = [];
+
+ this.requestDeleteTags({
+ ids: itemsToBeDeleted.map(x => this.tags[x].name),
+ imageId: this.$route.params.id,
+ });
+ },
+ onDeletionConfirmed() {
+ this.track('confirm_delete');
+ if (this.isMultiDelete) {
+ this.handleMultipleDelete();
+ } else {
+ const index = this.itemsToBeDeleted[0];
+ this.handleSingleDelete(this.tags[index]);
+ }
+ },
+ handleResize() {
+ this.isDesktop = GlBreakpointInstance.isDesktop();
+ },
+ },
+};
</script>
<template>
- <div></div>
+ <div
+ v-gl-resize-observer="handleResize"
+ class="my-3 position-absolute w-100 slide-enter-to-element"
+ >
+ <div class="d-flex my-3 align-items-center">
+ <h4>
+ <gl-sprintf :message="s__('ContainerRegistry|%{imageName} tags')">
+ <template #imageName>
+ {{ imageName }}
+ </template>
+ </gl-sprintf>
+ </h4>
+ </div>
+ <gl-loading-icon v-if="isLoading" />
+ <template v-else-if="tags.length > 0">
+ <gl-table :items="tags" :fields="fields" :stacked="!isDesktop">
+ <template v-if="isDesktop" #head(checkbox)>
+ <gl-form-checkbox
+ ref="mainCheckbox"
+ :checked="selectAllChecked"
+ @change="onSelectAllChange"
+ />
+ </template>
+ <template #head(actions)>
+ <gl-button
+ ref="bulkDeleteButton"
+ v-gl-tooltip
+ :disabled="!selectedItems || selectedItems.length === 0"
+ class="float-right"
+ variant="danger"
+ :title="s__('ContainerRegistry|Remove selected tags')"
+ :aria-label="s__('ContainerRegistry|Remove selected tags')"
+ @click="deleteMultipleItems()"
+ >
+ <gl-icon name="remove" />
+ </gl-button>
+ </template>
+
+ <template #cell(checkbox)="{index}">
+ <gl-form-checkbox
+ ref="rowCheckbox"
+ class="js-row-checkbox"
+ :checked="selectedItems.includes(index)"
+ @change="updateSelectedItems(index)"
+ />
+ </template>
+ <template #cell(name)="{item}">
+ <span ref="rowName">
+ {{ 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"
+ />
+ </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">
+ &middot;
+ </template>
+ {{ layers(item.layers) }}
+ </span>
+ </template>
+ <template #cell(created_at)="{value}">
+ <span ref="rowTime">
+ {{ timeFormatted(value) }}
+ </span>
+ </template>
+ <template #cell(actions)="{index, item}">
+ <gl-button
+ ref="singleDeleteButton"
+ :title="s__('ContainerRegistry|Remove tag')"
+ :aria-label="s__('ContainerRegistry|Remove tag')"
+ :disabled="!item.destroy_path"
+ variant="danger"
+ :class="['js-delete-registry float-right btn-inverted btn-border-color btn-icon']"
+ @click="deleteSingleItem(index)"
+ >
+ <gl-icon name="remove" />
+ </gl-button>
+ </template>
+ </gl-table>
+ <gl-pagination
+ ref="pagination"
+ v-model="currentPage"
+ :per-page="tagsPagination.perPage"
+ :total-items="tagsPagination.total"
+ align="center"
+ class="w-100"
+ />
+ <gl-modal
+ ref="deleteModal"
+ modal-id="delete-tag-modal"
+ ok-variant="danger"
+ @ok="onDeletionConfirmed"
+ @cancel="track('cancel_delete')"
+ >
+ <template #modal-title>{{ modalAction }}</template>
+ <template #modal-ok>{{ modalAction }}</template>
+ <p v-if="modalDescription">
+ <gl-sprintf :message="modalDescription.message">
+ <template #item>
+ <b>{{ modalDescription.item }}</b>
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-modal>
+ </template>
+ <gl-empty-state
+ v-else
+ :title="s__('ContainerRegistry|This image has no active tags')"
+ :svg-path="config.noContainersImage"
+ :description="
+ s__(
+ `ContainerRegistry|The last tag related to this image was recently removed.
+ This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
+ If you have any questions, contact your administrator.`,
+ )
+ "
+ class="mx-auto my-0"
+ />
+ </div>
</template>
diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue
index 6d32ba41eae..dc730ac2828 100644
--- a/app/assets/javascripts/registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/registry/explorer/pages/list.vue
@@ -1,7 +1,214 @@
<script>
-export default {};
+import { mapState, mapActions } from 'vuex';
+import {
+ GlLoadingIcon,
+ GlEmptyState,
+ GlPagination,
+ GlTooltipDirective,
+ GlButton,
+ GlIcon,
+ GlModal,
+ GlSprintf,
+ GlLink,
+} from '@gitlab/ui';
+import Tracking from '~/tracking';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import ProjectEmptyState from '../components/project_empty_state.vue';
+import GroupEmptyState from '../components/group_empty_state.vue';
+
+export default {
+ name: 'RegistryListApp',
+ components: {
+ GlEmptyState,
+ GlLoadingIcon,
+ GlPagination,
+ ProjectEmptyState,
+ GroupEmptyState,
+ ClipboardButton,
+ GlButton,
+ GlIcon,
+ GlModal,
+ GlSprintf,
+ GlLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [Tracking.mixin()],
+ data() {
+ return {
+ itemToDelete: {},
+ };
+ },
+ computed: {
+ ...mapState(['config', 'isLoading', 'images', 'pagination']),
+ tracking() {
+ return {
+ label: 'registry_repository_delete',
+ };
+ },
+ currentPage: {
+ get() {
+ return this.pagination.page;
+ },
+ set(page) {
+ this.requestImagesList({ page });
+ },
+ },
+ },
+ methods: {
+ ...mapActions(['requestImagesList', 'requestDeleteImage']),
+ deleteImage(item) {
+ // This event is already tracked in the system and so the name must be kept to aggregate the data
+ this.track('click_button');
+ this.itemToDelete = item;
+ this.$refs.deleteModal.show();
+ },
+ handleDeleteRepository() {
+ this.track('confirm_delete');
+ this.requestDeleteImage(this.itemToDelete.destroy_path);
+ this.itemToDelete = {};
+ },
+ encodeListItem(item) {
+ const params = JSON.stringify({ name: item.path, tags_path: item.tags_path });
+ return window.btoa(params);
+ },
+ },
+};
</script>
<template>
- <div></div>
+ <div class="position-absolute w-100 slide-enter-from-element">
+ <gl-empty-state
+ v-if="config.characterError"
+ :title="s__('ContainerRegistry|Docker connection error')"
+ :svg-path="config.containersErrorImage"
+ >
+ <template #description>
+ <p>
+ <gl-sprintf
+ :message="
+ s__(`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an
+ issue with your project name or path.
+ %{docLinkStart}More Information%{docLinkEnd}`)
+ "
+ >
+ <template #docLink="{content}">
+ <gl-link :href="`${config.helpPagePath}#docker-connection-error`" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </template>
+ </gl-empty-state>
+
+ <template v-else>
+ <gl-loading-icon v-if="isLoading" size="md" class="prepend-top-16" />
+
+ <template v-else>
+ <div v-if="images.length" ref="imagesList">
+ <h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
+ <p>
+ <gl-sprintf
+ :message="
+ s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
+ project can have its own space to store its Docker images.
+ %{docLinkStart}More Information%{docLinkEnd}`)
+ "
+ >
+ <template #docLink="{content}">
+ <gl-link :href="config.helpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <div class="d-flex flex-column">
+ <div
+ v-for="(listItem, index) in images"
+ :key="index"
+ ref="rowItem"
+ :class="[
+ 'd-flex justify-content-between align-items-center py-2 border-bottom',
+ { 'border-top': index === 0 },
+ ]"
+ >
+ <div>
+ <router-link
+ ref="detailsLink"
+ :to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
+ >
+ {{ listItem.path }}
+ </router-link>
+ <clipboard-button
+ v-if="listItem.location"
+ ref="clipboardButton"
+ :text="listItem.location"
+ :title="listItem.location"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
+ </div>
+ <div
+ v-gl-tooltip="{ disabled: listItem.destroy_path }"
+ class="d-none d-sm-block"
+ :title="
+ s__(
+ 'ContainerRegistry|Missing or insufficient permission, delete button disabled',
+ )
+ "
+ >
+ <gl-button
+ ref="deleteImageButton"
+ v-gl-tooltip
+ :disabled="!listItem.destroy_path"
+ :title="s__('ContainerRegistry|Remove repository')"
+ :aria-label="s__('ContainerRegistry|Remove repository')"
+ class="btn-inverted"
+ variant="danger"
+ @click="deleteImage(listItem)"
+ >
+ <gl-icon name="remove" />
+ </gl-button>
+ </div>
+ </div>
+ </div>
+ <gl-pagination
+ v-model="currentPage"
+ :per-page="pagination.perPage"
+ :total-items="pagination.total"
+ align="center"
+ class="w-100 mt-2"
+ />
+ </div>
+ <template v-else>
+ <project-empty-state v-if="!config.isGroupPage" />
+ <group-empty-state v-else />
+ </template>
+ </template>
+
+ <gl-modal
+ ref="deleteModal"
+ modal-id="delete-image-modal"
+ ok-variant="danger"
+ @ok="handleDeleteRepository"
+ @cancel="track('cancel_delete')"
+ >
+ <template #modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template>
+ <p>
+ <gl-sprintf
+ :message=" s__(
+ 'ContainerRegistry|You are about to remove repository %{title}. Once you confirm, this repository will be permanently deleted.',
+ ),"
+ >
+ <template #title>
+ <b>{{ itemToDelete.path }}</b>
+ </template>
+ </gl-sprintf>
+ </p>
+ <template #modal-ok>{{ __('Remove') }}</template>
+ </gl-modal>
+ </template>
+ </div>
</template>
diff --git a/app/assets/javascripts/registry/explorer/stores/actions.js b/app/assets/javascripts/registry/explorer/stores/actions.js
index 7c06a12a5fc..25ff105ac53 100644
--- a/app/assets/javascripts/registry/explorer/stores/actions.js
+++ b/app/assets/javascripts/registry/explorer/stores/actions.js
@@ -45,11 +45,11 @@ export const requestImagesList = ({ commit, dispatch, state }, pagination = {})
export const requestTagsList = ({ commit, dispatch }, { pagination = {}, id }) => {
commit(types.SET_MAIN_LOADING, true);
- const url = window.atob(id);
+ const { tags_path } = JSON.parse(window.atob(id));
const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination;
return axios
- .get(url, { params: { page, per_page: perPage } })
+ .get(tags_path, { params: { page, per_page: perPage } })
.then(({ data, headers }) => {
dispatch('receiveTagsListSuccess', { data, headers });
})
diff --git a/app/assets/javascripts/registry/explorer/stores/mutations.js b/app/assets/javascripts/registry/explorer/stores/mutations.js
index 186f36a759a..a2c6a11de20 100644
--- a/app/assets/javascripts/registry/explorer/stores/mutations.js
+++ b/app/assets/javascripts/registry/explorer/stores/mutations.js
@@ -5,6 +5,7 @@ export default {
[types.SET_INITIAL_STATE](state, config) {
state.config = {
...config,
+ isGroupPage: config.isGroupPage !== undefined,
};
},