Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-30 15:09:00 +00:00
parent e672c333df
commit f4199ade27
66 changed files with 2037 additions and 102 deletions

View File

@ -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:

View File

@ -1 +1 @@
13.24.1
13.24.2

View File

@ -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();
});
},

View File

@ -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();
};

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 */

View File

@ -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');

View File

@ -0,0 +1,3 @@
export * from './common';
export * from './list';
export * from './details';

View File

@ -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,
];

View File

@ -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,
};
};

View File

@ -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);
};

View File

@ -0,0 +1,5 @@
<template>
<div>
<router-view ref="router-view" />
</div>
</template>

View File

@ -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>

View File

@ -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;
}

View File

@ -13,7 +13,8 @@ export default {
props: {
title: {
type: String,
required: true,
default: '',
required: false,
},
isLoading: {
type: Boolean,

View File

@ -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();
}

View File

@ -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();

View File

@ -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();
}

View File

@ -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: {

View File

@ -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);
}
};

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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
)

View File

@ -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 } }

View File

@ -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

View File

@ -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, } }

View File

@ -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 }

View File

@ -2771,7 +2771,7 @@
:worker_name: ProjectExportWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :throttled
:urgency: :low
:resource_boundary: :memory
:weight: 1
:idempotent:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
610c5ded785f174d195a660062bb74e718bfd5a38b13773215e20e8f95c59da4

View File

@ -0,0 +1 @@
9f597a462768531b0c6ad23e6e1a52edb765724518e1cebc0684160b030d6225

View File

@ -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)),

View File

@ -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`. |

View File

@ -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

View File

@ -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)).

View File

@ -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

View File

@ -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)

View File

@ -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|

View File

@ -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

View File

@ -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, youll be asked for URLs from GitLabs 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 ""

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -49,6 +49,7 @@ const noop = () => {};
* expectedActions: [],
* })
*/
export default (
actionArg,
payloadArg,

View File

@ -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', () => {

View File

@ -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 =

View File

@ -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' },
]);
});
});
});
});

View File

@ -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');
});
});
});
});

View File

@ -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);
});
});
});

View File

@ -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',
};

View File

@ -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);
});
});

View File

@ -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,
});
});
});
});
});

View File

@ -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',
});
});
});
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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