Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-03-21 12:09:23 +00:00
parent daf5e72855
commit e5de3582ae
31 changed files with 476 additions and 107 deletions

View File

@ -181,6 +181,7 @@ class GfmAutoComplete {
skipSpecialCharacterTest: true,
skipMarkdownCharacterTest: true,
data: GfmAutoComplete.defaultLoadingData,
maxLen: 100,
displayTpl(value) {
const cssClasses = [];
@ -297,6 +298,7 @@ class GfmAutoComplete {
at: '/submit_review ',
alias: 'submit_review',
data: Object.keys(REVIEW_STATES),
maxLen: 100,
displayTpl({ name }) {
const reviewState = REVIEW_STATES[name];
@ -315,6 +317,7 @@ class GfmAutoComplete {
insertTpl: GfmAutoComplete.Emoji.insertTemplateFunction,
skipSpecialCharacterTest: true,
data: GfmAutoComplete.defaultLoadingData,
maxLen: 100,
callbacks: {
...this.getDefaultCallbacks(),
matcher(flag, subtext) {
@ -373,6 +376,7 @@ class GfmAutoComplete {
$input.atwho({
at: '@',
alias: USERS_ALIAS,
maxLen: 100,
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
const { avatarTag, username, title, icon, availability } = value;
@ -478,6 +482,7 @@ class GfmAutoComplete {
at: '#',
alias: ISSUES_ALIAS,
searchKey: 'search',
maxLen: 100,
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
@ -515,6 +520,7 @@ class GfmAutoComplete {
searchKey: 'search',
// eslint-disable-next-line no-template-curly-in-string
insertTpl: '${atwho-at}${title}',
maxLen: 100,
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
@ -570,6 +576,7 @@ class GfmAutoComplete {
at: '!',
alias: MERGEREQUESTS_ALIAS,
searchKey: 'search',
maxLen: 100,
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
@ -616,6 +623,7 @@ class GfmAutoComplete {
alias: LABELS_ALIAS,
searchKey: 'search',
data: GfmAutoComplete.defaultLoadingData,
maxLen: 100,
displayTpl(value) {
let tmpl = GfmAutoComplete.Labels.templateFunction(value.color, value.title);
if (GfmAutoComplete.isLoading(value)) {
@ -703,6 +711,7 @@ class GfmAutoComplete {
at: '$',
alias: SNIPPETS_ALIAS,
searchKey: 'search',
maxLen: 100,
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
@ -741,6 +750,7 @@ class GfmAutoComplete {
suffix: ']',
alias: CONTACTS_ALIAS,
searchKey: 'search',
maxLen: 100,
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.email != null) {

View File

@ -1,6 +1,6 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlLoadingIcon, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { GlButton, GlLoadingIcon, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
const MARK_TEXT = __('Mark as done');
@ -8,6 +8,7 @@ const TODO_TEXT = __('Add a to do');
export default {
components: {
GlButton,
GlIcon,
GlLoadingIcon,
},
@ -42,8 +43,11 @@ export default {
computed: {
buttonClasses() {
return this.collapsed
? 'btn-blank btn-todo sidebar-collapsed-icon js-dont-change-state'
: 'gl-button btn btn-default btn-todo issuable-header-btn gl-float-right';
? 'sidebar-collapsed-icon js-dont-change-state'
: 'issuable-header-btn gl-float-right';
},
buttonVariant() {
return this.collapsed ? 'link' : 'default';
},
buttonLabel() {
return this.isTodo ? MARK_TEXT : TODO_TEXT;
@ -70,13 +74,15 @@ export default {
</script>
<template>
<button
<gl-button
v-gl-tooltip.left.viewport
:class="buttonClasses"
:variant="buttonVariant"
:title="buttonTooltip"
:aria-label="buttonLabel"
:data-issuable-id="issuableId"
:data-issuable-type="issuableType"
size="small"
type="button"
@click="handleButtonClick"
>
@ -85,7 +91,7 @@ export default {
:class="collapsedButtonIconClasses"
:name="collapsedButtonIcon"
/>
<span v-show="!collapsed" class="issuable-todo-inner">{{ buttonLabel }}</span>
<gl-loading-icon v-show="isActionActive" size="sm" :inline="true" />
</button>
<span v-if="!collapsed" class="issuable-todo-inner">{{ buttonLabel }}</span>
<gl-loading-icon v-if="isActionActive" size="sm" inline />
</gl-button>
</template>

View File

@ -51,12 +51,12 @@ export default {
v-for="option in defaultVisibilityLevels"
:key="option.value"
:value="option.value"
class="mb-3"
class="gl-mb-3"
>
<div class="d-flex gl-align-items-center">
<div class="gl-display-flex gl-align-items-center">
<gl-icon :size="16" :name="option.icon" />
<span
class="font-weight-bold ml-1 js-visibility-option"
class="gl-font-weight-semibold gl-ml-2 js-visibility-option"
data-testid="visibility-content"
:data-qa-visibility="option.label"
>{{ option.label }}</span
@ -71,7 +71,7 @@ export default {
</gl-form-radio-group>
</gl-form-group>
<div class="text-muted" data-testid="restricted-levels-info">
<div class="gl-text-secondary" data-testid="restricted-levels-info">
<template v-if="!defaultVisibilityLevels.length">{{
$options.SNIPPET_LEVELS_DISABLED
}}</template>

View File

@ -26,9 +26,6 @@ export default {
<template>
<div class="block gl-display-flex gl-justify-content-space-between gl-border-b-gray-100!">
<span class="issuable-header-text hide-collapsed">
{{ __('To Do') }}
</span>
<sidebar-todo
v-if="!sidebarCollapsed"
:project-path="projectPath"

View File

@ -138,8 +138,8 @@ export default {
<template>
<div
:class="{
'block todo': sidebarCollapsed,
'gl-ml-auto': !sidebarCollapsed,
block: sidebarCollapsed,
'gl-display-inline-flex gl-flex-basis-full': !sidebarCollapsed,
}"
>
<todo

View File

@ -1,5 +1,5 @@
<script>
import { GlAlert, GlButton, GlForm, GlFormGroup } from '@gitlab/ui';
import { GlAlert, GlButton, GlForm, GlFormGroup, GlFormTextarea } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { helpPagePath } from '~/helpers/help_page_helper';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
@ -22,6 +22,7 @@ export default {
GlButton,
GlForm,
GlFormGroup,
GlFormTextarea,
MarkdownEditor,
WorkItemDescriptionRendered,
},
@ -312,11 +313,12 @@ export default {
</p>
<details class="gl-mb-5">
<summary class="gl-text-blue-500">{{ s__('WorkItem|View current version') }}</summary>
<textarea
class="note-textarea js-gfm-input js-autosize markdown-area gl-p-3"
<gl-form-textarea
class="js-gfm-input js-autosize markdown-area gl-font-monospace!"
data-testid="conflicted-description"
readonly
:value="conflictedDescription"
></textarea>
/>
</details>
<template #actions>
<gl-button

View File

@ -1,6 +1,6 @@
<script>
import { isEmpty } from 'lodash';
import { GlAlert, GlSkeletonLoader, GlButton, GlTooltipDirective, GlEmptyState } from '@gitlab/ui';
import { GlAlert, GlButton, GlTooltipDirective, GlEmptyState } from '@gitlab/ui';
import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg?raw';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { s__ } from '~/locale';
@ -46,6 +46,7 @@ import WorkItemRelationships from './work_item_relationships/work_item_relations
import WorkItemStickyHeader from './work_item_sticky_header.vue';
import WorkItemAncestors from './work_item_ancestors/work_item_ancestors.vue';
import WorkItemTitleWithEdit from './work_item_title_with_edit.vue';
import WorkItemLoading from './work_item_loading.vue';
export default {
i18n,
@ -56,7 +57,6 @@ export default {
components: {
GlAlert,
GlButton,
GlSkeletonLoader,
GlEmptyState,
WorkItemActions,
WorkItemTodos,
@ -74,6 +74,7 @@ export default {
WorkItemStickyHeader,
WorkItemAncestors,
WorkItemTitleWithEdit,
WorkItemLoading,
},
mixins: [glFeatureFlagMixin()],
inject: ['fullPath', 'isGroup', 'reportAbusePath'],
@ -428,11 +429,8 @@ export default {
</gl-alert>
</section>
<section :class="workItemBodyClass">
<div v-if="workItemLoading" class="gl-max-w-26 gl-py-5">
<gl-skeleton-loader :height="65" :width="240">
<rect width="240" height="20" x="5" y="0" rx="4" />
<rect width="100" height="20" x="5" y="45" rx="4" />
</gl-skeleton-loader>
<div v-if="workItemLoading">
<work-item-loading :two-column-view="workItemsBetaEnabled" />
</div>
<template v-else>
<div class="gl-sm-display-none! gl-display-flex">

View File

@ -0,0 +1,135 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
export default {
name: 'WorkItemLoading',
loader: {
repeat: 10,
width: 1000,
height: 40,
descriptionRepeat: 2,
attributesRepeat: 6,
},
components: {
GlSkeletonLoader,
},
props: {
twoColumnView: {
type: Boolean,
required: false,
default: false,
},
},
};
</script>
<template>
<div v-if="twoColumnView" class="work-item-overview" data-testid="work-item-two-column-loading">
<section>
<gl-skeleton-loader :height="60" :width="600" data-testid="work-title-and-meta-loading">
<!--- START work item title -->
<rect width="500" height="20" x="5" y="0" rx="4" />
<!--- END work item title -->
<!--- START work item meta -->
<rect width="50" height="15" x="5" y="30" rx="6" />
<rect width="150" height="5" x="60" y="35" rx="2" />
<!--- END work item meta -->
</gl-skeleton-loader>
<!--- START work item description -->
<div data-testid="work-item-description-loading">
<gl-skeleton-loader
v-for="i in $options.loader.descriptionRepeat"
:key="i"
:width="600"
:height="20"
>
<rect width="500" x="5" y="0" height="5" rx="2" />
<rect width="595" x="5" y="10" height="5" rx="2" />
</gl-skeleton-loader>
<gl-skeleton-loader :width="600" :height="20">
<rect width="300" x="5" y="0" height="5" rx="2" />
</gl-skeleton-loader>
<gl-skeleton-loader :width="600" :height="20">
<rect width="500" x="5" y="0" height="5" rx="2" />
<rect width="595" x="5" y="10" height="5" rx="2" />
</gl-skeleton-loader>
<gl-skeleton-loader :width="600" :height="20">
<rect width="300" x="5" y="0" height="5" rx="2" />
</gl-skeleton-loader>
</div>
<!--- END work item description -->
<!--- START work item attributes wrapper small/xs screen -->
<div
class="work-item-attributes-wrapper gl-md-display-none!"
data-testid="work-item-attributes-xssm-loading"
>
<gl-skeleton-loader
v-for="i in $options.loader.attributesRepeat"
:key="i"
:width="240"
:height="20"
>
<rect width="50" x="2" y="0" height="2" rx="1" />
<rect width="100" x="2" y="5" height="4" rx="1" />
</gl-skeleton-loader>
</div>
<!--- END work item attributes wrapper small/xs screen -->
<!--- START work item notes activity placeholder -->
<gl-skeleton-loader
:height="30"
:width="600"
data-testid="work-item-activity-placeholder-loading"
>
<rect width="100" height="15" x="5" y="0" rx="4" />
</gl-skeleton-loader>
<!--- END work item notes activity placeholder -->
<!--- START work item notes -->
<gl-skeleton-loader
v-for="i in $options.loader.repeat"
:key="i"
data-testid="work-item-notes-loading"
:width="$options.loader.width"
:height="$options.loader.height"
preserve-aspect-ratio="xMinYMax meet"
>
<circle cx="20" cy="20" r="16" />
<rect width="950" x="45" y="15" height="10" rx="4" />
</gl-skeleton-loader>
<!--- END work item notes -->
</section>
<!--- START work item attributes wrapper md/lg screens -->
<aside
class="work-item-overview-right-sidebar gl-md-display-block! gl-display-none"
data-testid="work-item-attributes-mdup-loading"
>
<div class="work-item-attributes-wrapper">
<gl-skeleton-loader
v-for="i in $options.loader.attributesRepeat"
:key="i"
:width="240"
:height="50"
>
<rect width="100" x="0" y="25" height="5" rx="4" />
<rect width="240" x="0" y="35" height="10" rx="4" />
</gl-skeleton-loader>
</div>
</aside>
<!--- END work item attributes wrapper md/lg screens -->
</div>
<div v-else data-testid="work-item-single-column-loading" class="gl-max-w-26 gl-py-5">
<!--- START work item loading original loader -->
<gl-skeleton-loader :height="65" :width="240">
<rect width="240" height="20" x="5" y="0" rx="4" />
<rect width="100" height="20" x="5" y="45" rx="4" />
</gl-skeleton-loader>
<!--- END work item loading original loader -->
</div>
</template>

View File

@ -349,14 +349,14 @@ export default {
/>
<div v-if="initialLoading" class="gl-mt-5">
<gl-skeleton-loader
v-for="index in $options.loader.repeat"
:key="index"
:width="$options.loader.width"
v-for="i in $options.loader.repeat"
:key="i"
:width="1000"
:height="$options.loader.height"
preserve-aspect-ratio="xMinYMax meet"
>
<circle cx="20" cy="20" r="16" />
<rect width="500" x="45" y="15" height="10" rx="4" />
<rect width="950" x="45" y="15" height="10" rx="4" />
</gl-skeleton-loader>
</div>
<div v-else class="issuable-discussion gl-mb-5 gl-clearfix!">

View File

@ -392,10 +392,6 @@
margin-left: $gl-spacing-scale-3;
margin-right: 0;
}
.user-access-role {
line-height: $gl-line-height-14;
}
}
@include media-breakpoint-down(md) {
@ -476,7 +472,7 @@
.form-control {
min-width: 100px;
}
&.form-group {
@include media-breakpoint-up(sm) {
margin-bottom: 0;

View File

@ -80,18 +80,6 @@ table.pipeline-project-metrics tr td {
}
}
.user-access-role {
padding-left: $gl-spacing-scale-3;
padding-right: $gl-spacing-scale-3;
display: inline-block;
color: $gl-text-color-secondary;
font-size: 12px;
line-height: 20px;
border: 1px solid $border-color;
border-radius: $label-border-radius;
font-weight: $gl-font-weight-normal;
}
.groups-list-tree-container {
> .group-list-tree > .group-row.has-children:first-child {
border-top: 0;

View File

@ -120,8 +120,11 @@ module Auth
end
end
# Return the project path (lowercase) as metadata
{ project_path: project&.full_path&.downcase }
{
project_path: project&.full_path&.downcase,
project_id: project&.id,
root_namespace_id: project&.root_ancestor&.id
}
end
private

View File

@ -1,10 +1,10 @@
- access = note_human_max_access(note)
- if note.noteable_author?(@noteable)
%span{ class: 'note-role user-access-role has-tooltip d-none d-md-inline-block', title: _("This user is the author of this %{noteable}.") % { noteable: @noteable.human_class_name } }= _("Author")
= render Pajamas::BadgeComponent.new(_("Author"), variant: 'neutral', class: 'gl-bg-transparent! gl-inset-border-1-gray-100! note-role user-access-role has-tooltip gl-display-none gl-md-display-inline-block', title: _("This user is the author of this %{noteable}.") % { noteable: @noteable.human_class_name })
- if access
%span{ class: 'note-role user-access-role has-tooltip', title: _("This user has the %{access} role in the %{name} project.") % { access: access.downcase, name: note.project_name } }= access
= render Pajamas::BadgeComponent.new(access, variant: 'neutral', class: 'gl-bg-transparent! gl-inset-border-1-gray-100! note-role user-access-role has-tooltip', title: _("This user has the %{access} role in the %{name} project.") % { access: access.downcase, name: note.project_name })
- elsif note.contributor?
%span{ class: 'note-role user-access-role has-tooltip', title: _("This user has previously committed to the %{name} project.") % { name: note.project_name } }= _("Contributor")
= render Pajamas::BadgeComponent.new(_("Contributor"), variant: 'neutral', class: 'gl-bg-transparent! gl-inset-border-1-gray-100! note-role user-access-role has-tooltip', title: _("This user has previously committed to the %{name} project.") % { name: note.project_name })
- if can?(current_user, :award_emoji, note)
- if note.emoji_awardable?

View File

@ -4,5 +4,5 @@
%h1.page-title.gl-font-size-h-display
= _("Edit Snippet")
%hr
= render "shared/snippets/form", url: project_snippet_path(@project, @snippet)

View File

@ -9,7 +9,7 @@
= link_to group.full_name, group, class: 'group-name'
- if access&.nonzero?
%span.user-access-role= Gitlab::Access.human_access(access)
= render Pajamas::BadgeComponent.new(Gitlab::Access.human_access(access), variant: 'neutral', class: 'gl-bg-transparent! gl-inset-border-1-gray-100!')
- if group.description.present?
.description

View File

@ -61,4 +61,4 @@
- if show_roles
.controls.member-controls.gl-align-items-center
= render_if_exists 'shared/members/ee/ldap_tag', can_override: member.can_override?
%span.member-access-text.user-access-role= member.human_access
= render Pajamas::BadgeComponent.new(member.human_access, variant: 'neutral', class: 'gl-bg-transparent! gl-inset-border-1-gray-100!')

View File

@ -50,7 +50,7 @@
- if !explore_projects_tab? && access&.nonzero?
-# haml-lint:disable UnnecessaryStringOutput
= ' ' # prevent haml from eating the space between elements
%span.user-access-role.gl-display-block.gl-m-0{ data: { testid: 'user-role-content' } }= localized_project_human_access(access)
= render Pajamas::BadgeComponent.new(localized_project_human_access(access), variant: 'neutral', class: 'gl-bg-transparent! gl-inset-border-1-gray-100!', data: { testid: 'user-access-role' })
- if !explore_projects_tab?
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: project, additional_classes: 'gl-ml-2!'

View File

@ -0,0 +1,67 @@
# Use this template to announce a feature deprecation or other
# important planned changes at least three releases prior to removal.
# Breaking changes must happen in a major release.
#
# See the deprecation guidelines to confirm your understanding of GitLab's definitions:
# https://docs.gitlab.com/ee/development/deprecation_guidelines/#terminology
#
# If an End of Support period applies, see the OPTIONAL section below.
#
# For more information, see the handbook:
# https://handbook.gitlab.com/handbook/marketing/blog/release-posts/#deprecations-and-other-planned-breaking-change-announcements
# ===================
# REQUIRED FIELDS
# ===================
# ----- DELETE EVERYTHING ABOVE THIS LINE -----
- title: "Hosted Runners on Linux operating system upgrade"
# The milestones for the deprecation announcement, and the removal.
removal_milestone: "17.0"
announcement_milestone: "16.10"
# Change breaking_change to false if needed.
breaking_change: true
# The stage and GitLab username of the person reporting the change,
# and a link to the deprecation issue
reporter: tmaczukin
stage: ci
issue_url: https://gitlab.com/gitlab-org/ci-cd/shared-runners/infrastructure/-/issues/60
impact: low # Can be one of: [critical, high, medium, low]
scope: instance # Can be one or a combination of: [instance, group, project]
resolution_role: Developer # Can be one of: [Admin, Owner, Maintainer, Developer]
manual_task: true # Can be true or false. Use this to denote whether a resolution action must be performed manually (true), or if it can be automated by using the API or other automation (false).
body: | # (required) Don't change this line.
With GitLab 17.0 we're upgrading the container-optimized operating system ([COS](https://cloud.google.com/container-optimized-os/docs))
of the ephemeral VMs used to execute jobs for [Hosted Runners on Linux](https://docs.gitlab.com/ee/ci/runners/saas/linux_saas_runner.html).
The COS upgrade includes a Docker Engine upgrade from Version 19.03.15 to Version 23.0.5, which introduced
a known compatibility issue.
GitLab CI/CD jobs [using Docker-in-Docker-based jobs](https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker)
with a Docker-in-Docker service version prior to 20.10 and GitLab CI/CD jobs [using Kaniko to build container images](https://docs.gitlab.com/ee/ci/docker/using_kaniko.html)
with a Kaniko service version older than `v1.9.0` will start failing.
To fix these issues, an update of service version in `.gitlab-ci.yml` is required.
Both issues, including a detailed explanation of how they affect jobs and how to fix
the issue, are described
[in the announcement blog post](https://about.gitlab.com/blog/2023/10/04/updating-the-os-version-of-saas-runners-on-linux/).
# ==============================
# OPTIONAL END-OF-SUPPORT FIELDS
# ==============================
#
# If an End of Support period applies:
# 1) Share this announcement in the `#spt_managers` Support channel in Slack
# 2) Mention `@gitlab-com/support` in this merge request.
#
# When support for this feature ends, in XX.YY milestone format.
end_of_support_milestone:
# Array of tiers the feature is currently available to,
# like [Free, Silver, Gold, Core, Premium, Ultimate]
tiers:
# Links to documentation and thumbnail image
documentation_url:
image_url:
# Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
video_url:

View File

@ -1241,6 +1241,33 @@ set `AUTO_DEVOPS_BUILD_IMAGE_CNB_BUILDER` to `heroku/builder:20`.
<div class="deprecation breaking-change" data-milestone="17.0">
### Hosted Runners on Linux operating system upgrade
<div class="deprecation-notes">
- Announced in GitLab <span class="milestone">16.10</span>
- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/ci-cd/shared-runners/infrastructure/-/issues/60).
</div>
With GitLab 17.0 we're upgrading the container-optimized operating system ([COS](https://cloud.google.com/container-optimized-os/docs))
of the ephemeral VMs used to execute jobs for [Hosted Runners on Linux](https://docs.gitlab.com/ee/ci/runners/saas/linux_saas_runner.html).
The COS upgrade includes a Docker Engine upgrade from Version 19.03.15 to Version 23.0.5, which introduced
a known compatibility issue.
GitLab CI/CD jobs [using Docker-in-Docker-based jobs](https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-docker-in-docker)
with a Docker-in-Docker service version prior to 20.10 and GitLab CI/CD jobs [using Kaniko to build container images](https://docs.gitlab.com/ee/ci/docker/using_kaniko.html)
with a Kaniko service version older than `v1.9.0` will start failing.
To fix these issues, an update of service version in `.gitlab-ci.yml` is required.
Both issues, including a detailed explanation of how they affect jobs and how to fix
the issue, are described
[in the announcement blog post](https://about.gitlab.com/blog/2023/10/04/updating-the-os-version-of-saas-runners-on-linux/).
</div>
<div class="deprecation breaking-change" data-milestone="17.0">
### Internal container registry API tag deletion endpoint
<div class="deprecation-notes">

View File

@ -165,10 +165,26 @@ pull-image:
- docker pull $GOOGLE_ARTIFACT_REGISTRY_REPOSITORY_LOCATION-docker.pkg.dev/$GOOGLE_ARTIFACT_REGISTRY_PROJECT_ID/$GOOGLE_ARTIFACT_REGISTRY_REPOSITORY_NAME/app:v0.1.0
```
### Copy an image from the GitLab Container Registry with Docker
#### Copy an image by using a CI/CD component
The following example shows how to replicate an image from the GitLab Container Registry to the Google Artifact Registry.
To do this, pull the image from GitLab, re-tag it, and then push to the Google Artifact Registry.
Google provides the [`upload-artifact-registry`](https://gitlab.com/explore/catalog/google-gitlab-components/artifact-registry) CI/CD component, which you can use to copy an image from the GitLab container registry to Artifact Registry.
To use the `upload-artifact-registry` component, add the following to your `.gitlab-ci.yml`:
```yaml
include:
- component: gitlab.com/google-gitlab-components/artifact-registry/upload-artifact-registry@<VERSION>
inputs:
stage: deploy
source: $CI_REGISTRY_IMAGE:v0.1.0
target: $GOOGLE_ARTIFACT_REGISTRY_REPOSITORY_LOCATION-docker.pkg.dev/$GOOGLE_ARTIFACT_REGISTRY_PROJECT_ID/$GOOGLE_ARTIFACT_REGISTRY_REPOSITORY_NAME/app:v0.1.0
```
For details, see [the component documentation](https://gitlab.com/explore/catalog/google-gitlab-components/artifact-registry).
Using the `upload-artifact-registry` component simplifies copying images to Artifact Registry and is the intended method for this integration. If you want to use Docker or Crane, see the following examples.
#### Copy an image by using Docker
In the following example, the `gcloud` CLI is used to set up the Docker authentication, as an alternative to the [standalone Docker credential helper](https://cloud.google.com/artifact-registry/docs/docker/authentication#standalone-helper).
@ -191,7 +207,7 @@ copy-image:
- docker push $TARGET_IMAGE
```
### Copy an image from the GitLab Container Registry with crane
#### Copy an image by using Crane
```yaml
copy-image:

View File

@ -10,7 +10,7 @@ module QA
view 'app/views/shared/projects/_project.html.haml' do
element 'project-content'
element 'user-role-content'
element 'user-access-role'
end
view 'app/views/dashboard/_projects_head.html.haml' do
@ -62,7 +62,7 @@ module QA
def has_project_with_access_role?(project_name, access_role)
within_element('project-content', text: project_name) do
has_element?('user-role-content', text: access_role)
has_element?('user-access-role', text: access_role)
end
end
end

View File

@ -30,7 +30,7 @@ RSpec.describe 'Dashboard Projects', :js, feature_category: :groups_and_projects
it 'shows role badge' do
visit dashboard_projects_path
page.within '.user-access-role' do
within_testid('user-access-role') do
expect(page).to have_content('Developer')
end
end
@ -39,7 +39,7 @@ RSpec.describe 'Dashboard Projects', :js, feature_category: :groups_and_projects
it 'displays the right role' do
visit dashboard_projects_path
page.within '.user-access-role' do
within_testid('user-access-role') do
expect(page).to have_content('Developer')
end
@ -47,7 +47,7 @@ RSpec.describe 'Dashboard Projects', :js, feature_category: :groups_and_projects
visit dashboard_projects_path
page.within '.user-access-role' do
within_testid('user-access-role') do
expect(page).to have_content('Maintainer')
end
end

View File

@ -1,12 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SidebarTodo template renders component container element with proper data attributes 1`] = `
<button
<gl-button-stub
aria-label="Mark as done"
class="btn btn-default btn-todo gl-button gl-float-right issuable-header-btn"
buttontextclasses=""
category="primary"
class="gl-float-right issuable-header-btn"
data-issuable-id="1"
data-issuable-type="epic"
icon=""
size="small"
type="button"
variant="default"
>
<gl-icon-stub
class="todo-undone"
@ -19,13 +24,5 @@ exports[`SidebarTodo template renders component container element with proper da
>
Mark as done
</span>
<gl-loading-icon-stub
color="dark"
inline="true"
label="Loading"
size="sm"
style="display: none;"
variant="spinner"
/>
</button>
</gl-button-stub>
`;

View File

@ -1,4 +1,4 @@
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
@ -20,14 +20,15 @@ describe('SidebarTodo', () => {
},
});
};
const findButton = () => wrapper.findComponent(GlButton);
it.each`
state | classes
${false} | ${['gl-button', 'btn', 'btn-default', 'btn-todo', 'issuable-header-btn', 'gl-float-right']}
${true} | ${['btn-blank', 'btn-todo', 'sidebar-collapsed-icon', 'js-dont-change-state']}
${false} | ${['issuable-header-btn', 'gl-float-right']}
${true} | ${['sidebar-collapsed-icon', 'js-dont-change-state']}
`('returns todo button classes for when `collapsed` prop is `$state`', ({ state, classes }) => {
createComponent({ collapsed: state });
expect(wrapper.find('button').classes()).toStrictEqual(classes);
expect(findButton().classes()).toStrictEqual(classes);
});
it.each`
@ -41,14 +42,14 @@ describe('SidebarTodo', () => {
expect(wrapper.findComponent(GlIcon).classes().join(' ')).toStrictEqual(iconClass);
expect(wrapper.findComponent(GlIcon).props('name')).toStrictEqual(icon);
expect(wrapper.find('button').text()).toBe(label);
expect(findButton().text()).toBe(label);
},
);
describe('template', () => {
it('emits `toggleTodo` event when clicked on button', async () => {
createComponent();
wrapper.find('button').trigger('click');
findButton().vm.$emit('click');
await nextTick();
expect(wrapper.emitted().toggleTodo).toHaveLength(1);

View File

@ -32,18 +32,18 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
valuefield="value"
>
<gl-form-radio-stub
class="mb-3"
class="gl-mb-3"
value="private"
>
<div
class="d-flex gl-align-items-center"
class="gl-align-items-center gl-display-flex"
>
<gl-icon-stub
name="lock"
size="16"
/>
<span
class="font-weight-bold js-visibility-option ml-1"
class="gl-font-weight-semibold gl-ml-2 js-visibility-option"
data-qa-visibility="Private"
data-testid="visibility-content"
>
@ -52,18 +52,18 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</div>
</gl-form-radio-stub>
<gl-form-radio-stub
class="mb-3"
class="gl-mb-3"
value="internal"
>
<div
class="d-flex gl-align-items-center"
class="gl-align-items-center gl-display-flex"
>
<gl-icon-stub
name="shield"
size="16"
/>
<span
class="font-weight-bold js-visibility-option ml-1"
class="gl-font-weight-semibold gl-ml-2 js-visibility-option"
data-qa-visibility="Internal"
data-testid="visibility-content"
>
@ -72,18 +72,18 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</div>
</gl-form-radio-stub>
<gl-form-radio-stub
class="mb-3"
class="gl-mb-3"
value="public"
>
<div
class="d-flex gl-align-items-center"
class="gl-align-items-center gl-display-flex"
>
<gl-icon-stub
name="earth"
size="16"
/>
<span
class="font-weight-bold js-visibility-option ml-1"
class="gl-font-weight-semibold gl-ml-2 js-visibility-option"
data-qa-visibility="Public"
data-testid="visibility-content"
>
@ -94,7 +94,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</gl-form-radio-group-stub>
</gl-form-group-stub>
<div
class="text-muted"
class="gl-text-secondary"
data-testid="restricted-levels-info"
/>
</div>

View File

@ -1,4 +1,4 @@
import { GlAlert, GlSkeletonLoader, GlEmptyState } from '@gitlab/ui';
import { GlAlert, GlEmptyState } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@ -7,6 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import setWindowLocation from 'helpers/set_window_location_helper';
import { stubComponent } from 'helpers/stub_component';
import WorkItemLoading from '~/work_items/components/work_item_loading.vue';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
import WorkItemAncestors from '~/work_items/components/work_item_ancestors/work_item_ancestors.vue';
@ -72,7 +73,7 @@ describe('WorkItemDetail component', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
const findWorkItemLoading = () => wrapper.findComponent(WorkItemLoading);
const findWorkItemActions = () => wrapper.findComponent(WorkItemActions);
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
const findCreatedUpdated = () => wrapper.findComponent(WorkItemCreatedUpdated);
@ -181,7 +182,7 @@ describe('WorkItemDetail component', () => {
});
it('renders skeleton loader', () => {
expect(findSkeleton().exists()).toBe(true);
expect(findWorkItemLoading().exists()).toBe(true);
expect(findWorkItemTitle().exists()).toBe(false);
});
});
@ -193,7 +194,7 @@ describe('WorkItemDetail component', () => {
});
it('does not render skeleton', () => {
expect(findSkeleton().exists()).toBe(false);
expect(findWorkItemLoading().exists()).toBe(false);
expect(findWorkItemTitle().exists()).toBe(true);
});

View File

@ -0,0 +1,73 @@
import { GlSkeletonLoader } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WorkItemLoading from '~/work_items/components/work_item_loading.vue';
describe('Work Item Loading spec', () => {
let wrapper;
const findWorkItemTwoColumnLoading = () => wrapper.findByTestId('work-item-two-column-loading');
const findWorkItemSingleColumnLoading = () =>
wrapper.findByTestId('work-item-single-column-loading');
const findWorkItemTitleMetaLoading = () => wrapper.findByTestId('work-title-and-meta-loading');
const findWorkItemDescriptionLoading = () =>
wrapper.findByTestId('work-item-description-loading');
const findWorkItemAttributesXsSmLoading = () =>
wrapper.findByTestId('work-item-attributes-xssm-loading');
const findWorkItemAttributesMdUpLoading = () =>
wrapper.findByTestId('work-item-attributes-mdup-loading');
const findWorkItemActivityPlaceholder = () =>
wrapper.findByTestId('work-item-activity-placeholder-loading');
const findWorkItemNotesLoading = () => wrapper.findByTestId('work-item-notes-loading');
const findLoaders = () => findWorkItemAttributesXsSmLoading().findAllComponents(GlSkeletonLoader);
const createComponent = ({ twoColumnView = false } = {}) => {
wrapper = shallowMountExtended(WorkItemLoading, {
propsData: {
twoColumnView,
},
});
};
describe('Work Item Single Column loading view', () => {
beforeEach(() => {
createComponent();
});
it('renders the single column loading', () => {
expect(findWorkItemSingleColumnLoading().exists()).toBe(true);
expect(findWorkItemTwoColumnLoading().exists()).toBe(false);
});
});
describe('Work Item Two Column loading view', () => {
beforeEach(() => {
createComponent({ twoColumnView: true });
});
it('renders the two column loading', () => {
expect(findWorkItemTwoColumnLoading().exists()).toBe(true);
});
it('renders the title and meta loading skeleton', () => {
expect(findWorkItemTitleMetaLoading().exists()).toBe(true);
});
it('renders the description loading skeleton', () => {
expect(findWorkItemDescriptionLoading().exists()).toBe(true);
});
it('renders the attributes loading skeleton', () => {
expect(findWorkItemAttributesXsSmLoading().exists()).toBe(true);
expect(findWorkItemAttributesMdUpLoading().exists()).toBe(true);
expect(findLoaders()).toHaveLength(WorkItemLoading.loader.attributesRepeat);
});
it('renders the activity placeholder loading skeleton', () => {
expect(findWorkItemActivityPlaceholder().exists()).toBe(true);
});
it('renders the notes loading skeleton', () => {
expect(findWorkItemNotesLoading().exists()).toBe(true);
});
});
});

View File

@ -9,6 +9,7 @@ RSpec.shared_examples 'autocompletes items' do
create(:milestone, project: project, title: 'My Cool Milestone')
project.add_maintainer(create(:user, name: 'JohnDoe123'))
project.add_maintainer(create(:user, name: 'ReallyLongUsername1234567890'))
else # group wikis
project = create(:project, group: group)
@ -18,6 +19,7 @@ RSpec.shared_examples 'autocompletes items' do
create(:milestone, group: group, title: 'My Cool Milestone')
project.add_maintainer(create(:user, name: 'JohnDoe123'))
project.add_maintainer(create(:user, name: 'ReallyLongUsername1234567890'))
end
end
@ -40,4 +42,10 @@ RSpec.shared_examples 'autocompletes items' do
fill_in :wiki_content, with: ':smil'
expect(page).to have_text 'smile_cat'
end
it 'autocompletes items with long names' do
fill_in :wiki_content, with: "@ReallyLongUsername1234567"
expect(page).to have_text 'ReallyLongUsername1234567890'
end
end

View File

@ -176,6 +176,7 @@ RSpec.shared_examples 'User creates wiki page' do
end
it_behaves_like 'wiki file attachments'
it_behaves_like 'autocompletes items'
end
context "when wiki is not empty", :js do

View File

@ -505,6 +505,9 @@ RSpec.shared_examples 'work items description' do
expect(page).to have_text(expected_warning)
page.find('summary', text: 'View current version').click
expect(find_by_testid('conflicted-description').value).to eq('oh no!')
click_button s_('WorkItem|Save and overwrite')
expect(page.find('[data-testid="work-item-description"]')).to have_text("oh yeah!")

View File

@ -86,7 +86,11 @@ RSpec.shared_examples 'an accessible' do
[{ 'type' => 'repository',
'name' => project.full_path,
'actions' => actions,
'meta' => { 'project_path' => project.full_path } }]
'meta' => {
'project_path' => project.full_path,
'project_id' => project.id,
'root_namespace_id' => project.root_ancestor.id
} }]
end
it_behaves_like 'a valid token'
@ -252,13 +256,21 @@ RSpec.shared_examples 'a container registry auth service' do
'type' => 'repository',
'name' => project.full_path,
'actions' => ['pull'],
'meta' => { 'project_path' => project.full_path }
'meta' => {
'project_path' => project.full_path,
'project_id' => project.id,
'root_namespace_id' => project.root_ancestor.id
}
},
{
'type' => 'repository',
'name' => "#{project.full_path}/*",
'actions' => ['pull'],
'meta' => { 'project_path' => project.full_path }
'meta' => {
'project_path' => project.full_path,
'project_id' => project.id,
'root_namespace_id' => project.root_ancestor.id
}
}
]
end
@ -880,19 +892,35 @@ RSpec.shared_examples 'a container registry auth service' do
{ 'type' => 'repository',
'name' => internal_project.full_path,
'actions' => ['pull'],
'meta' => { 'project_path' => internal_project.full_path } },
'meta' => {
'project_path' => internal_project.full_path,
'project_id' => internal_project.id,
'root_namespace_id' => internal_project.root_ancestor.id
} },
{ 'type' => 'repository',
'name' => private_project.full_path,
'actions' => ['pull'],
'meta' => { 'project_path' => private_project.full_path } },
'meta' => {
'project_path' => private_project.full_path,
'project_id' => private_project.id,
'root_namespace_id' => private_project.root_ancestor.id
} },
{ 'type' => 'repository',
'name' => public_project.full_path,
'actions' => ['pull'],
'meta' => { 'project_path' => public_project.full_path } },
'meta' => {
'project_path' => public_project.full_path,
'project_id' => public_project.id,
'root_namespace_id' => public_project.root_ancestor.id
} },
{ 'type' => 'repository',
'name' => public_project_private_container_registry.full_path,
'actions' => ['pull'],
'meta' => { 'project_path' => public_project_private_container_registry.full_path } }
'meta' => {
'project_path' => public_project_private_container_registry.full_path,
'project_id' => public_project_private_container_registry.id,
'root_namespace_id' => public_project_private_container_registry.root_ancestor.id
} }
]
end
end
@ -907,11 +935,19 @@ RSpec.shared_examples 'a container registry auth service' do
{ 'type' => 'repository',
'name' => internal_project.full_path,
'actions' => ['pull'],
'meta' => { 'project_path' => internal_project.full_path } },
'meta' => {
'project_path' => internal_project.full_path,
'project_id' => internal_project.id,
'root_namespace_id' => internal_project.root_ancestor.id
} },
{ 'type' => 'repository',
'name' => public_project.full_path,
'actions' => ['pull'],
'meta' => { 'project_path' => public_project.full_path } }
'meta' => {
'project_path' => public_project.full_path,
'project_id' => public_project.id,
'root_namespace_id' => public_project.root_ancestor.id
} }
]
end
end
@ -926,7 +962,11 @@ RSpec.shared_examples 'a container registry auth service' do
{ 'type' => 'repository',
'name' => public_project.full_path,
'actions' => ['pull'],
'meta' => { 'project_path' => public_project.full_path } }
'meta' => {
'project_path' => public_project.full_path,
'project_id' => public_project.id,
'root_namespace_id' => public_project.root_ancestor.id
} }
]
end
end