summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/registry/explorer/components/list_page
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/registry/explorer/components/list_page')
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue103
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue39
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list.vue52
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue136
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue107
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue138
6 files changed, 575 insertions, 0 deletions
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
new file mode 100644
index 00000000000..8b06797c0ae
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
@@ -0,0 +1,103 @@
+<script>
+import { GlDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
+import Tracking from '~/tracking';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import {
+ QUICK_START,
+ LOGIN_COMMAND_LABEL,
+ COPY_LOGIN_TITLE,
+ BUILD_COMMAND_LABEL,
+ COPY_BUILD_TITLE,
+ PUSH_COMMAND_LABEL,
+ COPY_PUSH_TITLE,
+} from '../../constants/index';
+
+export default {
+ components: {
+ GlDropdown,
+ GlFormGroup,
+ GlFormInputGroup,
+ ClipboardButton,
+ },
+ mixins: [Tracking.mixin({ label: 'quickstart_dropdown' })],
+ i18n: {
+ dropdownTitle: QUICK_START,
+ loginCommandLabel: LOGIN_COMMAND_LABEL,
+ copyLoginTitle: COPY_LOGIN_TITLE,
+ buildCommandLabel: BUILD_COMMAND_LABEL,
+ copyBuildTitle: COPY_BUILD_TITLE,
+ pushCommandLabel: PUSH_COMMAND_LABEL,
+ copyPushTitle: COPY_PUSH_TITLE,
+ },
+ computed: {
+ ...mapGetters(['dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand']),
+ },
+};
+</script>
+<template>
+ <gl-dropdown
+ :text="$options.i18n.dropdownTitle"
+ variant="primary"
+ size="sm"
+ right
+ @shown="track('click_dropdown')"
+ >
+ <!-- This li is used as a container since gl-dropdown produces a root ul, this mimics the functionality exposed by b-dropdown-form -->
+ <li role="presentation" class="px-2 py-1 dropdown-menu-large">
+ <form>
+ <gl-form-group
+ label-size="sm"
+ label-for="docker-login-btn"
+ :label="$options.i18n.loginCommandLabel"
+ >
+ <gl-form-input-group id="docker-login-btn" :value="dockerLoginCommand" readonly>
+ <template #append>
+ <clipboard-button
+ class="border"
+ :text="dockerLoginCommand"
+ :title="$options.i18n.copyLoginTitle"
+ @click.native="track('click_copy_login')"
+ />
+ </template>
+ </gl-form-input-group>
+ </gl-form-group>
+
+ <gl-form-group
+ label-size="sm"
+ label-for="docker-build-btn"
+ :label="$options.i18n.buildCommandLabel"
+ >
+ <gl-form-input-group id="docker-build-btn" :value="dockerBuildCommand" readonly>
+ <template #append>
+ <clipboard-button
+ class="border"
+ :text="dockerBuildCommand"
+ :title="$options.i18n.copyBuildTitle"
+ @click.native="track('click_copy_build')"
+ />
+ </template>
+ </gl-form-input-group>
+ </gl-form-group>
+
+ <gl-form-group
+ class="mb-0"
+ label-size="sm"
+ label-for="docker-push-btn"
+ :label="$options.i18n.pushCommandLabel"
+ >
+ <gl-form-input-group id="docker-push-btn" :value="dockerPushCommand" readonly>
+ <template #append>
+ <clipboard-button
+ class="border"
+ :text="dockerPushCommand"
+ :title="$options.i18n.copyPushTitle"
+ @click.native="track('click_copy_push')"
+ />
+ </template>
+ </gl-form-input-group>
+ </gl-form-group>
+ </form>
+ </li>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue b/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue
new file mode 100644
index 00000000000..a29a9bd23c3
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/list_page/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/list_page/image_list.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
new file mode 100644
index 00000000000..9d48769cbad
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlPagination } from '@gitlab/ui';
+import ImageListRow from './image_list_row.vue';
+
+export default {
+ name: 'ImageList',
+ components: {
+ GlPagination,
+ ImageListRow,
+ },
+ props: {
+ images: {
+ type: Array,
+ required: true,
+ },
+ pagination: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ currentPage: {
+ get() {
+ return this.pagination.page;
+ },
+ set(page) {
+ this.$emit('pageChange', page);
+ },
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-flex-direction-column">
+ <image-list-row
+ v-for="(listItem, index) in images"
+ :key="index"
+ :item="listItem"
+ :show-top-border="index === 0"
+ @delete="$emit('delete', $event)"
+ />
+
+ <gl-pagination
+ v-model="currentPage"
+ :per-page="pagination.perPage"
+ :total-items="pagination.total"
+ align="center"
+ class="w-100 gl-mt-3"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
new file mode 100644
index 00000000000..cd878c38081
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
@@ -0,0 +1,136 @@
+<script>
+import { GlTooltipDirective, GlButton, GlIcon, GlSprintf } from '@gitlab/ui';
+import { n__ } from '~/locale';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+import {
+ ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ LIST_DELETE_BUTTON_DISABLED,
+ REMOVE_REPOSITORY_LABEL,
+ ROW_SCHEDULED_FOR_DELETION,
+} from '../../constants/index';
+
+export default {
+ name: 'ImageListrow',
+ components: {
+ ClipboardButton,
+ GlButton,
+ GlSprintf,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ item: {
+ type: Object,
+ required: true,
+ },
+ showTopBorder: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ i18n: {
+ LIST_DELETE_BUTTON_DISABLED,
+ REMOVE_REPOSITORY_LABEL,
+ ROW_SCHEDULED_FOR_DELETION,
+ ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ },
+ computed: {
+ encodedItem() {
+ const params = JSON.stringify({
+ name: this.item.path,
+ tags_path: this.item.tags_path,
+ id: this.item.id,
+ });
+ return window.btoa(params);
+ },
+ disabledDelete() {
+ return !this.item.destroy_path || this.item.deleting;
+ },
+ tagsCountText() {
+ return n__(
+ 'ContainerRegistry|%{count} Tag',
+ 'ContainerRegistry|%{count} Tags',
+ this.item.tags_count,
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-gl-tooltip="{
+ placement: 'left',
+ disabled: !item.deleting,
+ title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
+ }"
+ >
+ <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 "
+ :class="{
+ 'gl-border-t-solid gl-border-t-1': showTopBorder,
+ 'disabled-content': item.deleting,
+ }"
+ >
+ <div class="gl-display-flex gl-flex-direction-column">
+ <div class="gl-display-flex gl-align-items-center">
+ <router-link
+ class="gl-text-black-normal gl-font-weight-bold"
+ data-testid="detailsLink"
+ :to="{ name: 'details', params: { id: encodedItem } }"
+ >
+ {{ item.path }}
+ </router-link>
+ <clipboard-button
+ v-if="item.location"
+ :disabled="item.deleting"
+ :text="item.location"
+ :title="item.location"
+ css-class="btn-default btn-transparent btn-clipboard gl-text-gray-500"
+ />
+ <gl-icon
+ v-if="item.failedDelete"
+ v-gl-tooltip
+ :title="$options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE"
+ name="warning"
+ class="text-warning"
+ />
+ </div>
+ <div class="gl-font-sm gl-text-gray-500">
+ <span class="gl-display-flex gl-align-items-center" data-testid="tagsCount">
+ <gl-icon name="tag" class="gl-mr-2" />
+ <gl-sprintf :message="tagsCountText">
+ <template #count>
+ {{ item.tags_count }}
+ </template>
+ </gl-sprintf>
+ </span>
+ </div>
+ </div>
+ <div
+ v-gl-tooltip="{
+ disabled: item.destroy_path,
+ title: $options.i18n.LIST_DELETE_BUTTON_DISABLED,
+ }"
+ class="d-none d-sm-block"
+ data-testid="deleteButtonWrapper"
+ >
+ <gl-button
+ v-gl-tooltip
+ data-testid="deleteImageButton"
+ :disabled="disabledDelete"
+ :title="$options.i18n.REMOVE_REPOSITORY_LABEL"
+ :aria-label="$options.i18n.REMOVE_REPOSITORY_LABEL"
+ category="secondary"
+ variant="danger"
+ icon="remove"
+ @click="$emit('delete', item)"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue b/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue
new file mode 100644
index 00000000000..c27d53f4351
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue
@@ -0,0 +1,107 @@
+<script>
+import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
+import { mapState, mapGetters } from 'vuex';
+import { s__ } from '~/locale';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import {
+ COPY_LOGIN_TITLE,
+ COPY_BUILD_TITLE,
+ COPY_PUSH_TITLE,
+ QUICK_START,
+} from '../../constants/index';
+
+export default {
+ name: 'ProjectEmptyState',
+ components: {
+ ClipboardButton,
+ GlEmptyState,
+ GlSprintf,
+ GlLink,
+ },
+ i18n: {
+ quickStart: QUICK_START,
+ copyLoginTitle: COPY_LOGIN_TITLE,
+ copyBuildTitle: COPY_BUILD_TITLE,
+ copyPushTitle: COPY_PUSH_TITLE,
+ introText: s__(
+ `ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`,
+ ),
+ notLoggedInMessage: 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.`,
+ ),
+ addImageText: s__(
+ 'ContainerRegistry|You can add an image to this registry with the following commands:',
+ ),
+ },
+ computed: {
+ ...mapState(['config']),
+ ...mapGetters(['dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand']),
+ },
+};
+</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="$options.i18n.introText">
+ <template #docLink="{content}">
+ <gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <h5>{{ $options.i18n.quickStart }}</h5>
+ <p class="js-not-logged-in-to-registry-text">
+ <gl-sprintf :message="$options.i18n.notLoggedInMessage">
+ <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="$options.i18n.copyLoginTitle"
+ class="input-group-text"
+ />
+ </span>
+ </div>
+ <p></p>
+ <p>
+ {{ $options.i18n.addImageText }}
+ </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="$options.i18n.copyBuildTitle"
+ 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="$options.i18n.copyPushTitle"
+ class="input-group-text"
+ />
+ </span>
+ </div>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
new file mode 100644
index 00000000000..d4ff84447bb
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
@@ -0,0 +1,138 @@
+<script>
+import { GlSprintf, GlLink, GlIcon } from '@gitlab/ui';
+import { n__ } from '~/locale';
+import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
+
+import {
+ CONTAINER_REGISTRY_TITLE,
+ LIST_INTRO_TEXT,
+ EXPIRATION_POLICY_WILL_RUN_IN,
+ EXPIRATION_POLICY_DISABLED_TEXT,
+ EXPIRATION_POLICY_DISABLED_MESSAGE,
+} from '../../constants/index';
+
+export default {
+ components: {
+ GlIcon,
+ GlSprintf,
+ GlLink,
+ },
+ props: {
+ expirationPolicy: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ imagesCount: {
+ type: Number,
+ default: 0,
+ required: false,
+ },
+ helpPagePath: {
+ type: String,
+ default: '',
+ required: false,
+ },
+ expirationPolicyHelpPagePath: {
+ type: String,
+ default: '',
+ required: false,
+ },
+ hideExpirationPolicyData: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ loader: {
+ repeat: 10,
+ width: 1000,
+ height: 40,
+ },
+ i18n: {
+ CONTAINER_REGISTRY_TITLE,
+ LIST_INTRO_TEXT,
+ EXPIRATION_POLICY_DISABLED_MESSAGE,
+ },
+ computed: {
+ imagesCountText() {
+ return n__(
+ 'ContainerRegistry|%{count} Image repository',
+ 'ContainerRegistry|%{count} Image repositories',
+ this.imagesCount,
+ );
+ },
+ timeTillRun() {
+ const difference = calculateRemainingMilliseconds(this.expirationPolicy?.next_run_at);
+ return approximateDuration(difference / 1000);
+ },
+ expirationPolicyEnabled() {
+ return this.expirationPolicy?.enabled;
+ },
+ expirationPolicyText() {
+ return this.expirationPolicyEnabled
+ ? EXPIRATION_POLICY_WILL_RUN_IN
+ : EXPIRATION_POLICY_DISABLED_TEXT;
+ },
+ showExpirationPolicyTip() {
+ return (
+ !this.expirationPolicyEnabled && this.imagesCount > 0 && !this.hideExpirationPolicyData
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center"
+ data-testid="header"
+ >
+ <h4 data-testid="title">{{ $options.i18n.CONTAINER_REGISTRY_TITLE }}</h4>
+ <div class="gl-display-none d-sm-block" data-testid="commands-slot">
+ <slot name="commands"></slot>
+ </div>
+ </div>
+ <div
+ v-if="imagesCount"
+ class="gl-display-flex gl-align-items-center gl-mt-1 gl-mb-3 gl-text-gray-700"
+ data-testid="subheader"
+ >
+ <span class="gl-mr-3" data-testid="images-count">
+ <gl-icon class="gl-mr-1" name="container-image" />
+ <gl-sprintf :message="imagesCountText">
+ <template #count>
+ {{ imagesCount }}
+ </template>
+ </gl-sprintf>
+ </span>
+ <span v-if="!hideExpirationPolicyData" data-testid="expiration-policy">
+ <gl-icon class="gl-mr-1" name="expire" />
+ <gl-sprintf :message="expirationPolicyText">
+ <template #time>
+ {{ timeTillRun }}
+ </template>
+ </gl-sprintf>
+ </span>
+ </div>
+ <div data-testid="info-area">
+ <p>
+ <span data-testid="default-intro">
+ <gl-sprintf :message="$options.i18n.LIST_INTRO_TEXT">
+ <template #docLink="{content}">
+ <gl-link :href="helpPagePath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ <span v-if="showExpirationPolicyTip" data-testid="expiration-disabled-message">
+ <gl-sprintf :message="$options.i18n.EXPIRATION_POLICY_DISABLED_MESSAGE">
+ <template #docLink="{content}">
+ <gl-link :href="expirationPolicyHelpPagePath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </p>
+ </div>
+ </div>
+</template>