Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e672c333df
commit
f4199ade27
|
@ -187,6 +187,9 @@ setup-test-env:
|
|||
- .setup-test-env-cache
|
||||
- .rails:rules:setup-test-env
|
||||
stage: prepare
|
||||
needs:
|
||||
- job: "update-gitaly-binaries-cache"
|
||||
optional: true
|
||||
variables:
|
||||
SETUP_DB: "false"
|
||||
script:
|
||||
|
|
|
@ -1 +1 @@
|
|||
13.24.1
|
||||
13.24.2
|
||||
|
|
|
@ -234,7 +234,7 @@ const Api = {
|
|||
|
||||
return axios
|
||||
.get(url, {
|
||||
params: Object.assign(defaults, options),
|
||||
params: { ...defaults, ...options },
|
||||
})
|
||||
.then(({ data, headers }) => {
|
||||
callback(data);
|
||||
|
@ -445,7 +445,7 @@ const Api = {
|
|||
},
|
||||
|
||||
// Return group projects list. Filtered by query
|
||||
groupProjects(groupId, query, options, callback) {
|
||||
groupProjects(groupId, query, options, callback = () => {}, useCustomErrorHandler = false) {
|
||||
const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
|
||||
const defaults = {
|
||||
search: query,
|
||||
|
@ -455,14 +455,21 @@ const Api = {
|
|||
.get(url, {
|
||||
params: { ...defaults, ...options },
|
||||
})
|
||||
.then(({ data }) => (callback ? callback(data) : data))
|
||||
.catch(() => {
|
||||
.then(({ data, headers }) => {
|
||||
callback(data);
|
||||
|
||||
return { data, headers };
|
||||
})
|
||||
.catch((error) => {
|
||||
if (useCustomErrorHandler) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
createFlash({
|
||||
message: __('Something went wrong while fetching projects'),
|
||||
});
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -70,8 +70,41 @@ const memoizedLocale = () => {
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers timeago time duration
|
||||
*/
|
||||
const memoizedLocaleDuration = () => {
|
||||
const cache = [];
|
||||
|
||||
const durations = [
|
||||
() => [s__('Duration|%s seconds')],
|
||||
() => [s__('Duration|%s seconds')],
|
||||
() => [s__('Duration|1 minute')],
|
||||
() => [s__('Duration|%s minutes')],
|
||||
() => [s__('Duration|1 hour')],
|
||||
() => [s__('Duration|%s hours')],
|
||||
() => [s__('Duration|1 day')],
|
||||
() => [s__('Duration|%s days')],
|
||||
() => [s__('Duration|1 week')],
|
||||
() => [s__('Duration|%s weeks')],
|
||||
() => [s__('Duration|1 month')],
|
||||
() => [s__('Duration|%s months')],
|
||||
() => [s__('Duration|1 year')],
|
||||
() => [s__('Duration|%s years')],
|
||||
];
|
||||
|
||||
return (_, index) => {
|
||||
if (cache[index]) {
|
||||
return cache[index];
|
||||
}
|
||||
cache[index] = durations[index] && durations[index]();
|
||||
return cache[index];
|
||||
};
|
||||
};
|
||||
|
||||
timeago.register(timeagoLanguageCode, memoizedLocale());
|
||||
timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining());
|
||||
timeago.register(`${timeagoLanguageCode}-duration`, memoizedLocaleDuration());
|
||||
|
||||
let memoizedFormatter = null;
|
||||
|
||||
|
@ -133,3 +166,16 @@ export const timeFor = (time, expiredLabel) => {
|
|||
}
|
||||
return timeago.format(time, `${timeagoLanguageCode}-remaining`).trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a duration of time given an amount.
|
||||
*
|
||||
* @param {number} milliseconds - Duration in milliseconds.
|
||||
* @returns {string} A formatted duration, e.g. "10 minutes".
|
||||
*/
|
||||
export const duration = (milliseconds) => {
|
||||
const now = new Date();
|
||||
return timeago
|
||||
.format(now.getTime() - Math.abs(milliseconds), `${timeagoLanguageCode}-duration`)
|
||||
.trim();
|
||||
};
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<script>
|
||||
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
|
||||
import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue';
|
||||
|
||||
export default {
|
||||
name: 'HarborList',
|
||||
components: {
|
||||
RegistryList,
|
||||
HarborListRow,
|
||||
},
|
||||
props: {
|
||||
images: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
metadataLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
pageInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<registry-list
|
||||
:items="images"
|
||||
:hidden-delete="true"
|
||||
:pagination="pageInfo"
|
||||
id-property="name"
|
||||
@prev-page="$emit('prev-page')"
|
||||
@next-page="$emit('next-page')"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<harbor-list-row :item="item" :metadata-loading="metadataLoading" />
|
||||
</template>
|
||||
</registry-list>
|
||||
</template>
|
|
@ -0,0 +1,67 @@
|
|||
<script>
|
||||
import { sprintf } from '~/locale';
|
||||
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||
import {
|
||||
HARBOR_REGISTRY_TITLE,
|
||||
LIST_INTRO_TEXT,
|
||||
imagesCountInfoText,
|
||||
} from '~/packages_and_registries/harbor_registry/constants';
|
||||
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
|
||||
|
||||
export default {
|
||||
name: 'HarborListHeader',
|
||||
components: {
|
||||
TitleArea,
|
||||
MetadataItem,
|
||||
},
|
||||
props: {
|
||||
imagesCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: false,
|
||||
},
|
||||
helpPagePath: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
metadataLoading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
HARBOR_REGISTRY_TITLE,
|
||||
},
|
||||
computed: {
|
||||
imagesCountText() {
|
||||
const pluralisedString = imagesCountInfoText(this.imagesCount);
|
||||
return sprintf(pluralisedString, { count: this.imagesCount });
|
||||
},
|
||||
infoMessages() {
|
||||
return [{ text: LIST_INTRO_TEXT, link: this.helpPagePath }];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<title-area
|
||||
:title="$options.i18n.HARBOR_REGISTRY_TITLE"
|
||||
:info-messages="infoMessages"
|
||||
:metadata-loading="metadataLoading"
|
||||
>
|
||||
<template #right-actions>
|
||||
<slot name="commands"></slot>
|
||||
</template>
|
||||
<template #metadata-count>
|
||||
<metadata-item
|
||||
v-if="imagesCount"
|
||||
data-testid="images-count"
|
||||
icon="container-image"
|
||||
:text="imagesCountText"
|
||||
/>
|
||||
</template>
|
||||
</title-area>
|
||||
</template>
|
|
@ -0,0 +1,84 @@
|
|||
<script>
|
||||
import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
|
||||
import { n__ } from '~/locale';
|
||||
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import ListItem from '~/vue_shared/components/registry/list_item.vue';
|
||||
|
||||
export default {
|
||||
name: 'HarborListRow',
|
||||
components: {
|
||||
ClipboardButton,
|
||||
GlSprintf,
|
||||
GlIcon,
|
||||
ListItem,
|
||||
GlSkeletonLoader,
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
metadataLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
id() {
|
||||
return this.item.id;
|
||||
},
|
||||
artifactCountText() {
|
||||
return n__(
|
||||
'HarborRegistry|%{count} Tag',
|
||||
'HarborRegistry|%{count} Tags',
|
||||
this.item.artifactCount,
|
||||
);
|
||||
},
|
||||
imageName() {
|
||||
return this.item.name;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<list-item v-bind="$attrs">
|
||||
<template #left-primary>
|
||||
<router-link
|
||||
class="gl-text-body gl-font-weight-bold"
|
||||
data-testid="details-link"
|
||||
data-qa-selector="registry_image_content"
|
||||
:to="{ name: 'details', params: { id } }"
|
||||
>
|
||||
{{ imageName }}
|
||||
</router-link>
|
||||
<clipboard-button
|
||||
v-if="item.location"
|
||||
:text="item.location"
|
||||
:title="item.location"
|
||||
category="tertiary"
|
||||
/>
|
||||
</template>
|
||||
<template #left-secondary>
|
||||
<template v-if="!metadataLoading">
|
||||
<span class="gl-display-flex gl-align-items-center" data-testid="tags-count">
|
||||
<gl-icon name="tag" class="gl-mr-2" />
|
||||
<gl-sprintf :message="artifactCountText">
|
||||
<template #count>
|
||||
{{ item.artifactCount }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<div v-else class="gl-w-full">
|
||||
<gl-skeleton-loader :width="900" :height="16" preserve-aspect-ratio="xMinYMax meet">
|
||||
<circle cx="6" cy="8" r="6" />
|
||||
<rect x="16" y="4" width="100" height="8" rx="4" />
|
||||
</gl-skeleton-loader>
|
||||
</div>
|
||||
</template>
|
||||
</list-item>
|
||||
</template>
|
|
@ -0,0 +1,29 @@
|
|||
import { s__, __ } from '~/locale';
|
||||
|
||||
export const ROOT_IMAGE_TEXT = s__('HarborRegistry|Root image');
|
||||
export const NAME_SORT_FIELD = { orderBy: 'NAME', label: __('Name') };
|
||||
|
||||
export const ASCENDING_ORDER = 'asc';
|
||||
export const DESCENDING_ORDER = 'desc';
|
||||
|
||||
export const NAME_SORT_FIELD_KEY = 'name';
|
||||
export const UPDATED_SORT_FIELD_KEY = 'update_time';
|
||||
export const CREATED_SORT_FIELD_KEY = 'creation_time';
|
||||
|
||||
export const SORT_FIELD_MAPPING = {
|
||||
NAME: NAME_SORT_FIELD_KEY,
|
||||
UPDATED: UPDATED_SORT_FIELD_KEY,
|
||||
CREATED: CREATED_SORT_FIELD_KEY,
|
||||
};
|
||||
|
||||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
export const dockerBuildCommand = (repositoryUrl) => {
|
||||
return `docker build -t ${repositoryUrl} .`;
|
||||
};
|
||||
export const dockerPushCommand = (repositoryUrl) => {
|
||||
return `docker push ${repositoryUrl}`;
|
||||
};
|
||||
export const dockerLoginCommand = (registryHostUrlWithPort) => {
|
||||
return `docker login ${registryHostUrlWithPort}`;
|
||||
};
|
||||
/* eslint-enable @gitlab/require-i18n-strings */
|
|
@ -0,0 +1,39 @@
|
|||
import { s__, __ } from '~/locale';
|
||||
|
||||
export const UPDATED_AT = s__('HarborRegistry|Last updated %{time}');
|
||||
|
||||
export const MISSING_OR_DELETED_IMAGE_TITLE = s__(
|
||||
'HarborRegistry|The image repository could not be found.',
|
||||
);
|
||||
|
||||
export const MISSING_OR_DELETED_IMAGE_MESSAGE = s__(
|
||||
'HarborRegistry|The requested image repository does not exist or has been deleted. If you think this is an error, try refreshing the page.',
|
||||
);
|
||||
|
||||
export const NO_TAGS_TITLE = s__('HarborRegistry|This image has no active tags');
|
||||
|
||||
export const NO_TAGS_MESSAGE = s__(
|
||||
`HarborRegistry|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.`,
|
||||
);
|
||||
|
||||
export const NO_TAGS_MATCHING_FILTERS_TITLE = s__('HarborRegistry|The filter returned no results');
|
||||
|
||||
export const NO_TAGS_MATCHING_FILTERS_DESCRIPTION = s__(
|
||||
'HarborRegistry|Please try different search criteria',
|
||||
);
|
||||
|
||||
export const DIGEST_LABEL = s__('HarborRegistry|Digest: %{imageId}');
|
||||
export const CREATED_AT_LABEL = s__('HarborRegistry|Published %{timeInfo}');
|
||||
export const PUBLISHED_DETAILS_ROW_TEXT = s__(
|
||||
'HarborRegistry|Published to the %{repositoryPath} image repository at %{time} on %{date}',
|
||||
);
|
||||
export const MANIFEST_DETAILS_ROW_TEST = s__('HarborRegistry|Manifest digest: %{digest}');
|
||||
export const CONFIGURATION_DETAILS_ROW_TEST = s__('HarborRegistry|Configuration digest: %{digest}');
|
||||
export const MISSING_MANIFEST_WARNING_TOOLTIP = s__(
|
||||
'HarborRegistry|Invalid tag: missing manifest digest',
|
||||
);
|
||||
|
||||
export const NOT_AVAILABLE_TEXT = __('N/A');
|
||||
export const NOT_AVAILABLE_SIZE = __('0 bytes');
|
|
@ -0,0 +1,3 @@
|
|||
export * from './common';
|
||||
export * from './list';
|
||||
export * from './details';
|
|
@ -0,0 +1,33 @@
|
|||
import { s__, __, n__ } from '~/locale';
|
||||
import { NAME_SORT_FIELD } from './common';
|
||||
|
||||
// Translations strings
|
||||
|
||||
export const HARBOR_REGISTRY_TITLE = s__('HarborRegistry|Harbor Registry');
|
||||
|
||||
export const CONNECTION_ERROR_TITLE = s__('HarborRegistry|Harbor connection error');
|
||||
export const CONNECTION_ERROR_MESSAGE = s__(
|
||||
`HarborRegistry|We are having trouble connecting to the Harbor Registry. Please try refreshing the page. If this error persists, please review %{docLinkStart}the troubleshooting documentation%{docLinkEnd}.`,
|
||||
);
|
||||
export const LIST_INTRO_TEXT = s__(
|
||||
`HarborRegistry|With the Harbor Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}`,
|
||||
);
|
||||
|
||||
export const imagesCountInfoText = (count) => {
|
||||
return n__(
|
||||
'HarborRegistry|%{count} Image repository',
|
||||
'HarborRegistry|%{count} Image repositories',
|
||||
count,
|
||||
);
|
||||
};
|
||||
|
||||
export const EMPTY_RESULT_TITLE = s__('HarborRegistry|Sorry, your filter produced no results.');
|
||||
export const EMPTY_RESULT_MESSAGE = s__(
|
||||
'HarborRegistry|To widen your search, change or remove the filters above.',
|
||||
);
|
||||
|
||||
export const SORT_FIELDS = [
|
||||
{ orderBy: 'UPDATED', label: __('Updated') },
|
||||
{ orderBy: 'CREATED', label: __('Created') },
|
||||
NAME_SORT_FIELD,
|
||||
];
|
|
@ -0,0 +1,78 @@
|
|||
import { GlToast } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import PerformancePlugin from '~/performance/vue_performance_plugin';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import RegistryBreadcrumb from '~/packages_and_registries/shared/components/registry_breadcrumb.vue';
|
||||
import { renderBreadcrumb } from '~/packages_and_registries/shared/utils';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import {
|
||||
dockerBuildCommand,
|
||||
dockerPushCommand,
|
||||
dockerLoginCommand,
|
||||
} from '~/packages_and_registries/harbor_registry/constants';
|
||||
import createRouter from './router';
|
||||
import HarborRegistryExplorer from './pages/index.vue';
|
||||
|
||||
Vue.use(Translate);
|
||||
Vue.use(GlToast);
|
||||
|
||||
Vue.use(PerformancePlugin, {
|
||||
components: [
|
||||
'RegistryListPage',
|
||||
'ListHeader',
|
||||
'ImageListRow',
|
||||
'RegistryDetailsPage',
|
||||
'DetailsHeader',
|
||||
'TagsList',
|
||||
],
|
||||
});
|
||||
|
||||
export default (id) => {
|
||||
const el = document.getElementById(id);
|
||||
|
||||
if (!el) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { endpoint, connectionError, invalidPathError, isGroupPage, ...config } = el.dataset;
|
||||
|
||||
const breadCrumbState = Vue.observable({
|
||||
name: '',
|
||||
updateName(value) {
|
||||
this.name = value;
|
||||
},
|
||||
});
|
||||
|
||||
const router = createRouter(endpoint, breadCrumbState);
|
||||
|
||||
const attachMainComponent = () => {
|
||||
return new Vue({
|
||||
el,
|
||||
router,
|
||||
provide() {
|
||||
return {
|
||||
breadCrumbState,
|
||||
config: {
|
||||
...config,
|
||||
connectionError: parseBoolean(connectionError),
|
||||
invalidPathError: parseBoolean(invalidPathError),
|
||||
isGroupPage: parseBoolean(isGroupPage),
|
||||
helpPagePath: helpPagePath('user/packages/container_registry/index'),
|
||||
},
|
||||
dockerBuildCommand: dockerBuildCommand(config.repositoryUrl),
|
||||
dockerPushCommand: dockerPushCommand(config.repositoryUrl),
|
||||
dockerLoginCommand: dockerLoginCommand(config.registryHostUrlWithPort),
|
||||
};
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(HarborRegistryExplorer);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
attachBreadcrumb: renderBreadcrumb(router, null, RegistryBreadcrumb),
|
||||
attachMainComponent,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,200 @@
|
|||
const mockRequestFn = (mockData) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(mockData);
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
export const harborListResponse = () => {
|
||||
const harborListResponseData = {
|
||||
repositories: [
|
||||
{
|
||||
artifactCount: 1,
|
||||
creationTime: '2022-03-02T06:35:53.205Z',
|
||||
id: 25,
|
||||
name: 'shao/flinkx',
|
||||
projectId: 21,
|
||||
pullCount: 0,
|
||||
updateTime: '2022-03-02T06:35:53.205Z',
|
||||
location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
},
|
||||
{
|
||||
artifactCount: 1,
|
||||
creationTime: '2022-03-02T06:35:53.205Z',
|
||||
id: 26,
|
||||
name: 'shao/flinkx1',
|
||||
projectId: 21,
|
||||
pullCount: 0,
|
||||
updateTime: '2022-03-02T06:35:53.205Z',
|
||||
location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
},
|
||||
{
|
||||
artifactCount: 1,
|
||||
creationTime: '2022-03-02T06:35:53.205Z',
|
||||
id: 27,
|
||||
name: 'shao/flinkx2',
|
||||
projectId: 21,
|
||||
pullCount: 0,
|
||||
updateTime: '2022-03-02T06:35:53.205Z',
|
||||
location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
},
|
||||
],
|
||||
totalCount: 3,
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
},
|
||||
};
|
||||
|
||||
return mockRequestFn(harborListResponseData);
|
||||
};
|
||||
|
||||
export const getHarborRegistryImageDetail = () => {
|
||||
const harborRegistryImageDetailData = {
|
||||
artifactCount: 1,
|
||||
creationTime: '2022-03-02T06:35:53.205Z',
|
||||
id: 25,
|
||||
name: 'shao/flinkx',
|
||||
projectId: 21,
|
||||
pullCount: 0,
|
||||
updateTime: '2022-03-02T06:35:53.205Z',
|
||||
location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
tagsCount: 10,
|
||||
};
|
||||
|
||||
return mockRequestFn(harborRegistryImageDetailData);
|
||||
};
|
||||
|
||||
export const harborTagsResponse = () => {
|
||||
const harborTagsResponseData = {
|
||||
tags: [
|
||||
{
|
||||
digest: 'sha256:7f386a1844faf341353e1c20f2f39f11f397604fedc475435d13f756eeb235d1',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
|
||||
name: '02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
|
||||
revision: 'f53bde3d44699e04e11cf15fb415046a0913e2623d878d89bc21adb2cbda5255',
|
||||
shortRevision: 'f53bde3d4',
|
||||
createdAt: '2022-03-02T23:59:05+00:00',
|
||||
totalSize: '6623124',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:4554416b84c4568fe93086620b637064ed029737aabe7308b96d50e3d9d92ed7',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
|
||||
name: '02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
|
||||
revision: 'e1fe52d8bab66d71bd54a6b8784d3b9edbc68adbd6ea87f5fa44d9974144ef9e',
|
||||
shortRevision: 'e1fe52d8b',
|
||||
createdAt: '2022-02-10T01:09:56+00:00',
|
||||
totalSize: '920760',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:14f37b60e52b9ce0e9f8f7094b311d265384798592f783487c30aaa3d58e6345',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
|
||||
name: '03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
|
||||
revision: 'c72770c6eb93c421bc496964b4bffc742b1ec2e642cdab876be7afda1856029f',
|
||||
shortRevision: 'c72770c6e',
|
||||
createdAt: '2021-12-22T04:48:48+00:00',
|
||||
totalSize: '48609053',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:e925e3b8277ea23f387ed5fba5e78280cfac7cfb261a78cf046becf7b6a3faae',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
|
||||
name: '03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
|
||||
revision: '1ac2a43194f4e15166abdf3f26e6ec92215240490b9cac834d63de1a3d87494a',
|
||||
shortRevision: '1ac2a4319',
|
||||
createdAt: '2022-03-09T11:02:27+00:00',
|
||||
totalSize: '35141894',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:7d8303fd5c077787a8c879f8f66b69e2b5605f48ccd3f286e236fb0749fcc1ca',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
|
||||
name: '05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
|
||||
revision: 'cf8fee086701016e1a84e6824f0c896951fef4cce9d4745459558b87eec3232c',
|
||||
shortRevision: 'cf8fee086',
|
||||
createdAt: '2022-01-21T11:31:43+00:00',
|
||||
totalSize: '48716070',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:b33611cefe20e4a41a6e0dce356a5d7ef3c177ea7536a58652f5b3a4f2f83549',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
|
||||
name: '093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
|
||||
revision: '1a4b48198b13d55242c5164e64d41c4e9f75b5d9506bc6e0efc1534dd0dd1f15',
|
||||
shortRevision: '1a4b48198',
|
||||
createdAt: '2022-01-21T11:31:51+00:00',
|
||||
totalSize: '6623127',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:d25c3c020e2dbd4711a67b9fe308f4cbb7b0bb21815e722a02f91c570dc5d519',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
|
||||
name: '09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
|
||||
revision: '03e2e2777dde01c30469ee8c710973dd08a7a4f70494d7dc1583c24b525d7f61',
|
||||
shortRevision: '03e2e2777',
|
||||
createdAt: '2022-03-02T23:58:20+00:00',
|
||||
totalSize: '911377',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:fb760e4d2184e9e8e39d6917534d4610fe01009734698a5653b2de1391ba28f4',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
|
||||
name: '09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
|
||||
revision: '350e78d60646bf6967244448c6aaa14d21ecb9a0c6cf87e9ff0361cbe59b9012',
|
||||
shortRevision: '350e78d60',
|
||||
createdAt: '2022-01-19T13:49:14+00:00',
|
||||
totalSize: '48710241',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:407250f380cea92729cbc038c420e74900f53b852e11edc6404fe75a0fd2c402',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
|
||||
name: '0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
|
||||
revision: '76038370b7f3904364891457c4a6a234897255e6b9f45d0a852bf3a7e5257e18',
|
||||
shortRevision: '76038370b',
|
||||
createdAt: '2022-01-24T12:56:22+00:00',
|
||||
totalSize: '280065',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:ada87f25218542951ce6720c27f3d0758e90c2540bd129f5cfb9e15b31e07b07',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
|
||||
name: '0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
|
||||
revision: '3d4b49a7bbb36c48bb721f4d0e76e7950bec3878ee29cdfdd6da39f575d6d37f',
|
||||
shortRevision: '3d4b49a7b',
|
||||
createdAt: '2022-02-17T17:37:52+00:00',
|
||||
totalSize: '48655767',
|
||||
},
|
||||
],
|
||||
totalCount: 10,
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: true,
|
||||
},
|
||||
};
|
||||
|
||||
return mockRequestFn(harborTagsResponseData);
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<router-view ref="router-view" />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,177 @@
|
|||
<script>
|
||||
import { GlEmptyState, GlSprintf, GlLink, GlSkeletonLoader } from '@gitlab/ui';
|
||||
import HarborListHeader from '~/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue';
|
||||
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
|
||||
import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue';
|
||||
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
|
||||
import {
|
||||
SORT_FIELDS,
|
||||
CONNECTION_ERROR_TITLE,
|
||||
CONNECTION_ERROR_MESSAGE,
|
||||
EMPTY_RESULT_TITLE,
|
||||
EMPTY_RESULT_MESSAGE,
|
||||
} from '~/packages_and_registries/harbor_registry/constants';
|
||||
import Tracking from '~/tracking';
|
||||
import { harborListResponse } from '../mock_api';
|
||||
|
||||
export default {
|
||||
name: 'HarborListPage',
|
||||
components: {
|
||||
HarborListHeader,
|
||||
HarborList,
|
||||
GlSkeletonLoader,
|
||||
GlEmptyState,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
PersistedSearch,
|
||||
CliCommands: () =>
|
||||
import(
|
||||
/* webpackChunkName: 'harbor_registry_components' */ '~/packages_and_registries/shared/components/cli_commands.vue'
|
||||
),
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
inject: ['config', 'dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand'],
|
||||
loader: {
|
||||
repeat: 10,
|
||||
width: 1000,
|
||||
height: 40,
|
||||
},
|
||||
i18n: {
|
||||
CONNECTION_ERROR_TITLE,
|
||||
CONNECTION_ERROR_MESSAGE,
|
||||
EMPTY_RESULT_TITLE,
|
||||
EMPTY_RESULT_MESSAGE,
|
||||
},
|
||||
searchConfig: SORT_FIELDS,
|
||||
data() {
|
||||
return {
|
||||
images: [],
|
||||
totalCount: 0,
|
||||
pageInfo: {},
|
||||
filter: [],
|
||||
isLoading: true,
|
||||
sorting: null,
|
||||
name: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showCommands() {
|
||||
return !this.isLoading && !this.config?.isGroupPage && this.images?.length;
|
||||
},
|
||||
showConnectionError() {
|
||||
return this.config.connectionError || this.config.invalidPathError;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchHarborImages() {
|
||||
// TODO: Waiting for harbor api integration to finish: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82777
|
||||
this.isLoading = true;
|
||||
|
||||
harborListResponse()
|
||||
.then((res) => {
|
||||
this.images = res?.repositories || [];
|
||||
this.totalCount = res?.totalCount || 0;
|
||||
this.pageInfo = res?.pageInfo || {};
|
||||
this.isLoading = false;
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
handleSearchUpdate({ sort, filters }) {
|
||||
this.sorting = sort;
|
||||
|
||||
const search = filters.find((i) => i.type === FILTERED_SEARCH_TERM);
|
||||
this.name = search?.value?.data;
|
||||
|
||||
this.fetchHarborImages();
|
||||
},
|
||||
fetchPrevPage() {
|
||||
// TODO: Waiting for harbor api integration to finish: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82777
|
||||
this.fetchHarborImages();
|
||||
},
|
||||
fetchNextPage() {
|
||||
// TODO: Waiting for harbor api integration to finish: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82777
|
||||
this.fetchHarborImages();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<gl-empty-state
|
||||
v-if="showConnectionError"
|
||||
:title="$options.i18n.CONNECTION_ERROR_TITLE"
|
||||
:svg-path="config.containersErrorImage"
|
||||
>
|
||||
<template #description>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.CONNECTION_ERROR_MESSAGE">
|
||||
<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>
|
||||
<harbor-list-header
|
||||
:metadata-loading="isLoading"
|
||||
:images-count="totalCount"
|
||||
:help-page-path="config.helpPagePath"
|
||||
>
|
||||
<template #commands>
|
||||
<cli-commands
|
||||
v-if="showCommands"
|
||||
:docker-build-command="dockerBuildCommand"
|
||||
:docker-push-command="dockerPushCommand"
|
||||
:docker-login-command="dockerLoginCommand"
|
||||
/>
|
||||
</template>
|
||||
</harbor-list-header>
|
||||
<persisted-search
|
||||
:sortable-fields="$options.searchConfig"
|
||||
:default-order="$options.searchConfig[0].orderBy"
|
||||
default-sort="desc"
|
||||
@update="handleSearchUpdate"
|
||||
/>
|
||||
|
||||
<div v-if="isLoading" class="gl-mt-5">
|
||||
<gl-skeleton-loader
|
||||
v-for="index in $options.loader.repeat"
|
||||
:key="index"
|
||||
:width="$options.loader.width"
|
||||
:height="$options.loader.height"
|
||||
preserve-aspect-ratio="xMinYMax meet"
|
||||
>
|
||||
<rect width="500" x="10" y="10" height="20" rx="4" />
|
||||
<circle cx="525" cy="20" r="10" />
|
||||
<rect x="960" y="0" width="40" height="40" rx="4" />
|
||||
</gl-skeleton-loader>
|
||||
</div>
|
||||
<template v-else>
|
||||
<template v-if="images.length > 0 || name">
|
||||
<harbor-list
|
||||
v-if="images.length"
|
||||
:images="images"
|
||||
:meta-data-loading="isLoading"
|
||||
:page-info="pageInfo"
|
||||
@prev-page="fetchPrevPage"
|
||||
@next-page="fetchNextPage"
|
||||
/>
|
||||
<gl-empty-state
|
||||
v-else
|
||||
:svg-path="config.noContainersImage"
|
||||
data-testid="emptySearch"
|
||||
:title="$options.i18n.EMPTY_RESULT_TITLE"
|
||||
>
|
||||
<template #description>
|
||||
{{ $options.i18n.EMPTY_RESULT_MESSAGE }}
|
||||
</template>
|
||||
</gl-empty-state>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,35 @@
|
|||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import { HARBOR_REGISTRY_TITLE } from './constants/index';
|
||||
import List from './pages/list.vue';
|
||||
import Details from './pages/details.vue';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
export default function createRouter(base, breadCrumbState) {
|
||||
const router = new VueRouter({
|
||||
base,
|
||||
mode: 'history',
|
||||
routes: [
|
||||
{
|
||||
name: 'list',
|
||||
path: '/',
|
||||
component: List,
|
||||
meta: {
|
||||
nameGenerator: () => HARBOR_REGISTRY_TITLE,
|
||||
root: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'details',
|
||||
path: '/:id',
|
||||
component: Details,
|
||||
meta: {
|
||||
nameGenerator: () => breadCrumbState.name,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
|
@ -13,7 +13,8 @@ export default {
|
|||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import HarborRegistryExplorer from '~/packages_and_registries/harbor_registry/index';
|
||||
|
||||
const explorer = HarborRegistryExplorer('js-harbor-registry-list-group');
|
||||
|
||||
if (explorer) {
|
||||
explorer.attachBreadcrumb();
|
||||
explorer.attachMainComponent();
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable no-new */
|
||||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import loadAwardsHandler from '~/awards_handler';
|
||||
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
||||
import Diff from '~/diff';
|
||||
|
@ -14,6 +15,7 @@ import { initCommitBoxInfo } from '~/projects/commit_box/info';
|
|||
import syntaxHighlight from '~/syntax_highlight';
|
||||
import ZenMode from '~/zen_mode';
|
||||
import '~/sourcegraph/load';
|
||||
import DiffStats from '~/diffs/components/diff_stats.vue';
|
||||
|
||||
const hasPerfBar = document.querySelector('.with-performance-bar');
|
||||
const performanceHeight = hasPerfBar ? 35 : 0;
|
||||
|
@ -26,6 +28,7 @@ initCommitBoxInfo();
|
|||
initDeprecatedNotes();
|
||||
|
||||
const filesContainer = $('.js-diffs-batch');
|
||||
const diffStatsElements = document.querySelectorAll('#js-diff-stats');
|
||||
|
||||
if (filesContainer.length) {
|
||||
const batchPath = filesContainer.data('diffFilesPath');
|
||||
|
@ -44,5 +47,29 @@ if (filesContainer.length) {
|
|||
} else {
|
||||
new Diff();
|
||||
}
|
||||
|
||||
if (diffStatsElements.length) {
|
||||
diffStatsElements.forEach((diffStatsEl) => {
|
||||
const { addedLines, removedLines, oldSize, newSize, viewerName } = diffStatsEl.dataset;
|
||||
|
||||
new Vue({
|
||||
el: diffStatsEl,
|
||||
render(createElement) {
|
||||
return createElement(DiffStats, {
|
||||
props: {
|
||||
diffFile: {
|
||||
old_size: oldSize,
|
||||
new_size: newSize,
|
||||
viewer: { name: viewerName },
|
||||
},
|
||||
addedLines: Number(addedLines),
|
||||
removedLines: Number(removedLines),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
loadAwardsHandler();
|
||||
initCommitActions();
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import HarborRegistryExplorer from '~/packages_and_registries/harbor_registry/index';
|
||||
|
||||
const explorer = HarborRegistryExplorer('js-harbor-registry-list-project');
|
||||
|
||||
if (explorer) {
|
||||
explorer.attachBreadcrumb();
|
||||
explorer.attachMainComponent();
|
||||
}
|
|
@ -14,7 +14,7 @@ export default {
|
|||
body: __(
|
||||
'Create a new %{codeStart}.gitlab-ci.yml%{codeEnd} file at the root of the repository to get started.',
|
||||
),
|
||||
btnText: __('Create new CI/CD pipeline'),
|
||||
btnText: __('Configure pipeline'),
|
||||
},
|
||||
inject: {
|
||||
emptyStateIllustrationPath: {
|
||||
|
|
|
@ -18,31 +18,35 @@ export const fetchGroups = ({ commit }, search) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const fetchProjects = ({ commit, state }, search) => {
|
||||
export const fetchProjects = ({ commit, state }, search, emptyCallback = () => {}) => {
|
||||
commit(types.REQUEST_PROJECTS);
|
||||
const groupId = state.query?.group_id;
|
||||
const callback = (data) => {
|
||||
if (data) {
|
||||
commit(types.RECEIVE_PROJECTS_SUCCESS, data);
|
||||
} else {
|
||||
createFlash({ message: __('There was an error fetching projects') });
|
||||
commit(types.RECEIVE_PROJECTS_ERROR);
|
||||
}
|
||||
|
||||
const handleCatch = () => {
|
||||
createFlash({ message: __('There was an error fetching projects') });
|
||||
commit(types.RECEIVE_PROJECTS_ERROR);
|
||||
};
|
||||
const handleSuccess = ({ data }) => {
|
||||
commit(types.RECEIVE_PROJECTS_SUCCESS, data);
|
||||
};
|
||||
|
||||
if (groupId) {
|
||||
// TODO (https://gitlab.com/gitlab-org/gitlab/-/issues/323331): For errors `createFlash` is called twice; in `callback` and in `Api.groupProjects`
|
||||
Api.groupProjects(
|
||||
groupId,
|
||||
search,
|
||||
{ order_by: 'similarity', with_shared: false, include_subgroups: true },
|
||||
callback,
|
||||
);
|
||||
{
|
||||
order_by: 'similarity',
|
||||
with_shared: false,
|
||||
include_subgroups: true,
|
||||
},
|
||||
emptyCallback,
|
||||
true,
|
||||
)
|
||||
.then(handleSuccess)
|
||||
.catch(handleCatch);
|
||||
} else {
|
||||
// The .catch() is due to the API method not handling a rejection properly
|
||||
Api.projects(search, { order_by: 'similarity' }, callback).catch(() => {
|
||||
callback();
|
||||
});
|
||||
Api.projects(search, { order_by: 'similarity' }).then(handleSuccess).catch(handleCatch);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -179,6 +179,18 @@ module DiffHelper
|
|||
}
|
||||
end
|
||||
|
||||
def diff_file_stats_data(diff_file)
|
||||
old_blob = diff_file.old_blob
|
||||
new_blob = diff_file.new_blob
|
||||
{
|
||||
old_size: old_blob&.size,
|
||||
new_size: new_blob&.size,
|
||||
added_lines: diff_file.added_lines,
|
||||
removed_lines: diff_file.removed_lines,
|
||||
viewer_name: diff_file.viewer.partial_name
|
||||
}
|
||||
end
|
||||
|
||||
def editable_diff?(diff_file)
|
||||
!diff_file.deleted_file? && @merge_request && @merge_request.source_project
|
||||
end
|
||||
|
|
|
@ -576,6 +576,8 @@ class ApplicationSetting < ApplicationRecord
|
|||
length: { maximum: 100, message: N_('is too long (maximum is 100 entries)') },
|
||||
allow_nil: false
|
||||
|
||||
validates :public_runner_releases_url, addressable_url: true, presence: true
|
||||
|
||||
attr_encrypted :asset_proxy_secret_key,
|
||||
mode: :per_attribute_iv,
|
||||
key: Settings.attr_encrypted_db_key_base_truncated,
|
||||
|
|
|
@ -33,7 +33,7 @@ module Projects
|
|||
if deleted_tags.any? && @container_repository.delete_tag_by_digest(deleted_tags.each_value.first)
|
||||
success(deleted: deleted_tags.keys)
|
||||
else
|
||||
error('could not delete tags')
|
||||
error("could not delete tags: #{@tag_names.join(', ')}".truncate(1000))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -168,7 +168,7 @@ module QuickActions
|
|||
next unless definition
|
||||
|
||||
definition.execute(self, arg)
|
||||
usage_ping_tracking(name, arg)
|
||||
usage_ping_tracking(definition.name, arg)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -186,7 +186,7 @@ module QuickActions
|
|||
|
||||
def usage_ping_tracking(quick_action_name, arg)
|
||||
Gitlab::UsageDataCounters::QuickActionActivityUniqueCounter.track_unique_action(
|
||||
quick_action_name,
|
||||
quick_action_name.to_s,
|
||||
args: arg&.strip,
|
||||
user: current_user
|
||||
)
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
#js-harbor-registry-list-group{ data: { endpoint: group_harbor_registries_path(@group),
|
||||
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
|
||||
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
|
||||
"help_page_path" => help_page_path('user/packages/container_registry/index'),
|
||||
"repository_url" => 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
"registry_host_url_with_port" => 'demo.harbor.com',
|
||||
connection_error: (!!@connection_error).to_s,
|
||||
invalid_path_error: (!!@invalid_path_error).to_s, } }
|
||||
invalid_path_error: (!!@invalid_path_error).to_s,
|
||||
is_group_page: true.to_s } }
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
- unless diff_file.submodule?
|
||||
.file-actions.gl-display-none.gl-sm-display-flex
|
||||
#js-diff-stats{ data: diff_file_stats_data(diff_file) }
|
||||
- if diff_file.blob&.readable_text?
|
||||
%span.has-tooltip{ title: _("Toggle comments for this file") }
|
||||
= link_to '#', class: 'js-toggle-diff-comments btn gl-button btn-default btn-icon selected', disabled: @diff_notes_disabled do
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
#js-harbor-registry-list-project{ data: { endpoint: project_harbor_registry_index_path(@project),
|
||||
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
|
||||
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
|
||||
"help_page_path" => help_page_path('user/packages/container_registry/index'),
|
||||
"repository_url" => 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
"registry_host_url_with_port" => 'demo.harbor.com',
|
||||
connection_error: (!!@connection_error).to_s,
|
||||
invalid_path_error: (!!@invalid_path_error).to_s, } }
|
||||
invalid_path_error: (!!@invalid_path_error).to_s,
|
||||
is_group_page: false.to_s, } }
|
||||
|
|
|
@ -1,26 +1,5 @@
|
|||
- options_hash = local_assigns.fetch(:options_hash, groups_sort_options_hash)
|
||||
- show_archive_options = local_assigns.fetch(:show_archive_options, false)
|
||||
- groups_sort_options = options_hash.map { |value, title| { value: value, text: title, href: filter_groups_path(sort: value) } }
|
||||
|
||||
.dropdown.inline.js-group-filter-dropdown-wrap.gl-mr-3
|
||||
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
|
||||
%span.dropdown-label
|
||||
= options_hash[project_list_sort_by]
|
||||
= sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
|
||||
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
|
||||
%li.dropdown-header
|
||||
= _("Sort by")
|
||||
- options_hash.each do |value, title|
|
||||
%li.js-filter-sort-order
|
||||
= link_to filter_groups_path(sort: value), class: ("is-active" if project_list_sort_by == value) do
|
||||
= title
|
||||
- if show_archive_options
|
||||
%li.divider
|
||||
%li.js-filter-archived-projects
|
||||
= link_to filter_groups_path(archived: nil), class: ("is-active" unless params[:archived].present?) do
|
||||
= _("Hide archived projects")
|
||||
%li.js-filter-archived-projects
|
||||
= link_to filter_groups_path(archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do
|
||||
= _("Show archived projects")
|
||||
%li.js-filter-archived-projects
|
||||
= link_to filter_groups_path(archived: 'only'), class: ("is-active" if params[:archived] == 'only') do
|
||||
= _("Show archived projects only")
|
||||
%div{ data: { testid: 'group_sort_by_dropdown' } }
|
||||
= gl_redirect_listbox_tag groups_sort_options, project_list_sort_by, data: { right: true }
|
||||
|
|
|
@ -2771,7 +2771,7 @@
|
|||
:worker_name: ProjectExportWorker
|
||||
:feature_category: :importers
|
||||
:has_external_dependencies:
|
||||
:urgency: :throttled
|
||||
:urgency: :low
|
||||
:resource_boundary: :memory
|
||||
:weight: 1
|
||||
:idempotent:
|
||||
|
|
|
@ -8,7 +8,7 @@ class ProjectExportWorker # rubocop:disable Scalability/IdempotentWorker
|
|||
|
||||
feature_category :importers
|
||||
worker_resource_boundary :memory
|
||||
urgency :throttled
|
||||
urgency :low
|
||||
loggable_arguments 2, 3
|
||||
sidekiq_options retry: false, dead: false
|
||||
sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddPublicGitLabRunnerReleasesUrlToApplicationSettings < Gitlab::Database::Migration[1.0]
|
||||
# rubocop:disable Migration/AddLimitToTextColumns
|
||||
# limit is added in 20220324173554_add_text_limit_to_public_git_lab_runner_releases_url_application_settings
|
||||
def change
|
||||
add_column :application_settings, :public_runner_releases_url, :text, null: false, default: 'https://gitlab.com/api/v4/projects/gitlab-org%2Fgitlab-runner/releases'
|
||||
end
|
||||
# rubocop:enable Migration/AddLimitToTextColumns
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTextLimitToPublicGitLabRunnerReleasesUrlApplicationSettings < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_text_limit :application_settings, :public_runner_releases_url, 255
|
||||
end
|
||||
|
||||
def down
|
||||
remove_text_limit :application_settings, :public_runner_releases_url
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
610c5ded785f174d195a660062bb74e718bfd5a38b13773215e20e8f95c59da4
|
|
@ -0,0 +1 @@
|
|||
9f597a462768531b0c6ad23e6e1a52edb765724518e1cebc0684160b030d6225
|
|
@ -11257,6 +11257,7 @@ CREATE TABLE application_settings (
|
|||
encrypted_database_grafana_api_key_iv bytea,
|
||||
database_grafana_api_url text,
|
||||
database_grafana_tag text,
|
||||
public_runner_releases_url text DEFAULT 'https://gitlab.com/api/v4/projects/gitlab-org%2Fgitlab-runner/releases'::text NOT NULL,
|
||||
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
|
||||
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
|
||||
CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)),
|
||||
|
@ -11279,6 +11280,7 @@ CREATE TABLE application_settings (
|
|||
CONSTRAINT check_718b4458ae CHECK ((char_length(personal_access_token_prefix) <= 20)),
|
||||
CONSTRAINT check_7227fad848 CHECK ((char_length(rate_limiting_response_text) <= 255)),
|
||||
CONSTRAINT check_85a39b68ff CHECK ((char_length(encrypted_ci_jwt_signing_key_iv) <= 255)),
|
||||
CONSTRAINT check_8dca35398a CHECK ((char_length(public_runner_releases_url) <= 255)),
|
||||
CONSTRAINT check_9a719834eb CHECK ((char_length(secret_detection_token_revocation_url) <= 255)),
|
||||
CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)),
|
||||
CONSTRAINT check_a5704163cc CHECK ((char_length(secret_detection_revocation_token_types_url) <= 255)),
|
||||
|
|
|
@ -113,6 +113,7 @@ There are also a number of [variables you can use to configure runner behavior](
|
|||
| `CI_SERVER_PORT` | 12.8 | all | The port of the GitLab instance URL, without host or protocol. For example `8080`. |
|
||||
| `CI_SERVER_PROTOCOL` | 12.8 | all | The protocol of the GitLab instance URL, without host or port. For example `https`. |
|
||||
| `CI_SERVER_REVISION` | all | all | GitLab revision that schedules jobs. |
|
||||
| `CI_SERVER_TLS_CA_FILE` | all | all | File containing the CA certificate to verify the GitLab server. |
|
||||
| `CI_SERVER_URL` | 12.7 | all | The base URL of the GitLab instance, including protocol and port. For example `https://gitlab.example.com:8080`. |
|
||||
| `CI_SERVER_VERSION_MAJOR` | 11.4 | all | The major version of the GitLab instance. For example, if the GitLab version is `13.6.1`, the `CI_SERVER_VERSION_MAJOR` is `13`. |
|
||||
| `CI_SERVER_VERSION_MINOR` | 11.4 | all | The minor version of the GitLab instance. For example, if the GitLab version is `13.6.1`, the `CI_SERVER_VERSION_MINOR` is `6`. |
|
||||
|
|
|
@ -84,6 +84,12 @@ From there you can create a new iteration or select an iteration to get a more d
|
|||
|
||||
## Create an iteration
|
||||
|
||||
> [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069) in GitLab 14.10.
|
||||
|
||||
WARNING:
|
||||
Manual iteration management is in its end-of-life process. Creating an iteration is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069)
|
||||
for use in GitLab 14.10, and is planned for removal in GitLab 15.6.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must have at least the Developer role for a group.
|
||||
|
@ -100,7 +106,13 @@ To create an iteration:
|
|||
|
||||
## Edit an iteration
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218277) in GitLab 13.2.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218277) in GitLab 13.2.
|
||||
> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069) in GitLab 14.10.
|
||||
|
||||
WARNING:
|
||||
Editing all attributes, with the exception of `description` is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069)
|
||||
for use in GitLab 14.10, and is planned for removal in GitLab 15.6.
|
||||
In the future only editing an iteration's `description` will be allowed.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
|
@ -110,7 +122,12 @@ To edit an iteration, select the three-dot menu (**{ellipsis_v}**) > **Edit**.
|
|||
|
||||
## Delete an iteration
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292268) in GitLab 14.3.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/292268) in GitLab 14.3.
|
||||
> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069) in GitLab 14.10.
|
||||
|
||||
WARNING:
|
||||
Manual iteration management is in its end-of-life process. Deleting an iteration is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/356069)
|
||||
for use in GitLab 14.10, and is planned for removal in GitLab 15.6.
|
||||
|
||||
Prerequisites:
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
|
@ -14,8 +14,12 @@ The numbers indicate how many issues, merge requests, and to-do items are assign
|
|||
|
||||
![issues and MRs dashboard links](img/dashboard_links_v14_6.png)
|
||||
|
||||
- **{issues}** **Issues**: The open issues assigned to you.
|
||||
- **{merge-request-open}** **Merge requests**: The [merge requests](../project/merge_requests/index.md) assigned to you.
|
||||
- **{issues}** **Issues**: Issues assigned to you.
|
||||
- **{merge-request-open}** **Merge requests**: Open [merge requests](../project/merge_requests/index.md).
|
||||
Select the icon to show a dropdown list of merge request filters:
|
||||
- [Attention requests](../project/merge_requests/index.md#request-attention-to-a-merge-request) (**{attention-solid}**) for you.
|
||||
- [Review requests](../project/merge_requests/reviews/index.md) for you.
|
||||
- Merge requests assigned to you.
|
||||
- **{todo-done}** **To-do items**: The [to-do items](../todos.md) assigned to you.
|
||||
|
||||
You can search through **Open**, **Closed**, or **All** issues.
|
||||
|
@ -37,6 +41,7 @@ in the search field in the upper right corner:
|
|||
> - Filtering by iterations was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118742) in GitLab 13.6.
|
||||
> - Filtering by iterations was moved from GitLab Ultimate to GitLab Premium in 13.9.
|
||||
> - Filtering by type was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322755) in GitLab 13.10 [with a flag](../../administration/feature_flags.md) named `vue_issues_list`. Disabled by default.
|
||||
> - Filtering by attention request was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/343528) in GitLab 14.10 [with a flag](../../administration/feature_flags.md) named `mr_attention_requests`. Disabled by default.
|
||||
|
||||
Follow these steps to filter the **Issues** and **Merge requests** list pages in projects and
|
||||
groups:
|
||||
|
@ -44,6 +49,7 @@ groups:
|
|||
1. Select **Search or filter results...**.
|
||||
1. In the dropdown list that appears, select the attribute you wish to filter by:
|
||||
- Assignee
|
||||
- [Attention requests](../project/merge_requests/index.md#request-attention-to-a-merge-request)
|
||||
- Author
|
||||
- Confidential
|
||||
- [Epic and child Epic](../group/epics/index.md) (available only for the group the Epic was created, not for [higher group levels](https://gitlab.com/gitlab-org/gitlab/-/issues/233729)).
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
class RunnerReleases
|
||||
include Singleton
|
||||
|
||||
RELEASES_VALIDITY_PERIOD = 1.day
|
||||
RELEASES_VALIDITY_AFTER_ERROR_PERIOD = 5.seconds
|
||||
|
||||
INITIAL_BACKOFF = 5.seconds
|
||||
MAX_BACKOFF = 1.hour
|
||||
BACKOFF_GROWTH_FACTOR = 2.0
|
||||
|
||||
def initialize
|
||||
reset!
|
||||
end
|
||||
|
||||
# Returns a sorted list of the publicly available GitLab Runner releases
|
||||
#
|
||||
def releases
|
||||
return @releases unless Time.now.utc >= @expire_time
|
||||
|
||||
@releases = fetch_new_releases
|
||||
end
|
||||
|
||||
def reset!
|
||||
@expire_time = Time.now.utc
|
||||
@releases = nil
|
||||
@backoff_count = 0
|
||||
end
|
||||
|
||||
public_class_method :instance
|
||||
|
||||
private
|
||||
|
||||
def fetch_new_releases
|
||||
response = Gitlab::HTTP.try_get(::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url)
|
||||
|
||||
releases = response.success? ? extract_releases(response) : nil
|
||||
ensure
|
||||
@expire_time = (releases ? RELEASES_VALIDITY_PERIOD : next_backoff).from_now
|
||||
end
|
||||
|
||||
def extract_releases(response)
|
||||
response.parsed_response.map { |release| parse_runner_release(release) }.sort!
|
||||
end
|
||||
|
||||
def parse_runner_release(release)
|
||||
::Gitlab::VersionInfo.parse(release['name'].delete_prefix('v'))
|
||||
end
|
||||
|
||||
def next_backoff
|
||||
return MAX_BACKOFF if @backoff_count >= 11 # optimization to prevent expensive exponentiation and possible overflows
|
||||
|
||||
backoff = (INITIAL_BACKOFF * (BACKOFF_GROWTH_FACTOR**@backoff_count))
|
||||
.clamp(INITIAL_BACKOFF, MAX_BACKOFF)
|
||||
.seconds
|
||||
@backoff_count += 1
|
||||
|
||||
backoff
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -15,10 +15,10 @@ module Gitlab
|
|||
@block = block
|
||||
end
|
||||
|
||||
def method_missing(name, *args, &block)
|
||||
def method_missing(...)
|
||||
__evaluate__
|
||||
|
||||
@result.__send__(name, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
|
||||
@result.__send__(...) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
|
||||
def respond_to_missing?(name, include_private = false)
|
||||
|
|
|
@ -291,7 +291,7 @@ module Gitlab
|
|||
parse_params do |attention_param|
|
||||
extract_users(attention_param)
|
||||
end
|
||||
command :attention do |users|
|
||||
command :attention, :attn do |users|
|
||||
next if users.empty?
|
||||
|
||||
users.each do |user|
|
||||
|
|
|
@ -47,7 +47,7 @@ module Sidebars
|
|||
::Sidebars::MenuItem.new(
|
||||
title: _('Container Registry'),
|
||||
link: project_container_registry_index_path(context.project),
|
||||
active_routes: { controller: :repositories },
|
||||
active_routes: { controller: 'projects/registry/repositories' },
|
||||
item_id: :container_registry
|
||||
)
|
||||
end
|
||||
|
@ -71,7 +71,7 @@ module Sidebars
|
|||
::Sidebars::MenuItem.new(
|
||||
title: _('Harbor Registry'),
|
||||
link: project_harbor_registry_index_path(context.project),
|
||||
active_routes: { controller: :harbor_registry },
|
||||
active_routes: { controller: 'projects/harbor/repositories' },
|
||||
item_id: :harbor_registry
|
||||
)
|
||||
end
|
||||
|
|
|
@ -9413,6 +9413,9 @@ msgstr ""
|
|||
msgid "Configure existing installation"
|
||||
msgstr ""
|
||||
|
||||
msgid "Configure pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Configure pipelines to deploy web apps, backend services, APIs and static resources to Google Cloud"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10511,9 +10514,6 @@ msgstr ""
|
|||
msgid "Create new %{name} by email"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create new CI/CD pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create new Value Stream"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13409,6 +13409,45 @@ msgstr ""
|
|||
msgid "Duration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duration|%s days"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duration|%s hours"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duration|%s minutes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duration|%s months"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duration|%s seconds"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duration|%s weeks"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duration|%s years"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duration|1 day"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duration|1 hour"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duration|1 minute"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duration|1 month"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duration|1 week"
|
||||
msgstr ""
|
||||
|
||||
msgid "Duration|1 year"
|
||||
msgstr ""
|
||||
|
||||
msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
|
||||
msgstr ""
|
||||
|
||||
|
@ -18262,6 +18301,76 @@ msgstr ""
|
|||
msgid "HarborIntegration|Use Harbor as this project's container registry."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|%{count} Image repository"
|
||||
msgid_plural "HarborRegistry|%{count} Image repositories"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "HarborRegistry|%{count} Tag"
|
||||
msgid_plural "HarborRegistry|%{count} Tags"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "HarborRegistry|Configuration digest: %{digest}"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Digest: %{imageId}"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Harbor Registry"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Harbor connection error"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Invalid tag: missing manifest digest"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Last updated %{time}"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Manifest digest: %{digest}"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Please try different search criteria"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Published %{timeInfo}"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Published to the %{repositoryPath} image repository at %{time} on %{date}"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Root image"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|Sorry, your filter produced no results."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|The filter returned no results"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|The image repository could not be found."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|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."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|The requested image repository does not exist or has been deleted. If you think this is an error, try refreshing the page."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|This image has no active tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|To widen your search, change or remove the filters above."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|We are having trouble connecting to the Harbor Registry. Please try refreshing the page. If this error persists, please review %{docLinkStart}the troubleshooting documentation%{docLinkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "HarborRegistry|With the Harbor Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hashed Storage must be enabled to use Geo"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -56,11 +56,8 @@ module DeprecationToolkitEnv
|
|||
# In this case, we recommend to add a silence together with an issue to patch or update
|
||||
# the dependency causing the problem.
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/commit/aea37f506bbe036378998916d374966c031bf347#note_647515736
|
||||
#
|
||||
# - lib/gitlab/lazy.rb: https://gitlab.com/gitlab-org/gitlab/-/issues/356367
|
||||
def self.allowed_kwarg_warning_paths
|
||||
%w[
|
||||
lib/gitlab/lazy.rb
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -33,6 +33,10 @@ RSpec.describe 'Commit' do
|
|||
it "reports the correct number of total changes" do
|
||||
expect(page).to have_content("Changes #{commit.diffs.size}")
|
||||
end
|
||||
|
||||
it 'renders diff stats', :js do
|
||||
expect(page).to have_selector(".diff-stats")
|
||||
end
|
||||
end
|
||||
|
||||
describe "pagination" do
|
||||
|
|
|
@ -14,25 +14,29 @@ RSpec.describe 'User sorts projects and order persists' do
|
|||
it "is set on the dashboard_projects_path" do
|
||||
visit(dashboard_projects_path)
|
||||
|
||||
expect(find('.dropdown-menu a.is-active', text: project_paths_label)).to have_content(project_paths_label)
|
||||
expect(find('#sort-projects-dropdown')).to have_content(project_paths_label)
|
||||
end
|
||||
|
||||
it "is set on the explore_projects_path" do
|
||||
visit(explore_projects_path)
|
||||
|
||||
expect(find('.dropdown-menu a.is-active', text: project_paths_label)).to have_content(project_paths_label)
|
||||
expect(find('#sort-projects-dropdown')).to have_content(project_paths_label)
|
||||
end
|
||||
|
||||
it "is set on the group_canonical_path" do
|
||||
visit(group_canonical_path(group))
|
||||
|
||||
expect(find('.dropdown-menu a.is-active', text: group_paths_label)).to have_content(group_paths_label)
|
||||
within '[data-testid=group_sort_by_dropdown]' do
|
||||
expect(find('.gl-dropdown-toggle')).to have_content(group_paths_label)
|
||||
end
|
||||
end
|
||||
|
||||
it "is set on the details_group_path" do
|
||||
visit(details_group_path(group))
|
||||
|
||||
expect(find('.dropdown-menu a.is-active', text: group_paths_label)).to have_content(group_paths_label)
|
||||
within '[data-testid=group_sort_by_dropdown]' do
|
||||
expect(find('.gl-dropdown-toggle')).to have_content(group_paths_label)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -58,23 +62,27 @@ RSpec.describe 'User sorts projects and order persists' do
|
|||
it_behaves_like "sort order persists across all views", "Name", "Name"
|
||||
end
|
||||
|
||||
context 'from group homepage' do
|
||||
context 'from group homepage', :js do
|
||||
before do
|
||||
sign_in(user)
|
||||
visit(group_canonical_path(group))
|
||||
find('button.dropdown-menu-toggle').click
|
||||
first(:link, 'Last created').click
|
||||
within '[data-testid=group_sort_by_dropdown]' do
|
||||
find('button.gl-dropdown-toggle').click
|
||||
first(:button, 'Last created').click
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like "sort order persists across all views", "Created date", "Last created"
|
||||
end
|
||||
|
||||
context 'from group details' do
|
||||
context 'from group details', :js do
|
||||
before do
|
||||
sign_in(user)
|
||||
visit(details_group_path(group))
|
||||
find('button.dropdown-menu-toggle').click
|
||||
first(:link, 'Most stars').click
|
||||
within '[data-testid=group_sort_by_dropdown]' do
|
||||
find('button.gl-dropdown-toggle').click
|
||||
first(:button, 'Most stars').click
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like "sort order persists across all views", "Stars", "Most stars"
|
||||
|
|
|
@ -49,6 +49,7 @@ const noop = () => {};
|
|||
* expectedActions: [],
|
||||
* })
|
||||
*/
|
||||
|
||||
export default (
|
||||
actionArg,
|
||||
payloadArg,
|
||||
|
|
|
@ -2,6 +2,9 @@ import MockAdapter from 'axios-mock-adapter';
|
|||
import Api, { DEFAULT_PER_PAGE } from '~/api';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import httpStatus from '~/lib/utils/http_status';
|
||||
import createFlash from '~/flash';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
describe('Api', () => {
|
||||
const dummyApiVersion = 'v3000';
|
||||
|
@ -675,6 +678,33 @@ describe('Api', () => {
|
|||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('uses flesh on error by default', async () => {
|
||||
const groupId = '123456';
|
||||
const query = 'dummy query';
|
||||
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
|
||||
const flashCallback = (callCount) => {
|
||||
expect(createFlash).toHaveBeenCalledTimes(callCount);
|
||||
createFlash.mockClear();
|
||||
};
|
||||
|
||||
mock.onGet(expectedUrl).reply(500, null);
|
||||
|
||||
const response = await Api.groupProjects(groupId, query, {}, () => {}).then(() => {
|
||||
flashCallback(1);
|
||||
});
|
||||
expect(response).toBeUndefined();
|
||||
});
|
||||
|
||||
it('NOT uses flesh on error with param useCustomErrorHandler', async () => {
|
||||
const groupId = '123456';
|
||||
const query = 'dummy query';
|
||||
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
|
||||
|
||||
mock.onGet(expectedUrl).reply(500, null);
|
||||
const apiCall = Api.groupProjects(groupId, query, {}, () => {}, true);
|
||||
await expect(apiCall).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupShareWithGroup', () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getTimeago, localTimeAgo, timeFor } from '~/lib/utils/datetime/timeago_utility';
|
||||
import { getTimeago, localTimeAgo, timeFor, duration } from '~/lib/utils/datetime/timeago_utility';
|
||||
import { s__ } from '~/locale';
|
||||
import '~/commons/bootstrap';
|
||||
|
||||
|
@ -66,6 +66,54 @@ describe('TimeAgo utils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('duration', () => {
|
||||
const ONE_DAY = 24 * 60 * 60;
|
||||
|
||||
it.each`
|
||||
secs | formatted
|
||||
${0} | ${'0 seconds'}
|
||||
${30} | ${'30 seconds'}
|
||||
${59} | ${'59 seconds'}
|
||||
${60} | ${'1 minute'}
|
||||
${-60} | ${'1 minute'}
|
||||
${2 * 60} | ${'2 minutes'}
|
||||
${60 * 60} | ${'1 hour'}
|
||||
${2 * 60 * 60} | ${'2 hours'}
|
||||
${ONE_DAY} | ${'1 day'}
|
||||
${2 * ONE_DAY} | ${'2 days'}
|
||||
${7 * ONE_DAY} | ${'1 week'}
|
||||
${14 * ONE_DAY} | ${'2 weeks'}
|
||||
${31 * ONE_DAY} | ${'1 month'}
|
||||
${61 * ONE_DAY} | ${'2 months'}
|
||||
${365 * ONE_DAY} | ${'1 year'}
|
||||
${365 * 2 * ONE_DAY} | ${'2 years'}
|
||||
`('formats $secs as "$formatted"', ({ secs, formatted }) => {
|
||||
const ms = secs * 1000;
|
||||
|
||||
expect(duration(ms)).toBe(formatted);
|
||||
});
|
||||
|
||||
// `duration` can be used to format Rails month durations.
|
||||
// Ensure formatting for quantities such as `2.months.to_i`
|
||||
// based on ActiveSupport::Duration::SECONDS_PER_MONTH.
|
||||
// See: https://api.rubyonrails.org/classes/ActiveSupport/Duration.html
|
||||
const SECONDS_PER_MONTH = 2629746; // 1.month.to_i
|
||||
|
||||
it.each`
|
||||
duration | secs | formatted
|
||||
${'1.month'} | ${SECONDS_PER_MONTH} | ${'1 month'}
|
||||
${'2.months'} | ${SECONDS_PER_MONTH * 2} | ${'2 months'}
|
||||
${'3.months'} | ${SECONDS_PER_MONTH * 3} | ${'3 months'}
|
||||
`(
|
||||
'formats ActiveSupport::Duration of `$duration` ($secs) as "$formatted"',
|
||||
({ secs, formatted }) => {
|
||||
const ms = secs * 1000;
|
||||
|
||||
expect(duration(ms)).toBe(formatted);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('localTimeAgo', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML =
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlSprintf } from '@gitlab/ui';
|
||||
import { nextTick } from 'vue';
|
||||
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||
import HarborListHeader from '~/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue';
|
||||
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
|
||||
import {
|
||||
HARBOR_REGISTRY_TITLE,
|
||||
LIST_INTRO_TEXT,
|
||||
} from '~/packages_and_registries/harbor_registry/constants/index';
|
||||
|
||||
describe('harbor_list_header', () => {
|
||||
let wrapper;
|
||||
|
||||
const findTitleArea = () => wrapper.find(TitleArea);
|
||||
const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]');
|
||||
const findImagesMetaDataItem = () => wrapper.find(MetadataItem);
|
||||
|
||||
const mountComponent = async (propsData, slots) => {
|
||||
wrapper = shallowMount(HarborListHeader, {
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
TitleArea,
|
||||
},
|
||||
propsData,
|
||||
slots,
|
||||
});
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('header', () => {
|
||||
it('has a title', () => {
|
||||
mountComponent({ metadataLoading: true });
|
||||
|
||||
expect(findTitleArea().props()).toMatchObject({
|
||||
title: HARBOR_REGISTRY_TITLE,
|
||||
metadataLoading: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('has a commands slot', () => {
|
||||
mountComponent(null, { commands: '<div data-testid="commands-slot">baz</div>' });
|
||||
|
||||
expect(findCommandsSlot().text()).toBe('baz');
|
||||
});
|
||||
|
||||
describe('sub header parts', () => {
|
||||
describe('images count', () => {
|
||||
it('exists', async () => {
|
||||
await mountComponent({ imagesCount: 1 });
|
||||
|
||||
expect(findImagesMetaDataItem().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('when there is one image', async () => {
|
||||
await mountComponent({ imagesCount: 1 });
|
||||
|
||||
expect(findImagesMetaDataItem().props()).toMatchObject({
|
||||
text: '1 Image repository',
|
||||
icon: 'container-image',
|
||||
});
|
||||
});
|
||||
|
||||
it('when there is more than one image', async () => {
|
||||
await mountComponent({ imagesCount: 3 });
|
||||
|
||||
expect(findImagesMetaDataItem().props('text')).toBe('3 Image repositories');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('info messages', () => {
|
||||
describe('default message', () => {
|
||||
it('is correctly bound to title_area props', () => {
|
||||
mountComponent({ helpPagePath: 'foo' });
|
||||
|
||||
expect(findTitleArea().props('infoMessages')).toEqual([
|
||||
{ text: LIST_INTRO_TEXT, link: 'foo' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,99 @@
|
|||
import { shallowMount, RouterLinkStub as RouterLink } from '@vue/test-utils';
|
||||
import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
|
||||
|
||||
import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue';
|
||||
import ListItem from '~/vue_shared/components/registry/list_item.vue';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import { harborListResponse } from '../../mock_data';
|
||||
|
||||
describe('Harbor List Row', () => {
|
||||
let wrapper;
|
||||
const [item] = harborListResponse.repositories;
|
||||
|
||||
const findDetailsLink = () => wrapper.find(RouterLink);
|
||||
const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
|
||||
const findTagsCount = () => wrapper.find('[data-testid="tags-count"]');
|
||||
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
||||
|
||||
const mountComponent = (props) => {
|
||||
wrapper = shallowMount(HarborListRow, {
|
||||
stubs: {
|
||||
RouterLink,
|
||||
GlSprintf,
|
||||
ListItem,
|
||||
},
|
||||
propsData: {
|
||||
item,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('image title and path', () => {
|
||||
it('contains a link to the details page', () => {
|
||||
mountComponent();
|
||||
|
||||
const link = findDetailsLink();
|
||||
expect(link.text()).toBe(item.name);
|
||||
expect(findDetailsLink().props('to')).toMatchObject({
|
||||
name: 'details',
|
||||
params: {
|
||||
id: item.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('contains a clipboard button', () => {
|
||||
mountComponent();
|
||||
const button = findClipboardButton();
|
||||
expect(button.exists()).toBe(true);
|
||||
expect(button.props('text')).toBe(item.location);
|
||||
expect(button.props('title')).toBe(item.location);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tags count', () => {
|
||||
it('exists', () => {
|
||||
mountComponent();
|
||||
expect(findTagsCount().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('contains a tag icon', () => {
|
||||
mountComponent();
|
||||
const icon = findTagsCount().find(GlIcon);
|
||||
expect(icon.exists()).toBe(true);
|
||||
expect(icon.props('name')).toBe('tag');
|
||||
});
|
||||
|
||||
describe('loading state', () => {
|
||||
it('shows a loader when metadataLoading is true', () => {
|
||||
mountComponent({ metadataLoading: true });
|
||||
|
||||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('hides the tags count while loading', () => {
|
||||
mountComponent({ metadataLoading: true });
|
||||
|
||||
expect(findTagsCount().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tags count text', () => {
|
||||
it('with one tag in the image', () => {
|
||||
mountComponent({ item: { ...item, artifactCount: 1 } });
|
||||
|
||||
expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag');
|
||||
});
|
||||
it('with more than one tag in the image', () => {
|
||||
mountComponent({ item: { ...item, artifactCount: 3 } });
|
||||
|
||||
expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue';
|
||||
import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue';
|
||||
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
|
||||
import { harborListResponse } from '../../mock_data';
|
||||
|
||||
describe('Harbor List', () => {
|
||||
let wrapper;
|
||||
|
||||
const findHarborListRow = () => wrapper.findAll(HarborListRow);
|
||||
|
||||
const mountComponent = (props) => {
|
||||
wrapper = shallowMount(HarborList, {
|
||||
stubs: { RegistryList },
|
||||
propsData: {
|
||||
images: harborListResponse.repositories,
|
||||
pageInfo: harborListResponse.pageInfo,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
it('contains one list element for each image', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findHarborListRow().length).toBe(harborListResponse.repositories.length);
|
||||
});
|
||||
|
||||
it('passes down the metadataLoading prop', () => {
|
||||
mountComponent({ metadataLoading: true });
|
||||
expect(findHarborListRow().at(0).props('metadataLoading')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,175 @@
|
|||
export const harborListResponse = {
|
||||
repositories: [
|
||||
{
|
||||
artifactCount: 1,
|
||||
creationTime: '2022-03-02T06:35:53.205Z',
|
||||
id: 25,
|
||||
name: 'shao/flinkx',
|
||||
projectId: 21,
|
||||
pullCount: 0,
|
||||
updateTime: '2022-03-02T06:35:53.205Z',
|
||||
location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
},
|
||||
{
|
||||
artifactCount: 1,
|
||||
creationTime: '2022-03-02T06:35:53.205Z',
|
||||
id: 26,
|
||||
name: 'shao/flinkx1',
|
||||
projectId: 21,
|
||||
pullCount: 0,
|
||||
updateTime: '2022-03-02T06:35:53.205Z',
|
||||
location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
},
|
||||
{
|
||||
artifactCount: 1,
|
||||
creationTime: '2022-03-02T06:35:53.205Z',
|
||||
id: 27,
|
||||
name: 'shao/flinkx2',
|
||||
projectId: 21,
|
||||
pullCount: 0,
|
||||
updateTime: '2022-03-02T06:35:53.205Z',
|
||||
location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
|
||||
},
|
||||
],
|
||||
totalCount: 3,
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const harborTagsResponse = {
|
||||
tags: [
|
||||
{
|
||||
digest: 'sha256:7f386a1844faf341353e1c20f2f39f11f397604fedc475435d13f756eeb235d1',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
|
||||
name: '02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c',
|
||||
revision: 'f53bde3d44699e04e11cf15fb415046a0913e2623d878d89bc21adb2cbda5255',
|
||||
shortRevision: 'f53bde3d4',
|
||||
createdAt: '2022-03-02T23:59:05+00:00',
|
||||
totalSize: '6623124',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:4554416b84c4568fe93086620b637064ed029737aabe7308b96d50e3d9d92ed7',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
|
||||
name: '02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160',
|
||||
revision: 'e1fe52d8bab66d71bd54a6b8784d3b9edbc68adbd6ea87f5fa44d9974144ef9e',
|
||||
shortRevision: 'e1fe52d8b',
|
||||
createdAt: '2022-02-10T01:09:56+00:00',
|
||||
totalSize: '920760',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:14f37b60e52b9ce0e9f8f7094b311d265384798592f783487c30aaa3d58e6345',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
|
||||
name: '03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a',
|
||||
revision: 'c72770c6eb93c421bc496964b4bffc742b1ec2e642cdab876be7afda1856029f',
|
||||
shortRevision: 'c72770c6e',
|
||||
createdAt: '2021-12-22T04:48:48+00:00',
|
||||
totalSize: '48609053',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:e925e3b8277ea23f387ed5fba5e78280cfac7cfb261a78cf046becf7b6a3faae',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
|
||||
name: '03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19',
|
||||
revision: '1ac2a43194f4e15166abdf3f26e6ec92215240490b9cac834d63de1a3d87494a',
|
||||
shortRevision: '1ac2a4319',
|
||||
createdAt: '2022-03-09T11:02:27+00:00',
|
||||
totalSize: '35141894',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:7d8303fd5c077787a8c879f8f66b69e2b5605f48ccd3f286e236fb0749fcc1ca',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
|
||||
name: '05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda',
|
||||
revision: 'cf8fee086701016e1a84e6824f0c896951fef4cce9d4745459558b87eec3232c',
|
||||
shortRevision: 'cf8fee086',
|
||||
createdAt: '2022-01-21T11:31:43+00:00',
|
||||
totalSize: '48716070',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:b33611cefe20e4a41a6e0dce356a5d7ef3c177ea7536a58652f5b3a4f2f83549',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
|
||||
name: '093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a',
|
||||
revision: '1a4b48198b13d55242c5164e64d41c4e9f75b5d9506bc6e0efc1534dd0dd1f15',
|
||||
shortRevision: '1a4b48198',
|
||||
createdAt: '2022-01-21T11:31:51+00:00',
|
||||
totalSize: '6623127',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:d25c3c020e2dbd4711a67b9fe308f4cbb7b0bb21815e722a02f91c570dc5d519',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
|
||||
name: '09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7',
|
||||
revision: '03e2e2777dde01c30469ee8c710973dd08a7a4f70494d7dc1583c24b525d7f61',
|
||||
shortRevision: '03e2e2777',
|
||||
createdAt: '2022-03-02T23:58:20+00:00',
|
||||
totalSize: '911377',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:fb760e4d2184e9e8e39d6917534d4610fe01009734698a5653b2de1391ba28f4',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
|
||||
name: '09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95',
|
||||
revision: '350e78d60646bf6967244448c6aaa14d21ecb9a0c6cf87e9ff0361cbe59b9012',
|
||||
shortRevision: '350e78d60',
|
||||
createdAt: '2022-01-19T13:49:14+00:00',
|
||||
totalSize: '48710241',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:407250f380cea92729cbc038c420e74900f53b852e11edc6404fe75a0fd2c402',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
|
||||
name: '0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557',
|
||||
revision: '76038370b7f3904364891457c4a6a234897255e6b9f45d0a852bf3a7e5257e18',
|
||||
shortRevision: '76038370b',
|
||||
createdAt: '2022-01-24T12:56:22+00:00',
|
||||
totalSize: '280065',
|
||||
},
|
||||
{
|
||||
digest: 'sha256:ada87f25218542951ce6720c27f3d0758e90c2540bd129f5cfb9e15b31e07b07',
|
||||
location:
|
||||
'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
|
||||
path:
|
||||
'gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
|
||||
name: '0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb',
|
||||
revision: '3d4b49a7bbb36c48bb721f4d0e76e7950bec3878ee29cdfdd6da39f575d6d37f',
|
||||
shortRevision: '3d4b49a7b',
|
||||
createdAt: '2022-02-17T17:37:52+00:00',
|
||||
totalSize: '48655767',
|
||||
},
|
||||
],
|
||||
totalCount: 100,
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const dockerCommands = {
|
||||
dockerBuildCommand: 'foofoo',
|
||||
dockerPushCommand: 'barbar',
|
||||
dockerLoginCommand: 'bazbaz',
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import component from '~/packages_and_registries/harbor_registry/pages/index.vue';
|
||||
|
||||
describe('List Page', () => {
|
||||
let wrapper;
|
||||
|
||||
const findRouterView = () => wrapper.find({ ref: 'router-view' });
|
||||
|
||||
const mountComponent = () => {
|
||||
wrapper = shallowMount(component, {
|
||||
stubs: {
|
||||
RouterView: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it('has a router view', () => {
|
||||
expect(findRouterView().exists()).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,140 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import { GlSkeletonLoader } from '@gitlab/ui';
|
||||
import HarborListHeader from '~/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue';
|
||||
import HarborRegistryList from '~/packages_and_registries/harbor_registry/pages/list.vue';
|
||||
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
// import { harborListResponse } from '~/packages_and_registries/harbor_registry/mock_api.js';
|
||||
import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue';
|
||||
import CliCommands from '~/packages_and_registries/shared/components/cli_commands.vue';
|
||||
import { SORT_FIELDS } from '~/packages_and_registries/harbor_registry/constants/index';
|
||||
import { harborListResponse, dockerCommands } from '../mock_data';
|
||||
|
||||
let mockHarborListResponse;
|
||||
jest.mock('~/packages_and_registries/harbor_registry/mock_api.js', () => ({
|
||||
harborListResponse: () => mockHarborListResponse,
|
||||
}));
|
||||
|
||||
describe('Harbor List Page', () => {
|
||||
let wrapper;
|
||||
|
||||
const waitForHarborPageRequest = async () => {
|
||||
await waitForPromises();
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockHarborListResponse = Promise.resolve(harborListResponse);
|
||||
});
|
||||
|
||||
const findHarborListHeader = () => wrapper.findComponent(HarborListHeader);
|
||||
const findPersistedSearch = () => wrapper.findComponent(PersistedSearch);
|
||||
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
||||
const findHarborList = () => wrapper.findComponent(HarborList);
|
||||
const findCliCommands = () => wrapper.findComponent(CliCommands);
|
||||
|
||||
const fireFirstSortUpdate = () => {
|
||||
findPersistedSearch().vm.$emit('update', { sort: 'UPDATED_DESC', filters: [] });
|
||||
};
|
||||
|
||||
const mountComponent = ({ config = { isGroupPage: false } } = {}) => {
|
||||
wrapper = shallowMount(HarborRegistryList, {
|
||||
stubs: {
|
||||
HarborListHeader,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
config,
|
||||
...dockerCommands,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('contains harbor registry header', async () => {
|
||||
mountComponent();
|
||||
fireFirstSortUpdate();
|
||||
await waitForHarborPageRequest();
|
||||
await nextTick();
|
||||
|
||||
expect(findHarborListHeader().exists()).toBe(true);
|
||||
expect(findHarborListHeader().props()).toMatchObject({
|
||||
imagesCount: 3,
|
||||
metadataLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLoading is true', () => {
|
||||
it('shows the skeleton loader', async () => {
|
||||
mountComponent();
|
||||
fireFirstSortUpdate();
|
||||
|
||||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('harborList is not visible', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findHarborList().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('cli commands is not visible', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findCliCommands().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('title has the metadataLoading props set to true', async () => {
|
||||
mountComponent();
|
||||
fireFirstSortUpdate();
|
||||
|
||||
expect(findHarborListHeader().props('metadataLoading')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('list is not empty', () => {
|
||||
describe('unfiltered state', () => {
|
||||
it('quick start is visible', async () => {
|
||||
mountComponent();
|
||||
fireFirstSortUpdate();
|
||||
|
||||
await waitForHarborPageRequest();
|
||||
await nextTick();
|
||||
|
||||
expect(findCliCommands().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('list component is visible', async () => {
|
||||
mountComponent();
|
||||
fireFirstSortUpdate();
|
||||
|
||||
await waitForHarborPageRequest();
|
||||
await nextTick();
|
||||
|
||||
expect(findHarborList().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search and sorting', () => {
|
||||
it('has a persisted search box element', async () => {
|
||||
mountComponent();
|
||||
fireFirstSortUpdate();
|
||||
await waitForHarborPageRequest();
|
||||
await nextTick();
|
||||
|
||||
const harborRegistrySearch = findPersistedSearch();
|
||||
expect(harborRegistrySearch.exists()).toBe(true);
|
||||
expect(harborRegistrySearch.props()).toMatchObject({
|
||||
defaultOrder: 'UPDATED',
|
||||
defaultSort: 'desc',
|
||||
sortableFields: SORT_FIELDS,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -56,7 +56,7 @@ describe('Global Search Store Actions', () => {
|
|||
${actions.fetchGroups} | ${{ method: 'onGet', code: 200, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${0}
|
||||
${actions.fetchGroups} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${1}
|
||||
${actions.fetchProjects} | ${{ method: 'onGet', code: 200, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${0}
|
||||
${actions.fetchProjects} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${2}
|
||||
${actions.fetchProjects} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${1}
|
||||
`(`axios calls`, ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
|
||||
describe(action.name, () => {
|
||||
describe(`on ${type}`, () => {
|
||||
|
@ -121,8 +121,8 @@ describe('Global Search Store Actions', () => {
|
|||
|
||||
describe('when groupId is set', () => {
|
||||
it('calls Api.groupProjects with expected parameters', () => {
|
||||
actions.fetchProjects({ commit: mockCommit, state });
|
||||
|
||||
const callbackTest = jest.fn();
|
||||
actions.fetchProjects({ commit: mockCommit, state }, undefined, callbackTest);
|
||||
expect(Api.groupProjects).toHaveBeenCalledWith(
|
||||
state.query.group_id,
|
||||
state.query.search,
|
||||
|
@ -131,7 +131,8 @@ describe('Global Search Store Actions', () => {
|
|||
include_subgroups: true,
|
||||
with_shared: false,
|
||||
},
|
||||
expect.any(Function),
|
||||
callbackTest,
|
||||
true,
|
||||
);
|
||||
expect(Api.projects).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -144,15 +145,10 @@ describe('Global Search Store Actions', () => {
|
|||
|
||||
it('calls Api.projects', () => {
|
||||
actions.fetchProjects({ commit: mockCommit, state });
|
||||
|
||||
expect(Api.groupProjects).not.toHaveBeenCalled();
|
||||
expect(Api.projects).toHaveBeenCalledWith(
|
||||
state.query.search,
|
||||
{
|
||||
order_by: 'similarity',
|
||||
},
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(Api.projects).toHaveBeenCalledWith(state.query.search, {
|
||||
order_by: 'similarity',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::RunnerReleases do
|
||||
subject { described_class.instance }
|
||||
|
||||
describe '#releases' do
|
||||
before do
|
||||
subject.reset!
|
||||
|
||||
stub_application_setting(public_runner_releases_url: 'the release API URL')
|
||||
allow(Gitlab::HTTP).to receive(:try_get).with('the release API URL').once { mock_http_response(response) }
|
||||
end
|
||||
|
||||
def releases
|
||||
subject.releases
|
||||
end
|
||||
|
||||
shared_examples 'requests that follow cache status' do |validity_period|
|
||||
context "almost #{validity_period.inspect} later" do
|
||||
let(:followup_request_interval) { validity_period - 0.001.seconds }
|
||||
|
||||
it 'returns cached releases' do
|
||||
releases
|
||||
|
||||
travel followup_request_interval do
|
||||
expect(Gitlab::HTTP).not_to receive(:try_get)
|
||||
|
||||
expect(releases).to eq(expected_result)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "after #{validity_period.inspect}" do
|
||||
let(:followup_request_interval) { validity_period + 1.second }
|
||||
let(:followup_response) { (response || []) + [{ 'name' => 'v14.9.2' }] }
|
||||
|
||||
it 'checks new releases' do
|
||||
releases
|
||||
|
||||
travel followup_request_interval do
|
||||
expect(Gitlab::HTTP).to receive(:try_get).with('the release API URL').once { mock_http_response(followup_response) }
|
||||
|
||||
expect(releases).to eq((expected_result || []) + [Gitlab::VersionInfo.new(14, 9, 2)])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when response is nil' do
|
||||
let(:response) { nil }
|
||||
let(:expected_result) { nil }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(releases).to be_nil
|
||||
end
|
||||
|
||||
it_behaves_like 'requests that follow cache status', 5.seconds
|
||||
|
||||
it 'performs exponential backoff on requests', :aggregate_failures do
|
||||
start_time = Time.now.utc.change(usec: 0)
|
||||
|
||||
http_call_timestamp_offsets = []
|
||||
allow(Gitlab::HTTP).to receive(:try_get).with('the release API URL') do
|
||||
http_call_timestamp_offsets << Time.now.utc - start_time
|
||||
mock_http_response(response)
|
||||
end
|
||||
|
||||
# An initial HTTP request fails
|
||||
travel_to(start_time)
|
||||
subject.reset!
|
||||
expect(releases).to be_nil
|
||||
|
||||
# Successive failed requests result in HTTP requests only after specific backoff periods
|
||||
backoff_periods = [5, 10, 20, 40, 80, 160, 320, 640, 1280, 2560, 3600].map(&:seconds)
|
||||
backoff_periods.each do |period|
|
||||
travel(period - 1.second)
|
||||
expect(releases).to be_nil
|
||||
|
||||
travel 1.second
|
||||
expect(releases).to be_nil
|
||||
end
|
||||
|
||||
expect(http_call_timestamp_offsets).to eq([0, 5, 15, 35, 75, 155, 315, 635, 1275, 2555, 5115, 8715])
|
||||
|
||||
# Finally a successful HTTP request results in releases being returned
|
||||
allow(Gitlab::HTTP).to receive(:try_get).with('the release API URL').once { mock_http_response([{ 'name' => 'v14.9.1' }]) }
|
||||
travel 1.hour
|
||||
expect(releases).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when response is not nil' do
|
||||
let(:response) { [{ 'name' => 'v14.9.1' }, { 'name' => 'v14.9.0' }] }
|
||||
let(:expected_result) { [Gitlab::VersionInfo.new(14, 9, 0), Gitlab::VersionInfo.new(14, 9, 1)] }
|
||||
|
||||
it 'returns parsed and sorted Gitlab::VersionInfo objects' do
|
||||
expect(releases).to eq(expected_result)
|
||||
end
|
||||
|
||||
it_behaves_like 'requests that follow cache status', 1.day
|
||||
end
|
||||
|
||||
def mock_http_response(response)
|
||||
http_response = instance_double(HTTParty::Response)
|
||||
|
||||
allow(http_response).to receive(:success?).and_return(response.present?)
|
||||
allow(http_response).to receive(:parsed_response).and_return(response)
|
||||
|
||||
http_response
|
||||
end
|
||||
end
|
||||
end
|
|
@ -58,7 +58,19 @@ RSpec.describe Projects::ContainerRepository::ThirdParty::DeleteTagsService do
|
|||
stub_put_manifest_request('Ba', 500, {})
|
||||
end
|
||||
|
||||
it { is_expected.to eq(status: :error, message: 'could not delete tags') }
|
||||
it { is_expected.to eq(status: :error, message: "could not delete tags: #{tags.join(', ')}")}
|
||||
|
||||
context 'when a large list of tag updates fails' do
|
||||
let(:tags) { Array.new(1000) { |i| "tag_#{i}" } }
|
||||
|
||||
before do
|
||||
expect(service).to receive(:replace_tag_manifests).and_return({})
|
||||
end
|
||||
|
||||
it 'truncates the log message' do
|
||||
expect(subject).to eq(status: :error, message: "could not delete tags: #{tags.join(', ')}".truncate(1000))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'a single tag update fails' do
|
||||
|
|
|
@ -726,6 +726,17 @@ RSpec.describe QuickActions::InterpretService do
|
|||
|
||||
expect(reviewer).to be_attention_requested
|
||||
end
|
||||
|
||||
it 'supports attn alias' do
|
||||
attn_cmd = content.gsub(/attention/, 'attn')
|
||||
_, _, message = service.execute(attn_cmd, issuable)
|
||||
|
||||
expect(message).to eq("Requested attention from #{developer.to_reference}.")
|
||||
|
||||
reviewer.reload
|
||||
|
||||
expect(reviewer).to be_attention_requested
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'remove attention command' do
|
||||
|
|
|
@ -20,6 +20,9 @@ RSpec.configure do |config|
|
|||
# We drop and recreate the database if any table has more than 1200 columns, just to be safe.
|
||||
if any_connection_class_with_more_than_allowed_columns?
|
||||
recreate_all_databases!
|
||||
|
||||
# Seed required data as recreating DBs will delete it
|
||||
TestEnv.seed_db
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -7,14 +7,14 @@ RSpec::Matchers.define :require_graphql_authorizations do |*expected|
|
|||
if klass.respond_to?(:required_permissions)
|
||||
klass.required_permissions
|
||||
else
|
||||
[klass.to_graphql.metadata[:authorize]]
|
||||
Array.wrap(klass.authorize)
|
||||
end
|
||||
end
|
||||
|
||||
match do |klass|
|
||||
actual = permissions_for(klass)
|
||||
|
||||
expect(actual).to match_array(expected)
|
||||
expect(actual).to match_array(expected.compact)
|
||||
end
|
||||
|
||||
failure_message do |klass|
|
||||
|
@ -213,16 +213,16 @@ RSpec::Matchers.define :have_graphql_resolver do |expected|
|
|||
match do |field|
|
||||
case expected
|
||||
when Method
|
||||
expect(field.to_graphql.metadata[:type_class].resolve_proc).to eq(expected)
|
||||
expect(field.type_class.resolve_proc).to eq(expected)
|
||||
else
|
||||
expect(field.to_graphql.metadata[:type_class].resolver).to eq(expected)
|
||||
expect(field.type_class.resolver).to eq(expected)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :have_graphql_extension do |expected|
|
||||
match do |field|
|
||||
expect(field.to_graphql.metadata[:type_class].extensions).to include(expected)
|
||||
expect(field.type_class.extensions).to include(expected)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'shared/groups/_dropdown.html.haml' do
|
||||
describe 'render' do
|
||||
describe 'when a sort option is not selected' do
|
||||
it 'renders a default sort option' do
|
||||
render 'shared/groups/dropdown'
|
||||
|
||||
expect(rendered).to have_content 'Last created'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when a sort option is selected' do
|
||||
before do
|
||||
assign(:sort, 'name_desc')
|
||||
|
||||
render 'shared/groups/dropdown'
|
||||
end
|
||||
|
||||
it 'renders the selected sort option' do
|
||||
expect(rendered).to have_content 'Name, descending'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue