Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
143f7be045
commit
0e1a6f6a2b
|
|
@ -1,4 +1,6 @@
|
|||
import { inactiveListId } from '~/boards/constants';
|
||||
|
||||
export default () => ({
|
||||
isShowingLabels: true,
|
||||
activeListId: 0,
|
||||
activeListId: inactiveListId,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { GlTable, GlLoadingIcon, GlBadge } from '@gitlab/ui';
|
||||
import { GlTable, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import { CLUSTER_TYPES, STATUSES } from '../constants';
|
||||
import { __, sprintf } from '~/locale';
|
||||
|
|
@ -8,54 +8,58 @@ import { __, sprintf } from '~/locale';
|
|||
export default {
|
||||
components: {
|
||||
GlTable,
|
||||
GlLink,
|
||||
GlLoadingIcon,
|
||||
GlBadge,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
key: 'name',
|
||||
label: __('Kubernetes cluster'),
|
||||
},
|
||||
{
|
||||
key: 'environmentScope',
|
||||
label: __('Environment scope'),
|
||||
},
|
||||
{
|
||||
key: 'size',
|
||||
label: __('Size'),
|
||||
},
|
||||
{
|
||||
key: 'cpu',
|
||||
label: __('Total cores (vCPUs)'),
|
||||
},
|
||||
{
|
||||
key: 'memory',
|
||||
label: __('Total memory (GB)'),
|
||||
},
|
||||
{
|
||||
key: 'clusterType',
|
||||
label: __('Cluster level'),
|
||||
formatter: value => CLUSTER_TYPES[value],
|
||||
},
|
||||
],
|
||||
computed: {
|
||||
...mapState(['clusters', 'loading']),
|
||||
fields() {
|
||||
return [
|
||||
{
|
||||
key: 'name',
|
||||
label: __('Kubernetes cluster'),
|
||||
},
|
||||
{
|
||||
key: 'environment_scope',
|
||||
label: __('Environment scope'),
|
||||
},
|
||||
// Wait for backend to send these fields
|
||||
// {
|
||||
// key: 'size',
|
||||
// label: __('Size'),
|
||||
// },
|
||||
// {
|
||||
// key: 'cpu',
|
||||
// label: __('Total cores (vCPUs)'),
|
||||
// },
|
||||
// {
|
||||
// key: 'memory',
|
||||
// label: __('Total memory (GB)'),
|
||||
// },
|
||||
{
|
||||
key: 'cluster_type',
|
||||
label: __('Cluster level'),
|
||||
formatter: value => CLUSTER_TYPES[value],
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// TODO - uncomment this once integrated with BE
|
||||
// this.fetchClusters();
|
||||
this.fetchClusters();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchClusters']),
|
||||
statusClass(status) {
|
||||
return STATUSES[status].className;
|
||||
const iconClass = STATUSES[status] || STATUSES.default;
|
||||
return iconClass.className;
|
||||
},
|
||||
statusTitle(status) {
|
||||
const { title } = STATUSES[status];
|
||||
return sprintf(__('Status: %{title}'), { title }, false);
|
||||
const iconTitle = STATUSES[status] || STATUSES.default;
|
||||
return sprintf(__('Status: %{title}'), { title: iconTitle.title }, false);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -63,17 +67,13 @@ export default {
|
|||
|
||||
<template>
|
||||
<gl-loading-icon v-if="loading" size="md" class="mt-3" />
|
||||
<gl-table
|
||||
v-else
|
||||
:items="clusters"
|
||||
:fields="$options.fields"
|
||||
stacked="md"
|
||||
variant="light"
|
||||
class="qa-clusters-table"
|
||||
>
|
||||
<gl-table v-else :items="clusters" :fields="fields" stacked="md" class="qa-clusters-table">
|
||||
<template #cell(name)="{ item }">
|
||||
<div class="d-flex flex-row-reverse flex-md-row js-status">
|
||||
{{ item.name }}
|
||||
<gl-link data-qa-selector="cluster" :data-qa-cluster-name="item.name" :href="item.path">
|
||||
{{ item.name }}
|
||||
</gl-link>
|
||||
|
||||
<gl-loading-icon
|
||||
v-if="item.status === 'deleting'"
|
||||
v-tooltip
|
||||
|
|
@ -84,13 +84,13 @@ export default {
|
|||
<div
|
||||
v-else
|
||||
v-tooltip
|
||||
class="cluster-status-indicator rounded-circle align-self-center gl-w-8 gl-h-8 mr-2 ml-md-2"
|
||||
class="cluster-status-indicator rounded-circle align-self-center gl-w-4 gl-h-4 mr-2 ml-md-2"
|
||||
:class="statusClass(item.status)"
|
||||
:title="statusTitle(item.status)"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(clusterType)="{value}">
|
||||
<template #cell(cluster_type)="{value}">
|
||||
<gl-badge variant="light">
|
||||
{{ value }}
|
||||
</gl-badge>
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ export const CLUSTER_TYPES = {
|
|||
};
|
||||
|
||||
export const STATUSES = {
|
||||
default: { className: 'bg-white', title: __('Unknown') },
|
||||
disabled: { className: 'disabled', title: __('Disabled') },
|
||||
connected: { className: 'bg-success', title: __('Connected') },
|
||||
created: { className: 'bg-success', title: __('Connected') },
|
||||
unreachable: { className: 'bg-danger', title: __('Unreachable') },
|
||||
authentication_failure: { className: 'bg-warning', title: __('Authentication Failure') },
|
||||
deleting: { title: __('Deleting') },
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import Poll from '~/lib/utils/poll';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import Visibility from 'visibilityjs';
|
||||
import flash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import * as types from './mutation_types';
|
||||
|
|
@ -14,23 +12,16 @@ export const fetchClusters = ({ state, commit }) => {
|
|||
data: state.endpoint,
|
||||
method: 'fetchClusters',
|
||||
successCallback: ({ data }) => {
|
||||
commit(types.SET_CLUSTERS_DATA, convertObjectPropsToCamelCase(data, { deep: true }));
|
||||
commit(types.SET_LOADING_STATE, false);
|
||||
if (data.clusters) {
|
||||
commit(types.SET_CLUSTERS_DATA, data);
|
||||
commit(types.SET_LOADING_STATE, false);
|
||||
poll.stop();
|
||||
}
|
||||
},
|
||||
errorCallback: () => flash(__('An error occurred while loading clusters')),
|
||||
});
|
||||
|
||||
if (!Visibility.hidden()) {
|
||||
poll.makeRequest();
|
||||
}
|
||||
|
||||
Visibility.change(() => {
|
||||
if (!Visibility.hidden()) {
|
||||
poll.restart();
|
||||
} else {
|
||||
poll.stop();
|
||||
}
|
||||
});
|
||||
poll.makeRequest();
|
||||
};
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ export default {
|
|||
[types.SET_LOADING_STATE](state, value) {
|
||||
state.loading = value;
|
||||
},
|
||||
[types.SET_CLUSTERS_DATA](state, clusters) {
|
||||
[types.SET_CLUSTERS_DATA](state, data) {
|
||||
Object.assign(state, {
|
||||
clusters,
|
||||
clusters: data.clusters,
|
||||
hasAncestorClusters: data.has_ancestor_clusters,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export default (initialState = {}) => ({
|
||||
endpoint: initialState.endpoint,
|
||||
loading: false, // TODO - set this to true once integrated with BE
|
||||
hasAncestorClusters: false,
|
||||
loading: true,
|
||||
clusters: [],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import getDesignListQuery from './graphql/queries/get_design_list.query.graphql'
|
|||
import { DESIGNS_ROUTE_NAME, ROOT_ROUTE_NAME } from './router/constants';
|
||||
|
||||
export default () => {
|
||||
const el = document.getElementById('js-design-management');
|
||||
const el = document.querySelector('.js-design-management');
|
||||
const badge = document.querySelector('.js-designs-count');
|
||||
const { issueIid, projectPath, issuePath } = el.dataset;
|
||||
const router = createRouter(issuePath);
|
||||
|
|
|
|||
|
|
@ -259,8 +259,10 @@ export default {
|
|||
});
|
||||
},
|
||||
trackEvent() {
|
||||
// TODO: This needs to be made aware of referers, or if it's rendered in a different context than a Issue
|
||||
trackDesignDetailView(
|
||||
'issue-design-collection',
|
||||
'issue',
|
||||
this.$route.query.version || this.latestVersionId,
|
||||
this.isLatestVersion,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ function assembleDesignPayload(payloadArr) {
|
|||
return {
|
||||
value: {
|
||||
'internal-object-refrerer': payloadArr[0],
|
||||
'version-number': payloadArr[1],
|
||||
'current-version': payloadArr[2],
|
||||
'design-collection-owner': payloadArr[1],
|
||||
'design-version-number': payloadArr[2],
|
||||
'design-is-current-version': payloadArr[3],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -14,9 +15,14 @@ function assembleDesignPayload(payloadArr) {
|
|||
const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function trackDesignDetailView(refrerer = '', designVersion = 1, latestVersion = false) {
|
||||
export function trackDesignDetailView(
|
||||
referer = '',
|
||||
owner = '',
|
||||
designVersion = 1,
|
||||
latestVersion = false,
|
||||
) {
|
||||
Tracking.event(DESIGN_TRACKING_PAGE_NAME, 'design_viewed', {
|
||||
label: 'design_viewed',
|
||||
...assembleDesignPayload([refrerer, designVersion, latestVersion]),
|
||||
...assembleDesignPayload([referer, owner, designVersion, latestVersion]),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
|
||||
const issueablesEventBus = new Vue();
|
||||
|
||||
export default issueablesEventBus;
|
||||
export default createEventHub();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,16 @@ export default function() {
|
|||
initIssueableApp();
|
||||
initSentryErrorStackTraceApp();
|
||||
initRelatedMergeRequestsApp();
|
||||
|
||||
// .js-design-management is currently EE-only.
|
||||
// This will be moved to CE as part of https://gitlab.com/gitlab-org/gitlab/-/issues/212566#frontend
|
||||
// at which point this conditional can be removed.
|
||||
if (document.querySelector('.js-design-management')) {
|
||||
import(/* webpackChunkName: 'design_management' */ '~/design_management')
|
||||
.then(module => module.default())
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
new Issue(); // eslint-disable-line no-new
|
||||
new ShortcutsIssuable(); // eslint-disable-line no-new
|
||||
new ZenMode(); // eslint-disable-line no-new
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
<script>
|
||||
import { GlPagination, GlTooltipDirective, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
|
||||
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';
|
||||
|
||||
export default {
|
||||
name: 'ImageList',
|
||||
components: {
|
||||
GlPagination,
|
||||
ClipboardButton,
|
||||
GlDeprecatedButton,
|
||||
GlIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
images: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
pagination: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
LIST_DELETE_BUTTON_DISABLED,
|
||||
REMOVE_REPOSITORY_LABEL,
|
||||
ROW_SCHEDULED_FOR_DELETION,
|
||||
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
|
||||
},
|
||||
computed: {
|
||||
currentPage: {
|
||||
get() {
|
||||
return this.pagination.page;
|
||||
},
|
||||
set(page) {
|
||||
this.$emit('pageChange', page);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
encodeListItem(item) {
|
||||
const params = JSON.stringify({ name: item.path, tags_path: item.tags_path, id: item.id });
|
||||
return window.btoa(params);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-flex-direction-column">
|
||||
<div
|
||||
v-for="(listItem, index) in images"
|
||||
:key="index"
|
||||
v-gl-tooltip="{
|
||||
placement: 'left',
|
||||
disabled: !listItem.deleting,
|
||||
title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
|
||||
}"
|
||||
data-testid="rowItem"
|
||||
>
|
||||
<div
|
||||
class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 border-bottom"
|
||||
:class="{ 'border-top': index === 0, 'disabled-content': listItem.deleting }"
|
||||
>
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<router-link
|
||||
data-testid="detailsLink"
|
||||
:to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
|
||||
>
|
||||
{{ listItem.path }}
|
||||
</router-link>
|
||||
<clipboard-button
|
||||
v-if="listItem.location"
|
||||
:disabled="listItem.deleting"
|
||||
:text="listItem.location"
|
||||
:title="listItem.location"
|
||||
css-class="btn-default btn-transparent btn-clipboard"
|
||||
/>
|
||||
<gl-icon
|
||||
v-if="listItem.failedDelete"
|
||||
v-gl-tooltip
|
||||
:title="$options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE"
|
||||
name="warning"
|
||||
class="text-warning align-middle"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-gl-tooltip="{ disabled: listItem.destroy_path }"
|
||||
class="d-none d-sm-block"
|
||||
:title="$options.i18n.LIST_DELETE_BUTTON_DISABLED"
|
||||
>
|
||||
<gl-deprecated-button
|
||||
v-gl-tooltip
|
||||
data-testid="deleteImageButton"
|
||||
:disabled="!listItem.destroy_path || listItem.deleting"
|
||||
:title="$options.i18n.REMOVE_REPOSITORY_LABEL"
|
||||
:aria-label="$options.i18n.REMOVE_REPOSITORY_LABEL"
|
||||
class="btn-inverted"
|
||||
variant="danger"
|
||||
@click="$emit('delete', listItem)"
|
||||
>
|
||||
<gl-icon name="remove" />
|
||||
</gl-deprecated-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<gl-pagination
|
||||
v-model="currentPage"
|
||||
:per-page="pagination.perPage"
|
||||
:total-items="pagination.total"
|
||||
align="center"
|
||||
class="w-100 gl-mt-2"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -37,6 +37,15 @@ export const DELETE_IMAGE_SUCCESS_MESSAGE = s__(
|
|||
'ContainerRegistry|%{title} was successfully scheduled for deletion',
|
||||
);
|
||||
|
||||
export const IMAGE_REPOSITORY_LIST_LABEL = s__('ContainerRegistry|Image Repositories');
|
||||
|
||||
export const SEARCH_PLACEHOLDER_TEXT = s__('ContainerRegistry|Filter by name');
|
||||
|
||||
export const EMPTY_RESULT_TITLE = s__('ContainerRegistry|Sorry, your filter produced no results.');
|
||||
export const EMPTY_RESULT_MESSAGE = s__(
|
||||
'ContainerRegistry|To widen your search, change or remove the filters above.',
|
||||
);
|
||||
|
||||
// Image details page
|
||||
|
||||
export const DETAILS_PAGE_TITLE = s__('ContainerRegistry|%{imageName} tags');
|
||||
|
|
|
|||
|
|
@ -2,53 +2,52 @@
|
|||
import { mapState, mapActions } from 'vuex';
|
||||
import {
|
||||
GlEmptyState,
|
||||
GlPagination,
|
||||
GlTooltipDirective,
|
||||
GlDeprecatedButton,
|
||||
GlIcon,
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
GlAlert,
|
||||
GlSkeletonLoader,
|
||||
GlSearchBoxByClick,
|
||||
} 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';
|
||||
import ProjectPolicyAlert from '../components/project_policy_alert.vue';
|
||||
import QuickstartDropdown from '../components/quickstart_dropdown.vue';
|
||||
import ImageList from '../components/image_list.vue';
|
||||
|
||||
import {
|
||||
DELETE_IMAGE_SUCCESS_MESSAGE,
|
||||
DELETE_IMAGE_ERROR_MESSAGE,
|
||||
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
|
||||
CONTAINER_REGISTRY_TITLE,
|
||||
CONNECTION_ERROR_TITLE,
|
||||
CONNECTION_ERROR_MESSAGE,
|
||||
LIST_INTRO_TEXT,
|
||||
LIST_DELETE_BUTTON_DISABLED,
|
||||
REMOVE_REPOSITORY_LABEL,
|
||||
REMOVE_REPOSITORY_MODAL_TEXT,
|
||||
ROW_SCHEDULED_FOR_DELETION,
|
||||
REMOVE_REPOSITORY_LABEL,
|
||||
SEARCH_PLACEHOLDER_TEXT,
|
||||
IMAGE_REPOSITORY_LIST_LABEL,
|
||||
EMPTY_RESULT_TITLE,
|
||||
EMPTY_RESULT_MESSAGE,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'RegistryListApp',
|
||||
components: {
|
||||
GlEmptyState,
|
||||
GlPagination,
|
||||
ProjectEmptyState,
|
||||
GroupEmptyState,
|
||||
ProjectPolicyAlert,
|
||||
ClipboardButton,
|
||||
QuickstartDropdown,
|
||||
GlDeprecatedButton,
|
||||
GlIcon,
|
||||
ImageList,
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
GlAlert,
|
||||
GlSkeletonLoader,
|
||||
GlSearchBoxByClick,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
|
@ -60,20 +59,23 @@ export default {
|
|||
height: 40,
|
||||
},
|
||||
i18n: {
|
||||
containerRegistryTitle: CONTAINER_REGISTRY_TITLE,
|
||||
connectionErrorTitle: CONNECTION_ERROR_TITLE,
|
||||
connectionErrorMessage: CONNECTION_ERROR_MESSAGE,
|
||||
introText: LIST_INTRO_TEXT,
|
||||
deleteButtonDisabled: LIST_DELETE_BUTTON_DISABLED,
|
||||
removeRepositoryLabel: REMOVE_REPOSITORY_LABEL,
|
||||
removeRepositoryModalText: REMOVE_REPOSITORY_MODAL_TEXT,
|
||||
rowScheduledForDeletion: ROW_SCHEDULED_FOR_DELETION,
|
||||
asyncDeleteErrorMessage: ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
|
||||
CONTAINER_REGISTRY_TITLE,
|
||||
CONNECTION_ERROR_TITLE,
|
||||
CONNECTION_ERROR_MESSAGE,
|
||||
LIST_INTRO_TEXT,
|
||||
REMOVE_REPOSITORY_MODAL_TEXT,
|
||||
REMOVE_REPOSITORY_LABEL,
|
||||
SEARCH_PLACEHOLDER_TEXT,
|
||||
IMAGE_REPOSITORY_LIST_LABEL,
|
||||
EMPTY_RESULT_TITLE,
|
||||
EMPTY_RESULT_MESSAGE,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
itemToDelete: {},
|
||||
deleteAlertType: null,
|
||||
search: null,
|
||||
isEmpty: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -83,14 +85,6 @@ export default {
|
|||
label: 'registry_repository_delete',
|
||||
};
|
||||
},
|
||||
currentPage: {
|
||||
get() {
|
||||
return this.pagination.page;
|
||||
},
|
||||
set(page) {
|
||||
this.requestImagesList({ page });
|
||||
},
|
||||
},
|
||||
showQuickStartDropdown() {
|
||||
return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length);
|
||||
},
|
||||
|
|
@ -110,8 +104,11 @@ export default {
|
|||
...mapActions(['requestImagesList', 'requestDeleteImage']),
|
||||
loadImageList(fromName) {
|
||||
if (!fromName || !this.images?.length) {
|
||||
this.requestImagesList();
|
||||
return this.requestImagesList().then(() => {
|
||||
this.isEmpty = this.images.length === 0;
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
deleteImage(item) {
|
||||
this.track('click_button');
|
||||
|
|
@ -128,10 +125,6 @@ export default {
|
|||
this.deleteAlertType = 'danger';
|
||||
});
|
||||
},
|
||||
encodeListItem(item) {
|
||||
const params = JSON.stringify({ name: item.path, tags_path: item.tags_path, id: item.id });
|
||||
return window.btoa(params);
|
||||
},
|
||||
dismissDeleteAlert() {
|
||||
this.deleteAlertType = null;
|
||||
this.itemToDelete = {};
|
||||
|
|
@ -160,12 +153,12 @@ export default {
|
|||
|
||||
<gl-empty-state
|
||||
v-if="config.characterError"
|
||||
:title="$options.i18n.connectionErrorTitle"
|
||||
:title="$options.i18n.CONNECTION_ERROR_TITLE"
|
||||
:svg-path="config.containersErrorImage"
|
||||
>
|
||||
<template #description>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.connectionErrorMessage">
|
||||
<gl-sprintf :message="$options.i18n.CONNECTION_ERROR_MESSAGE">
|
||||
<template #docLink="{content}">
|
||||
<gl-link :href="`${config.helpPagePath}#docker-connection-error`" target="_blank">
|
||||
{{ content }}
|
||||
|
|
@ -179,11 +172,11 @@ export default {
|
|||
<template v-else>
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h4>{{ $options.i18n.containerRegistryTitle }}</h4>
|
||||
<h4>{{ $options.i18n.CONTAINER_REGISTRY_TITLE }}</h4>
|
||||
<quickstart-dropdown v-if="showQuickStartDropdown" class="d-none d-sm-block" />
|
||||
</div>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.introText">
|
||||
<gl-sprintf :message="$options.i18n.LIST_INTRO_TEXT">
|
||||
<template #docLink="{content}">
|
||||
<gl-link :href="config.helpPagePath" target="_blank">
|
||||
{{ content }}
|
||||
|
|
@ -207,73 +200,40 @@ export default {
|
|||
</gl-skeleton-loader>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-if="images.length" ref="imagesList" class="d-flex flex-column">
|
||||
<div
|
||||
v-for="(listItem, index) in images"
|
||||
:key="index"
|
||||
ref="rowItem"
|
||||
v-gl-tooltip="{
|
||||
placement: 'left',
|
||||
disabled: !listItem.deleting,
|
||||
title: $options.i18n.rowScheduledForDeletion,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="d-flex justify-content-between align-items-center py-2 px-1 border-bottom"
|
||||
:class="{ 'border-top': index === 0, 'disabled-content': listItem.deleting }"
|
||||
>
|
||||
<div class="d-felx align-items-center">
|
||||
<router-link
|
||||
ref="detailsLink"
|
||||
:to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
|
||||
>
|
||||
{{ listItem.path }}
|
||||
</router-link>
|
||||
<clipboard-button
|
||||
v-if="listItem.location"
|
||||
ref="clipboardButton"
|
||||
:disabled="listItem.deleting"
|
||||
:text="listItem.location"
|
||||
:title="listItem.location"
|
||||
css-class="btn-default btn-transparent btn-clipboard"
|
||||
/>
|
||||
<gl-icon
|
||||
v-if="listItem.failedDelete"
|
||||
v-gl-tooltip
|
||||
:title="$options.i18n.asyncDeleteErrorMessage"
|
||||
name="warning"
|
||||
class="text-warning align-middle"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-gl-tooltip="{ disabled: listItem.destroy_path }"
|
||||
class="d-none d-sm-block"
|
||||
:title="$options.i18n.deleteButtonDisabled"
|
||||
>
|
||||
<gl-deprecated-button
|
||||
ref="deleteImageButton"
|
||||
v-gl-tooltip
|
||||
:disabled="!listItem.destroy_path || listItem.deleting"
|
||||
:title="$options.i18n.removeRepositoryLabel"
|
||||
:aria-label="$options.i18n.removeRepositoryLabel"
|
||||
class="btn-inverted"
|
||||
variant="danger"
|
||||
@click="deleteImage(listItem)"
|
||||
>
|
||||
<gl-icon name="remove" />
|
||||
</gl-deprecated-button>
|
||||
</div>
|
||||
<template v-if="!isEmpty">
|
||||
<div class="gl-display-flex gl-p-1" data-testid="listHeader">
|
||||
<div class="gl-flex-fill-1">
|
||||
<h5>{{ $options.i18n.IMAGE_REPOSITORY_LIST_LABEL }}</h5>
|
||||
</div>
|
||||
<div>
|
||||
<gl-search-box-by-click
|
||||
v-model="search"
|
||||
:placeholder="$options.i18n.SEARCH_PLACEHOLDER_TEXT"
|
||||
@submit="requestImagesList({ name: $event })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<gl-pagination
|
||||
v-model="currentPage"
|
||||
:per-page="pagination.perPage"
|
||||
:total-items="pagination.total"
|
||||
align="center"
|
||||
class="w-100 mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<image-list
|
||||
v-if="images.length"
|
||||
:images="images"
|
||||
:pagination="pagination"
|
||||
@pageChange="requestImagesList({ pagination: { page: $event }, name: search })"
|
||||
@delete="deleteImage"
|
||||
/>
|
||||
|
||||
<gl-empty-state
|
||||
v-else
|
||||
:svg-path="config.noContainersImage"
|
||||
data-testid="emptySearch"
|
||||
:title="$options.i18n.EMPTY_RESULT_TITLE"
|
||||
class="container-message"
|
||||
>
|
||||
<template #description>
|
||||
{{ $options.i18n.EMPTY_RESULT_MESSAGE }}
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
</template>
|
||||
<template v-else>
|
||||
<project-empty-state v-if="!config.isGroupPage" />
|
||||
<group-empty-state v-else />
|
||||
|
|
@ -287,9 +247,9 @@ export default {
|
|||
@ok="handleDeleteImage"
|
||||
@cancel="track('cancel_delete')"
|
||||
>
|
||||
<template #modal-title>{{ $options.i18n.removeRepositoryLabel }}</template>
|
||||
<template #modal-title>{{ $options.i18n.REMOVE_REPOSITORY_LABEL }}</template>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.removeRepositoryModalText">
|
||||
<gl-sprintf :message="$options.i18n.REMOVE_REPOSITORY_MODAL_TEXT">
|
||||
<template #title>
|
||||
<b>{{ itemToDelete.path }}</b>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -23,12 +23,15 @@ export const receiveTagsListSuccess = ({ commit }, { data, headers }) => {
|
|||
commit(types.SET_TAGS_PAGINATION, headers);
|
||||
};
|
||||
|
||||
export const requestImagesList = ({ commit, dispatch, state }, pagination = {}) => {
|
||||
export const requestImagesList = (
|
||||
{ commit, dispatch, state },
|
||||
{ pagination = {}, name = null } = {},
|
||||
) => {
|
||||
commit(types.SET_MAIN_LOADING, true);
|
||||
const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination;
|
||||
|
||||
return axios
|
||||
.get(state.config.endpoint, { params: { page, per_page: perPage } })
|
||||
.get(state.config.endpoint, { params: { page, per_page: perPage, name } })
|
||||
.then(({ data, headers }) => {
|
||||
dispatch('receiveImagesListSuccess', { data, headers });
|
||||
})
|
||||
|
|
|
|||
|
|
@ -69,11 +69,6 @@ $item-weight-max-width: 48px;
|
|||
font-weight: $gl-font-weight-bold;
|
||||
}
|
||||
|
||||
.issue-token-state-icon-open,
|
||||
.issue-token-state-icon-closed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sortable-link {
|
||||
color: $gray-900;
|
||||
font-weight: normal;
|
||||
|
|
@ -92,7 +87,8 @@ $item-weight-max-width: 48px;
|
|||
|
||||
@include media-breakpoint-down(lg) {
|
||||
.issue-count-badge {
|
||||
padding-left: 0;
|
||||
padding: 0;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1317,6 +1317,14 @@ class MergeRequest < ApplicationRecord
|
|||
actual_head_pipeline&.has_reports?(Ci::JobArtifact.terraform_reports)
|
||||
end
|
||||
|
||||
def compare_accessibility_reports
|
||||
unless has_accessibility_reports?
|
||||
return { status: :error, status_reason: _('This merge request does not have accessibility reports') }
|
||||
end
|
||||
|
||||
compare_reports(Ci::CompareAccessibilityReportsService)
|
||||
end
|
||||
|
||||
# TODO: this method and compare_test_reports use the same
|
||||
# result type, which is handled by the controller's #reports_response.
|
||||
# we should minimize mistakes by isolating the common parts.
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
|
|||
can?(current_user, :create_cluster, clusterable)
|
||||
end
|
||||
|
||||
def index_path
|
||||
polymorphic_path([clusterable, :clusters])
|
||||
def index_path(options = {})
|
||||
polymorphic_path([clusterable, :clusters], options)
|
||||
end
|
||||
|
||||
def new_path(options = {})
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ class InstanceClusterablePresenter < ClusterablePresenter
|
|||
end
|
||||
|
||||
override :index_path
|
||||
def index_path
|
||||
admin_clusters_path
|
||||
def index_path(options = {})
|
||||
admin_clusters_path(options)
|
||||
end
|
||||
|
||||
override :new_path
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ class IssuableBaseService < BaseService
|
|||
params.delete(:milestone_id)
|
||||
params.delete(:labels)
|
||||
params.delete(:add_label_ids)
|
||||
params.delete(:add_labels)
|
||||
params.delete(:remove_label_ids)
|
||||
params.delete(:remove_labels)
|
||||
params.delete(:label_ids)
|
||||
params.delete(:assignee_ids)
|
||||
params.delete(:assignee_id)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
= link_to _('More information'), help_page_path('user/group/clusters/index', anchor: 'cluster-precedence')
|
||||
|
||||
- if Feature.enabled?(:clusters_list_redesign)
|
||||
#js-clusters-list-app{ data: { endpoint: 'todo/add/endpoint' } }
|
||||
#js-clusters-list-app{ data: { endpoint: clusterable.index_path(format: :json) } }
|
||||
- else
|
||||
.clusters-table.js-clusters-list
|
||||
.gl-responsive-table-row.table-row-header{ role: "row" }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Add migration to import changes to the system dashboard Prometheus queries
|
||||
into DB
|
||||
merge_request: 31618
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add search bar to container registry image list
|
||||
merge_request: 31322
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Migrate from Vue event hub to Mitt in issuables list
|
||||
merge_request: 31652
|
||||
author: Arun Kumar Mohan
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add ability to add or remove MR labels via API
|
||||
merge_request: 31522
|
||||
author: Lee Tickett
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move Browser-Perfomance-Testing.gitlab-ci.yml to `rules` syntax
|
||||
merge_request: 31413
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChangeVariableInterpolationFormatInCommonMetrics < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
# The import cannot be reversed since we do not know the state that the
|
||||
# common metrics in the PrometheusMetric table were in before the import.
|
||||
end
|
||||
end
|
||||
|
|
@ -13765,5 +13765,6 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200506085748
|
||||
20200506125731
|
||||
20200507221434
|
||||
20200511145545
|
||||
\.
|
||||
|
||||
|
|
|
|||
|
|
@ -1130,6 +1130,8 @@ PUT /projects/:id/merge_requests/:merge_request_iid
|
|||
| `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
|
||||
| `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
|
||||
| `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. |
|
||||
| `add_labels` | string | no | Comma-separated label names to add to a merge request. |
|
||||
| `remove_labels` | string | no | Comma-separated label names to remove from a merge request. |
|
||||
| `description` | string | no | Description of MR. Limited to 1,048,576 characters. |
|
||||
| `state_event` | string | no | New state (close/reopen) |
|
||||
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
|
||||
|
|
|
|||
|
|
@ -188,7 +188,27 @@ Example response:
|
|||
"name": "Administrator",
|
||||
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon"
|
||||
}
|
||||
}
|
||||
},
|
||||
"versions": [
|
||||
{
|
||||
"id":2,
|
||||
"version":"2.0-SNAPSHOT",
|
||||
"created_at":"2020-04-28T04:42:11.573Z",
|
||||
"pipeline": {
|
||||
"id": 234,
|
||||
"status": "pending",
|
||||
"ref": "new-pipeline",
|
||||
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
|
||||
"web_url": "https://example.com/foo/bar/pipelines/58",
|
||||
"created_at": "2016-08-11T11:28:34.085Z",
|
||||
"updated_at": "2016-08-11T11:32:35.169Z",
|
||||
"user": {
|
||||
"name": "Administrator",
|
||||
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ module API
|
|||
assignee_ids
|
||||
description
|
||||
labels
|
||||
add_labels
|
||||
remove_labels
|
||||
milestone_id
|
||||
remove_source_branch
|
||||
state_event
|
||||
|
|
@ -180,6 +182,8 @@ module API
|
|||
optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
|
||||
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
|
||||
optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
|
||||
optional :add_labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
|
||||
optional :remove_labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
|
||||
optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
|
||||
optional :allow_collaboration, type: Boolean, desc: 'Allow commits from members who can merge to the target branch'
|
||||
optional :allow_maintainer_to_push, type: Boolean, as: :allow_collaboration, desc: '[deprecated] See allow_collaboration'
|
||||
|
|
|
|||
|
|
@ -30,11 +30,9 @@ performance:
|
|||
paths:
|
||||
- performance.json
|
||||
- sitespeed-results/
|
||||
only:
|
||||
refs:
|
||||
- branches
|
||||
- tags
|
||||
kubernetes: active
|
||||
except:
|
||||
variables:
|
||||
- $PERFORMANCE_DISABLED
|
||||
rules:
|
||||
- if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
|
||||
when: never
|
||||
- if: '$PERFORMANCE_DISABLED'
|
||||
when: never
|
||||
- if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
|
||||
|
|
|
|||
|
|
@ -5692,12 +5692,18 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Expiration schedule:"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Filter by name"
|
||||
msgstr ""
|
||||
|
||||
msgid "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."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Image ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Image Repositories"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Keep and protect the images that matter most."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -5766,6 +5772,9 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Sorry, your filter produced no results."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Tag"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -5817,6 +5826,9 @@ msgstr ""
|
|||
msgid "ContainerRegistry|This image repository is scheduled for deletion"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|To widen your search, change or remove the filters above."
|
||||
msgstr ""
|
||||
|
||||
msgid "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}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -9308,9 +9320,6 @@ msgstr ""
|
|||
msgid "Filter"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter by %{issuable_type} that are currently archived."
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter by %{issuable_type} that are currently closed."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -9326,6 +9335,12 @@ msgstr ""
|
|||
msgid "Filter by name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter by requirements that are currently archived."
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter by requirements that are currently opened."
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter by status"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -19152,15 +19167,15 @@ msgstr ""
|
|||
msgid "Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %{b_start}will%{b_end} lose access to your account."
|
||||
msgstr ""
|
||||
|
||||
msgid "Show all %{issuable_type}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Show all activity"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show all members"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show all requirements."
|
||||
msgstr ""
|
||||
|
||||
msgid "Show archived projects"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -19535,6 +19550,9 @@ msgstr ""
|
|||
msgid "Something went wrong while fetching related merge requests."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while fetching requirements count."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while fetching requirements list."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -21629,6 +21647,9 @@ msgstr ""
|
|||
msgid "This means you can not push code until you create an empty repository or import existing one."
|
||||
msgstr ""
|
||||
|
||||
msgid "This merge request does not have accessibility reports"
|
||||
msgstr ""
|
||||
|
||||
msgid "This merge request is locked."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -22263,15 +22284,9 @@ msgstr ""
|
|||
msgid "Total artifacts size: %{total_size}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Total cores (vCPUs)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Total issues"
|
||||
msgstr ""
|
||||
|
||||
msgid "Total memory (GB)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Total test time for all commits/merges"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,46 +1,63 @@
|
|||
import Vuex from 'vuex';
|
||||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import { GlTable, GlLoadingIcon } from '@gitlab/ui';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import Clusters from '~/clusters_list/components/clusters.vue';
|
||||
import mockData from '../mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
import ClusterStore from '~/clusters_list/store';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { apiData } from '../mock_data';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { GlTable, GlLoadingIcon } from '@gitlab/ui';
|
||||
|
||||
describe('Clusters', () => {
|
||||
let mock;
|
||||
let store;
|
||||
let wrapper;
|
||||
|
||||
const findTable = () => wrapper.find(GlTable);
|
||||
const endpoint = 'some/endpoint';
|
||||
|
||||
const findLoader = () => wrapper.find(GlLoadingIcon);
|
||||
const findTable = () => wrapper.find(GlTable);
|
||||
const findStatuses = () => findTable().findAll('.js-status');
|
||||
|
||||
const mountComponent = _state => {
|
||||
const state = { clusters: mockData, endpoint: 'some/endpoint', ..._state };
|
||||
const store = new Vuex.Store({
|
||||
state,
|
||||
});
|
||||
const mockPollingApi = (response, body, header) => {
|
||||
mock.onGet(endpoint).reply(response, body, header);
|
||||
};
|
||||
|
||||
wrapper = mount(Clusters, { localVue, store });
|
||||
const mountWrapper = () => {
|
||||
store = ClusterStore({ endpoint });
|
||||
wrapper = mount(Clusters, { store });
|
||||
return axios.waitForAll();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mountComponent({ loading: false });
|
||||
mock = new MockAdapter(axios);
|
||||
mockPollingApi(200, apiData, {});
|
||||
|
||||
return mountWrapper();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('clusters table', () => {
|
||||
it('displays a loader instead of the table while loading', () => {
|
||||
mountComponent({ loading: true });
|
||||
expect(findLoader().exists()).toBe(true);
|
||||
expect(findTable().exists()).toBe(false);
|
||||
describe('when data is loading', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.vm.$store.state.loading = true;
|
||||
return wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('displays a loader instead of the table while loading', () => {
|
||||
expect(findLoader().exists()).toBe(true);
|
||||
expect(findTable().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays a table component', () => {
|
||||
expect(findTable().exists()).toBe(true);
|
||||
expect(findTable().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the correct table headers', () => {
|
||||
const tableHeaders = wrapper.vm.$options.fields;
|
||||
const tableHeaders = wrapper.vm.fields;
|
||||
const headers = findTable().findAll('th');
|
||||
|
||||
expect(headers.length).toBe(tableHeaders.length);
|
||||
|
|
@ -62,7 +79,8 @@ describe('Clusters', () => {
|
|||
${'unreachable'} | ${'bg-danger'} | ${1}
|
||||
${'authentication_failure'} | ${'bg-warning'} | ${2}
|
||||
${'deleting'} | ${null} | ${3}
|
||||
${'connected'} | ${'bg-success'} | ${4}
|
||||
${'created'} | ${'bg-success'} | ${4}
|
||||
${'default'} | ${'bg-white'} | ${5}
|
||||
`('renders a status for each cluster', ({ statusName, className, lineNumber }) => {
|
||||
const statuses = findStatuses();
|
||||
const status = statuses.at(lineNumber);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export default [
|
||||
export const clusterList = [
|
||||
{
|
||||
name: 'My Cluster 1',
|
||||
environmentScope: '*',
|
||||
|
|
@ -40,8 +40,22 @@ export default [
|
|||
environmentScope: 'development',
|
||||
size: '12',
|
||||
clusterType: 'project_type',
|
||||
status: 'connected',
|
||||
status: 'created',
|
||||
cpu: '6 (100% free)',
|
||||
memory: '20.12 (35% free)',
|
||||
},
|
||||
{
|
||||
name: 'My Cluster 6',
|
||||
environmentScope: '*',
|
||||
size: '1',
|
||||
clusterType: 'project_type',
|
||||
status: 'cleanup_ongoing',
|
||||
cpu: '6 (100% free)',
|
||||
memory: '20.12 (35% free)',
|
||||
},
|
||||
];
|
||||
|
||||
export const apiData = {
|
||||
clusters: clusterList,
|
||||
has_ancestor_clusters: false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
|
|||
import flashError from '~/flash';
|
||||
import testAction from 'helpers/vuex_action_helper';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { apiData } from '../mock_data';
|
||||
import * as types from '~/clusters_list/store/mutation_types';
|
||||
import * as actions from '~/clusters_list/store/actions';
|
||||
|
||||
|
|
@ -10,8 +11,6 @@ jest.mock('~/flash.js');
|
|||
describe('Clusters store actions', () => {
|
||||
describe('fetchClusters', () => {
|
||||
let mock;
|
||||
const endpoint = '/clusters';
|
||||
const clusters = [{ name: 'test' }];
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
|
|
@ -20,14 +19,14 @@ describe('Clusters store actions', () => {
|
|||
afterEach(() => mock.restore());
|
||||
|
||||
it('should commit SET_CLUSTERS_DATA with received response', done => {
|
||||
mock.onGet().reply(200, clusters);
|
||||
mock.onGet().reply(200, apiData);
|
||||
|
||||
testAction(
|
||||
actions.fetchClusters,
|
||||
{ endpoint },
|
||||
{ endpoint: apiData.endpoint },
|
||||
{},
|
||||
[
|
||||
{ type: types.SET_CLUSTERS_DATA, payload: clusters },
|
||||
{ type: types.SET_CLUSTERS_DATA, payload: apiData },
|
||||
{ type: types.SET_LOADING_STATE, payload: false },
|
||||
],
|
||||
[],
|
||||
|
|
@ -38,7 +37,7 @@ describe('Clusters store actions', () => {
|
|||
it('should show flash on API error', done => {
|
||||
mock.onGet().reply(400, 'Not Found');
|
||||
|
||||
testAction(actions.fetchClusters, { endpoint }, {}, [], [], () => {
|
||||
testAction(actions.fetchClusters, { endpoint: apiData.endpoint }, {}, [], [], () => {
|
||||
expect(flashError).toHaveBeenCalledWith(expect.stringMatching('error'));
|
||||
done();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,8 +22,9 @@ describe('Tracking Events', () => {
|
|||
label: eventName,
|
||||
value: {
|
||||
'internal-object-refrerer': '',
|
||||
'version-number': 1,
|
||||
'current-version': false,
|
||||
'design-collection-owner': '',
|
||||
'design-version-number': 1,
|
||||
'design-is-current-version': false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
@ -32,7 +33,7 @@ describe('Tracking Events', () => {
|
|||
it('trackDesignDetailView allows to customize the value payload', () => {
|
||||
const trackingSpy = getTrackingSpy(eventKey);
|
||||
|
||||
trackDesignDetailView('from-a-test', 100, true);
|
||||
trackDesignDetailView('from-a-test', 'test', 100, true);
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith(
|
||||
eventKey,
|
||||
|
|
@ -41,8 +42,9 @@ describe('Tracking Events', () => {
|
|||
label: eventName,
|
||||
value: {
|
||||
'internal-object-refrerer': 'from-a-test',
|
||||
'version-number': 100,
|
||||
'current-version': true,
|
||||
'design-collection-owner': 'test',
|
||||
'design-version-number': 100,
|
||||
'design-is-current-version': true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlPagination } from '@gitlab/ui';
|
||||
import Component from '~/registry/explorer/components/image_list.vue';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import { RouterLink } from '../stubs';
|
||||
import { imagesListResponse, imagePagination } from '../mock_data';
|
||||
|
||||
describe('Image List', () => {
|
||||
let wrapper;
|
||||
|
||||
const firstElement = imagesListResponse.data[0];
|
||||
|
||||
const findDeleteBtn = () => wrapper.find('[data-testid="deleteImageButton"]');
|
||||
const findRowItems = () => wrapper.findAll('[data-testid="rowItem"]');
|
||||
const findDetailsLink = () => wrapper.find('[data-testid="detailsLink"]');
|
||||
const findClipboardButton = () => wrapper.find(ClipboardButton);
|
||||
const findPagination = () => wrapper.find(GlPagination);
|
||||
|
||||
const mountComponent = () => {
|
||||
wrapper = shallowMount(Component, {
|
||||
stubs: {
|
||||
RouterLink,
|
||||
},
|
||||
propsData: {
|
||||
images: imagesListResponse.data,
|
||||
pagination: imagePagination,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it('contains one list element for each image', () => {
|
||||
expect(findRowItems().length).toBe(imagesListResponse.data.length);
|
||||
});
|
||||
|
||||
it('contains a link to the details page', () => {
|
||||
const link = findDetailsLink();
|
||||
expect(link.html()).toContain(firstElement.path);
|
||||
expect(link.props('to').name).toBe('details');
|
||||
});
|
||||
|
||||
it('contains a clipboard button', () => {
|
||||
const button = findClipboardButton();
|
||||
expect(button.exists()).toBe(true);
|
||||
expect(button.props('text')).toBe(firstElement.location);
|
||||
expect(button.props('title')).toBe(firstElement.location);
|
||||
});
|
||||
|
||||
it('should be possible to delete a repo', () => {
|
||||
const deleteBtn = findDeleteBtn();
|
||||
expect(deleteBtn.exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
it('exists', () => {
|
||||
expect(findPagination().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('is wired to the correct pagination props', () => {
|
||||
const pagination = findPagination();
|
||||
expect(pagination.props('perPage')).toBe(imagePagination.perPage);
|
||||
expect(pagination.props('totalItems')).toBe(imagePagination.total);
|
||||
expect(pagination.props('value')).toBe(imagePagination.page);
|
||||
});
|
||||
|
||||
it('emits a pageChange event when the page change', () => {
|
||||
wrapper.setData({ currentPage: 2 });
|
||||
expect(wrapper.emitted('pageChange')).toEqual([[2]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -87,3 +87,11 @@ export const tagsListResponse = {
|
|||
],
|
||||
headers,
|
||||
};
|
||||
|
||||
export const imagePagination = {
|
||||
perPage: 10,
|
||||
page: 1,
|
||||
total: 14,
|
||||
totalPages: 2,
|
||||
nextPage: 2,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlPagination, GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui';
|
||||
import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitlab/ui';
|
||||
import Tracking from '~/tracking';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import component from '~/registry/explorer/pages/list.vue';
|
||||
import QuickstartDropdown from '~/registry/explorer/components/quickstart_dropdown.vue';
|
||||
import GroupEmptyState from '~/registry/explorer/components/group_empty_state.vue';
|
||||
import ProjectEmptyState from '~/registry/explorer/components/project_empty_state.vue';
|
||||
import ProjectPolicyAlert from '~/registry/explorer/components/project_policy_alert.vue';
|
||||
import ImageList from '~/registry/explorer/components/image_list.vue';
|
||||
import { createStore } from '~/registry/explorer/stores/';
|
||||
import {
|
||||
SET_MAIN_LOADING,
|
||||
|
|
@ -16,9 +18,11 @@ import {
|
|||
import {
|
||||
DELETE_IMAGE_SUCCESS_MESSAGE,
|
||||
DELETE_IMAGE_ERROR_MESSAGE,
|
||||
IMAGE_REPOSITORY_LIST_LABEL,
|
||||
SEARCH_PLACEHOLDER_TEXT,
|
||||
} from '~/registry/explorer/constants';
|
||||
import { imagesListResponse } from '../mock_data';
|
||||
import { GlModal, GlEmptyState, RouterLink } from '../stubs';
|
||||
import { GlModal, GlEmptyState } from '../stubs';
|
||||
import { $toast } from '../../shared/mocks';
|
||||
|
||||
describe('List Page', () => {
|
||||
|
|
@ -26,20 +30,21 @@ describe('List Page', () => {
|
|||
let dispatchSpy;
|
||||
let store;
|
||||
|
||||
const findDeleteBtn = () => wrapper.find({ ref: 'deleteImageButton' });
|
||||
const findDeleteModal = () => wrapper.find(GlModal);
|
||||
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
|
||||
const findImagesList = () => wrapper.find({ ref: 'imagesList' });
|
||||
const findRowItems = () => wrapper.findAll({ ref: 'rowItem' });
|
||||
|
||||
const findEmptyState = () => wrapper.find(GlEmptyState);
|
||||
const findDetailsLink = () => wrapper.find({ ref: 'detailsLink' });
|
||||
const findClipboardButton = () => wrapper.find({ ref: 'clipboardButton' });
|
||||
const findPagination = () => wrapper.find(GlPagination);
|
||||
|
||||
const findQuickStartDropdown = () => wrapper.find(QuickstartDropdown);
|
||||
const findProjectEmptyState = () => wrapper.find(ProjectEmptyState);
|
||||
const findGroupEmptyState = () => wrapper.find(GroupEmptyState);
|
||||
const findProjectPolicyAlert = () => wrapper.find(ProjectPolicyAlert);
|
||||
const findDeleteAlert = () => wrapper.find(GlAlert);
|
||||
const findImageList = () => wrapper.find(ImageList);
|
||||
const findListHeader = () => wrapper.find('[data-testid="listHeader"]');
|
||||
const findSearchBox = () => wrapper.find(GlSearchBoxByClick);
|
||||
const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
|
||||
|
||||
const mountComponent = ({ mocks } = {}) => {
|
||||
wrapper = shallowMount(component, {
|
||||
|
|
@ -48,7 +53,6 @@ describe('List Page', () => {
|
|||
GlModal,
|
||||
GlEmptyState,
|
||||
GlSprintf,
|
||||
RouterLink,
|
||||
},
|
||||
mocks: {
|
||||
$toast,
|
||||
|
|
@ -164,6 +168,7 @@ describe('List Page', () => {
|
|||
beforeEach(() => {
|
||||
store.commit(SET_IMAGES_LIST_SUCCESS, []);
|
||||
mountComponent();
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('quick start is not visible', () => {
|
||||
|
|
@ -191,54 +196,39 @@ describe('List Page', () => {
|
|||
it('quick start is not visible', () => {
|
||||
expect(findQuickStartDropdown().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('list header is not visible', () => {
|
||||
expect(findListHeader().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('list is not empty', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it('quick start is visible', () => {
|
||||
expect(findQuickStartDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('listElement', () => {
|
||||
let listElements;
|
||||
let firstElement;
|
||||
|
||||
describe('unfiltered state', () => {
|
||||
beforeEach(() => {
|
||||
listElements = findRowItems();
|
||||
[firstElement] = store.state.images;
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it('contains one list element for each image', () => {
|
||||
expect(listElements.length).toBe(store.state.images.length);
|
||||
it('quick start is visible', () => {
|
||||
expect(findQuickStartDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('contains a link to the details page', () => {
|
||||
const link = findDetailsLink();
|
||||
expect(link.html()).toContain(firstElement.path);
|
||||
expect(link.props('to').name).toBe('details');
|
||||
it('list component is visible', () => {
|
||||
expect(findImageList().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('contains a clipboard button', () => {
|
||||
const button = findClipboardButton();
|
||||
expect(button.exists()).toBe(true);
|
||||
expect(button.props('text')).toBe(firstElement.location);
|
||||
expect(button.props('title')).toBe(firstElement.location);
|
||||
it('list header is visible', () => {
|
||||
const header = findListHeader();
|
||||
expect(header.exists()).toBe(true);
|
||||
expect(header.text()).toBe(IMAGE_REPOSITORY_LIST_LABEL);
|
||||
});
|
||||
|
||||
describe('delete image', () => {
|
||||
it('should be possible to delete a repo', () => {
|
||||
const deleteBtn = findDeleteBtn();
|
||||
expect(deleteBtn.exists()).toBe(true);
|
||||
});
|
||||
|
||||
const itemToDelete = { path: 'bar' };
|
||||
it('should call deleteItem when confirming deletion', () => {
|
||||
dispatchSpy.mockResolvedValue();
|
||||
findDeleteBtn().vm.$emit('click');
|
||||
expect(wrapper.vm.itemToDelete).not.toEqual({});
|
||||
findImageList().vm.$emit('delete', itemToDelete);
|
||||
expect(wrapper.vm.itemToDelete).toEqual(itemToDelete);
|
||||
findDeleteModal().vm.$emit('ok');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
'requestDeleteImage',
|
||||
|
|
@ -248,8 +238,8 @@ describe('List Page', () => {
|
|||
|
||||
it('should show a success alert when delete request is successful', () => {
|
||||
dispatchSpy.mockResolvedValue();
|
||||
findDeleteBtn().vm.$emit('click');
|
||||
expect(wrapper.vm.itemToDelete).not.toEqual({});
|
||||
findImageList().vm.$emit('delete', itemToDelete);
|
||||
expect(wrapper.vm.itemToDelete).toEqual(itemToDelete);
|
||||
return wrapper.vm.handleDeleteImage().then(() => {
|
||||
const alert = findDeleteAlert();
|
||||
expect(alert.exists()).toBe(true);
|
||||
|
|
@ -261,8 +251,8 @@ describe('List Page', () => {
|
|||
|
||||
it('should show an error alert when delete request fails', () => {
|
||||
dispatchSpy.mockRejectedValue();
|
||||
findDeleteBtn().vm.$emit('click');
|
||||
expect(wrapper.vm.itemToDelete).not.toEqual({});
|
||||
findImageList().vm.$emit('delete', itemToDelete);
|
||||
expect(wrapper.vm.itemToDelete).toEqual(itemToDelete);
|
||||
return wrapper.vm.handleDeleteImage().then(() => {
|
||||
const alert = findDeleteAlert();
|
||||
expect(alert.exists()).toBe(true);
|
||||
|
|
@ -272,71 +262,93 @@ describe('List Page', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
it('exists', () => {
|
||||
expect(findPagination().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('is wired to the correct pagination props', () => {
|
||||
const pagination = findPagination();
|
||||
expect(pagination.props('perPage')).toBe(store.state.pagination.perPage);
|
||||
expect(pagination.props('totalItems')).toBe(store.state.pagination.total);
|
||||
expect(pagination.props('value')).toBe(store.state.pagination.page);
|
||||
});
|
||||
|
||||
it('fetch the data from the API when the v-model changes', () => {
|
||||
dispatchSpy.mockReturnValue();
|
||||
wrapper.setData({ currentPage: 2 });
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', { page: 2 });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('modal', () => {
|
||||
it('exists', () => {
|
||||
expect(findDeleteModal().exists()).toBe(true);
|
||||
describe('search', () => {
|
||||
it('has a search box element', () => {
|
||||
mountComponent();
|
||||
const searchBox = findSearchBox();
|
||||
expect(searchBox.exists()).toBe(true);
|
||||
expect(searchBox.attributes('placeholder')).toBe(SEARCH_PLACEHOLDER_TEXT);
|
||||
});
|
||||
|
||||
it('contains a description with the path of the item to delete', () => {
|
||||
wrapper.setData({ itemToDelete: { path: 'foo' } });
|
||||
it('performs a search', () => {
|
||||
mountComponent();
|
||||
findSearchBox().vm.$emit('submit', 'foo');
|
||||
expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', {
|
||||
name: 'foo',
|
||||
});
|
||||
});
|
||||
|
||||
it('when search result is empty displays an empty search message', () => {
|
||||
mountComponent();
|
||||
store.commit(SET_IMAGES_LIST_SUCCESS, []);
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findDeleteModal().html()).toContain('foo');
|
||||
expect(findEmptySearchMessage().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tracking', () => {
|
||||
const testTrackingCall = action => {
|
||||
expect(Tracking.event).toHaveBeenCalledWith(undefined, action, {
|
||||
label: 'registry_repository_delete',
|
||||
describe('pagination', () => {
|
||||
it('pageChange event triggers the appropriate store function', () => {
|
||||
mountComponent();
|
||||
findImageList().vm.$emit('pageChange', 2);
|
||||
expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', {
|
||||
pagination: { page: 2 },
|
||||
name: wrapper.vm.search,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Tracking, 'event');
|
||||
dispatchSpy.mockResolvedValue();
|
||||
});
|
||||
|
||||
it('send an event when delete button is clicked', () => {
|
||||
const deleteBtn = findDeleteBtn();
|
||||
deleteBtn.vm.$emit('click');
|
||||
testTrackingCall('click_button');
|
||||
});
|
||||
|
||||
it('send an event when cancel is pressed on modal', () => {
|
||||
const deleteModal = findDeleteModal();
|
||||
deleteModal.vm.$emit('cancel');
|
||||
testTrackingCall('cancel_delete');
|
||||
});
|
||||
|
||||
it('send an event when confirm is clicked on modal', () => {
|
||||
const deleteModal = findDeleteModal();
|
||||
deleteModal.vm.$emit('ok');
|
||||
testTrackingCall('confirm_delete');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('modal', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it('exists', () => {
|
||||
expect(findDeleteModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('contains a description with the path of the item to delete', () => {
|
||||
wrapper.setData({ itemToDelete: { path: 'foo' } });
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findDeleteModal().html()).toContain('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tracking', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
const testTrackingCall = action => {
|
||||
expect(Tracking.event).toHaveBeenCalledWith(undefined, action, {
|
||||
label: 'registry_repository_delete',
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Tracking, 'event');
|
||||
dispatchSpy.mockResolvedValue();
|
||||
});
|
||||
|
||||
it('send an event when delete button is clicked', () => {
|
||||
findImageList().vm.$emit('delete', {});
|
||||
testTrackingCall('click_button');
|
||||
});
|
||||
|
||||
it('send an event when cancel is pressed on modal', () => {
|
||||
const deleteModal = findDeleteModal();
|
||||
deleteModal.vm.$emit('cancel');
|
||||
testTrackingCall('cancel_delete');
|
||||
});
|
||||
|
||||
it('send an event when confirm is clicked on modal', () => {
|
||||
const deleteModal = findDeleteModal();
|
||||
deleteModal.vm.$emit('ok');
|
||||
testTrackingCall('confirm_delete');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -920,8 +920,8 @@ describe('ReadyToMerge', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Commit message area', () => {
|
||||
describe('when using merge commits', () => {
|
||||
describe('Merge request project settings', () => {
|
||||
describe('when the merge commit merge method is enabled', () => {
|
||||
beforeEach(() => {
|
||||
vm = createComponent({
|
||||
mr: { ffOnlyEnabled: false },
|
||||
|
|
@ -937,7 +937,7 @@ describe('ReadyToMerge', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when fast-forward merge is enabled', () => {
|
||||
describe('when the fast-forward merge method is enabled', () => {
|
||||
beforeEach(() => {
|
||||
vm = createComponent({
|
||||
mr: { ffOnlyEnabled: true },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'Jobs/Browser-Performance-Testing.gitlab-ci.yml' do
|
||||
subject(:template) do
|
||||
<<~YAML
|
||||
stages:
|
||||
- test
|
||||
- performance
|
||||
|
||||
include:
|
||||
- template: 'Jobs/Browser-Performance-Testing.gitlab-ci.yml'
|
||||
|
||||
placeholder:
|
||||
script:
|
||||
- keep pipeline validator happy by having a job when stages are intentionally empty
|
||||
YAML
|
||||
end
|
||||
|
||||
describe 'the created pipeline' do
|
||||
let(:user) { create(:admin) }
|
||||
let(:project) do
|
||||
create(:project, :repository, variables: [
|
||||
build(:ci_variable, key: 'CI_KUBERNETES_ACTIVE', value: 'true')
|
||||
])
|
||||
end
|
||||
|
||||
let(:default_branch) { 'master' }
|
||||
let(:pipeline_ref) { default_branch }
|
||||
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
|
||||
let(:pipeline) { service.execute!(:push) }
|
||||
let(:build_names) { pipeline.builds.pluck(:name) }
|
||||
|
||||
before do
|
||||
stub_ci_pipeline_yaml_file(template)
|
||||
|
||||
allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
|
||||
allow(project).to receive(:default_branch).and_return(default_branch)
|
||||
end
|
||||
|
||||
it 'has no errors' do
|
||||
expect(pipeline.errors).to be_empty
|
||||
end
|
||||
|
||||
shared_examples_for 'performance job on tag or branch' do
|
||||
it 'by default' do
|
||||
expect(build_names).to include('performance')
|
||||
end
|
||||
|
||||
it 'when PERFORMANCE_DISABLED' do
|
||||
create(:ci_variable, project: project, key: 'PERFORMANCE_DISABLED', value: '1')
|
||||
|
||||
expect(build_names).not_to include('performance')
|
||||
end
|
||||
end
|
||||
|
||||
context 'on master' do
|
||||
it_behaves_like 'performance job on tag or branch'
|
||||
end
|
||||
|
||||
context 'on another branch' do
|
||||
let(:pipeline_ref) { 'feature' }
|
||||
|
||||
it_behaves_like 'performance job on tag or branch'
|
||||
end
|
||||
|
||||
context 'on tag' do
|
||||
let(:pipeline_ref) { 'v1.0.0' }
|
||||
|
||||
it_behaves_like 'performance job on tag or branch'
|
||||
end
|
||||
|
||||
context 'on merge request' do
|
||||
let(:service) { MergeRequests::CreatePipelineService.new(project, user) }
|
||||
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
|
||||
let(:pipeline) { service.execute(merge_request) }
|
||||
|
||||
it 'has no jobs' do
|
||||
expect(pipeline).to be_merge_request_event
|
||||
expect(build_names).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require Rails.root.join('db', 'post_migrate', '20200511145545_change_variable_interpolation_format_in_common_metrics')
|
||||
|
||||
describe ChangeVariableInterpolationFormatInCommonMetrics, :migration do
|
||||
let(:prometheus_metrics) { table(:prometheus_metrics) }
|
||||
|
||||
let!(:common_metric) do
|
||||
prometheus_metrics.create!(
|
||||
identifier: 'system_metrics_kubernetes_container_memory_total',
|
||||
query: 'avg(sum(container_memory_usage_bytes{container_name!="POD",' \
|
||||
'pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"})' \
|
||||
' by (job)) without (job) /1024/1024/1024',
|
||||
project_id: nil,
|
||||
title: 'Memory Usage (Total)',
|
||||
y_label: 'Total Memory Used (GB)',
|
||||
unit: 'GB',
|
||||
legend: 'Total (GB)',
|
||||
group: -5,
|
||||
common: true
|
||||
)
|
||||
end
|
||||
|
||||
it 'updates query to use {{}}' do
|
||||
expected_query = 'avg(sum(container_memory_usage_bytes{container_name!="POD",' \
|
||||
'pod_name=~"^{{ci_environment_slug}}-(.*)",namespace="{{kube_namespace}}"})' \
|
||||
' by (job)) without (job) /1024/1024/1024'
|
||||
|
||||
migrate!
|
||||
|
||||
expect(common_metric.reload.query).to eq(expected_query)
|
||||
end
|
||||
end
|
||||
|
|
@ -1889,6 +1889,62 @@ describe MergeRequest do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#compare_accessibility_reports' do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:merge_request, reload: true) { create(:merge_request, :with_accessibility_reports, source_project: project) }
|
||||
let_it_be(:pipeline) { merge_request.head_pipeline }
|
||||
|
||||
subject { merge_request.compare_accessibility_reports }
|
||||
|
||||
context 'when head pipeline has accessibility reports' do
|
||||
let(:job) do
|
||||
create(:ci_build, options: { artifacts: { reports: { pa11y: ['accessibility.json'] } } }, pipeline: pipeline)
|
||||
end
|
||||
|
||||
let(:artifacts_metadata) { create(:ci_job_artifact, :metadata, job: job) }
|
||||
|
||||
context 'when reactive cache worker is parsing results asynchronously' do
|
||||
it 'returns parsing status' do
|
||||
expect(subject[:status]).to eq(:parsing)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when reactive cache worker is inline' do
|
||||
before do
|
||||
synchronous_reactive_cache(merge_request)
|
||||
end
|
||||
|
||||
it 'returns parsed status' do
|
||||
expect(subject[:status]).to eq(:parsed)
|
||||
expect(subject[:data]).to be_present
|
||||
end
|
||||
|
||||
context 'when an error occurrs' do
|
||||
before do
|
||||
merge_request.update!(head_pipeline: nil)
|
||||
end
|
||||
|
||||
it 'returns an error status' do
|
||||
expect(subject[:status]).to eq(:error)
|
||||
expect(subject[:status_reason]).to eq("This merge request does not have accessibility reports")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when cached result is not latest' do
|
||||
before do
|
||||
allow_next_instance_of(Ci::CompareAccessibilityReportsService) do |service|
|
||||
allow(service).to receive(:latest?).and_return(false)
|
||||
end
|
||||
end
|
||||
|
||||
it 'raises an InvalidateReactiveCache error' do
|
||||
expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#all_commit_shas' do
|
||||
context 'when merge request is persisted' do
|
||||
let(:all_commit_shas) do
|
||||
|
|
|
|||
|
|
@ -87,4 +87,20 @@ describe ClusterablePresenter do
|
|||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
describe '#index_path' do
|
||||
let(:clusterable) { create(:group) }
|
||||
|
||||
context 'without options' do
|
||||
subject { described_class.new(clusterable).index_path }
|
||||
|
||||
it { is_expected.to eq(group_clusters_path(clusterable)) }
|
||||
end
|
||||
|
||||
context 'with options' do
|
||||
subject { described_class.new(clusterable).index_path(format: :json) }
|
||||
|
||||
it { is_expected.to eq(group_clusters_path(clusterable, format: :json)) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2308,6 +2308,33 @@ describe API::MergeRequests do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with labels' do
|
||||
include_context 'with labels'
|
||||
|
||||
let(:api_base) { api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) }
|
||||
|
||||
it 'when adding labels, keeps existing labels and adds new' do
|
||||
put api_base, params: { add_labels: '1, 2' }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['labels']).to contain_exactly(label.title, label2.title, '1', '2')
|
||||
end
|
||||
|
||||
it 'when removing labels, only removes those specified' do
|
||||
put api_base, params: { remove_labels: "#{label.title}" }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['labels']).to eq([label2.title])
|
||||
end
|
||||
|
||||
it 'when removing all labels, keeps no labels' do
|
||||
put api_base, params: { remove_labels: "#{label.title}, #{label2.title}" }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['labels']).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not update state when title is empty' do
|
||||
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: { state_event: 'close', title: nil }
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue