Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
17295c75a1
commit
a4db97517a
|
|
@ -69,11 +69,6 @@ stages:
|
|||
QA_INTERCEPT_REQUESTS: "true"
|
||||
GITLAB_LICENSE_MODE: test
|
||||
GITLAB_QA_ADMIN_ACCESS_TOKEN: $QA_ADMIN_ACCESS_TOKEN
|
||||
before_script:
|
||||
- !reference [.qa-base, before_script]
|
||||
# Prepend the file paths with the absolute path from inside the container since the files will be read from there
|
||||
- export RSPEC_FAST_QUARANTINE_PATH="/home/gitlab/qa/${RSPEC_FAST_QUARANTINE_PATH}"
|
||||
- export RSPEC_SKIPPED_TESTS_REPORT_PATH="/home/gitlab/qa/rspec/skipped_tests-${CI_JOB_ID}.txt"
|
||||
# Allow QA jobs to fail as they are flaky. The top level `package-and-e2e:ee`
|
||||
# pipeline is not allowed to fail, so without allowing QA to fail, we will be
|
||||
# blocking merges due to flaky tests.
|
||||
|
|
@ -98,26 +93,6 @@ stages:
|
|||
- qa/knapsack/*.json
|
||||
expire_in: 1 day
|
||||
|
||||
.download-fast-quarantine-report:
|
||||
image: ${GITLAB_DEPENDENCY_PROXY_ADDRESS}alpine:edge
|
||||
stage: .pre
|
||||
variables:
|
||||
GIT_STRATEGY: none
|
||||
before_script:
|
||||
- apk add --no-cache --update curl bash
|
||||
script:
|
||||
- mkdir -p "${QA_RSPEC_REPORT_PATH}"
|
||||
- |
|
||||
if [[ ! -f "${QA_RSPEC_REPORT_PATH}/${RSPEC_FAST_QUARANTINE_FILE}" ]]; then
|
||||
curl --location -o "${QA_RSPEC_REPORT_PATH}/${RSPEC_FAST_QUARANTINE_FILE}" "https://gitlab-org.gitlab.io/quality/engineering-productivity/fast-quarantine/${RSPEC_FAST_QUARANTINE_PATH}" ||
|
||||
echo "" > "${QA_RSPEC_REPORT_PATH}/${RSPEC_FAST_QUARANTINE_FILE}"
|
||||
fi
|
||||
allow_failure: true
|
||||
artifacts:
|
||||
paths:
|
||||
- "${QA_RSPEC_REPORT_PATH}/${RSPEC_FAST_QUARANTINE_FILE}"
|
||||
expire_in: 1 day
|
||||
|
||||
.upload-knapsack-report:
|
||||
extends:
|
||||
- .generate-knapsack-report-base
|
||||
|
|
|
|||
|
|
@ -14,7 +14,4 @@ variables:
|
|||
QA_RUN_ALL_TESTS: "true"
|
||||
# Used by gitlab-qa to set up a volume for `${CI_PROJECT_DIR}/qa/rspec:/home/gitlab/qa/rspec/`
|
||||
QA_RSPEC_REPORT_PATH: "${CI_PROJECT_DIR}/qa/rspec"
|
||||
RSPEC_FAST_QUARANTINE_FILE: "fast_quarantine-gitlab.txt"
|
||||
# This path is relative to /home/gitlab/qa/ in the QA container
|
||||
RSPEC_FAST_QUARANTINE_PATH: "rspec/${RSPEC_FAST_QUARANTINE_FILE}"
|
||||
QA_OMNIBUS_MR_TESTS: "only-smoke-reliable"
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ export default {
|
|||
<gl-dropdown-item
|
||||
v-else-if="isDropdownWithEmojiTrigger"
|
||||
v-bind="componentAttributes"
|
||||
button-class="top-nav-menu-item"
|
||||
@click="openModal"
|
||||
>
|
||||
{{ displayText }}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,10 @@ export default {
|
|||
} = await this.$apollo.mutate({
|
||||
mutation: organizationCreateMutation,
|
||||
variables: {
|
||||
input: { name: formValues.name, path: formValues.path },
|
||||
input: { name: formValues.name, path: formValues.path, avatar: formValues.avatar },
|
||||
},
|
||||
context: {
|
||||
hasUpload: formValues.avatar instanceof File,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import { s__, __ } from '~/locale';
|
|||
import { createAlert } from '~/alert';
|
||||
import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
|
||||
import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
|
||||
import { FORM_FIELD_NAME, FORM_FIELD_ID } from '~/organizations/shared/constants';
|
||||
import {
|
||||
FORM_FIELD_NAME,
|
||||
FORM_FIELD_ID,
|
||||
FORM_FIELD_AVATAR,
|
||||
} from '~/organizations/shared/constants';
|
||||
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPE_ORGANIZATION } from '~/graphql_shared/constants';
|
||||
|
|
@ -25,7 +29,7 @@ export default {
|
|||
),
|
||||
successMessage: s__('Organization|Organization was successfully updated.'),
|
||||
},
|
||||
fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID],
|
||||
fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_AVATAR],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
|
|
@ -33,9 +37,24 @@ export default {
|
|||
};
|
||||
},
|
||||
methods: {
|
||||
avatarInput(formValues) {
|
||||
// Organization has an avatar and it is been explicitly removed.
|
||||
if (this.organization.avatar && formValues.avatar === null) {
|
||||
return { avatar: null };
|
||||
}
|
||||
|
||||
// Avatar has been set or changed.
|
||||
if (formValues.avatar instanceof File) {
|
||||
return { avatar: formValues.avatar };
|
||||
}
|
||||
|
||||
// Avatar has not been changed at all, do not include the `avatar` key in input.
|
||||
return {};
|
||||
},
|
||||
async onSubmit(formValues) {
|
||||
this.errors = [];
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
|
|
@ -47,8 +66,12 @@ export default {
|
|||
input: {
|
||||
id: convertToGraphQLId(TYPE_ORGANIZATION, this.organization.id),
|
||||
name: formValues.name,
|
||||
...this.avatarInput(formValues),
|
||||
},
|
||||
},
|
||||
context: {
|
||||
hasUpload: formValues.avatar instanceof File,
|
||||
},
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import { GlForm, GlFormFields, GlButton } from '@gitlab/ui';
|
|||
import { formValidators } from '@gitlab/ui/dist/utils';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { slugify } from '~/lib/utils/text_utility';
|
||||
import AvatarUploadDropzone from '~/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue';
|
||||
import {
|
||||
FORM_FIELD_NAME,
|
||||
FORM_FIELD_ID,
|
||||
FORM_FIELD_PATH,
|
||||
FORM_FIELD_AVATAR,
|
||||
FORM_FIELD_PATH_VALIDATORS,
|
||||
} from '../constants';
|
||||
import OrganizationUrlField from './organization_url_field.vue';
|
||||
|
|
@ -18,6 +20,7 @@ export default {
|
|||
GlFormFields,
|
||||
GlButton,
|
||||
OrganizationUrlField,
|
||||
AvatarUploadDropzone,
|
||||
},
|
||||
i18n: {
|
||||
cancel: __('Cancel'),
|
||||
|
|
@ -36,6 +39,7 @@ export default {
|
|||
return {
|
||||
[FORM_FIELD_NAME]: '',
|
||||
[FORM_FIELD_PATH]: '',
|
||||
[FORM_FIELD_AVATAR]: null,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
@ -43,7 +47,7 @@ export default {
|
|||
type: Array,
|
||||
required: false,
|
||||
default() {
|
||||
return [FORM_FIELD_NAME, FORM_FIELD_PATH];
|
||||
return [FORM_FIELD_NAME, FORM_FIELD_PATH, FORM_FIELD_AVATAR];
|
||||
},
|
||||
},
|
||||
submitButtonText: {
|
||||
|
|
@ -98,6 +102,13 @@ export default {
|
|||
class: 'gl-w-full',
|
||||
},
|
||||
},
|
||||
[FORM_FIELD_AVATAR]: {
|
||||
label: s__('Organization|Organization avatar'),
|
||||
groupAttrs: {
|
||||
class: 'gl-w-full',
|
||||
labelSrOnly: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return Object.entries(fields).reduce((accumulator, [fieldKey, fieldDefinition]) => {
|
||||
|
|
@ -148,6 +159,14 @@ export default {
|
|||
@blur="blur"
|
||||
/>
|
||||
</template>
|
||||
<template #input(avatar)="{ input, value }">
|
||||
<avatar-upload-dropzone
|
||||
:value="value"
|
||||
:entity="formValues"
|
||||
:label="fields.avatar.label"
|
||||
@input="input"
|
||||
/>
|
||||
</template>
|
||||
</gl-form-fields>
|
||||
<div class="gl-display-flex gl-gap-3">
|
||||
<gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="loading">{{
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { s__ } from '~/locale';
|
|||
export const FORM_FIELD_NAME = 'name';
|
||||
export const FORM_FIELD_ID = 'id';
|
||||
export const FORM_FIELD_PATH = 'path';
|
||||
export const FORM_FIELD_AVATAR = 'avatar';
|
||||
|
||||
export const FORM_FIELD_PATH_VALIDATORS = [
|
||||
formValidators.required(s__('Organization|Organization URL is required.')),
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export default {
|
|||
:entity-name="organization.name"
|
||||
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
|
||||
:size="64"
|
||||
:src="organization.avatar_url"
|
||||
/>
|
||||
<div class="gl-ml-3">
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
|
|
|
|||
|
|
@ -119,7 +119,11 @@ export default {
|
|||
},
|
||||
) {
|
||||
if (mergeRequestMergeStatusUpdated) {
|
||||
this.state = mergeRequestMergeStatusUpdated;
|
||||
this.state = {
|
||||
...mergeRequestMergeStatusUpdated,
|
||||
mergeRequestsFfOnlyEnabled: this.state.mergeRequestsFfOnlyEnabled,
|
||||
onlyAllowMergeIfPipelineSucceeds: this.state.onlyAllowMergeIfPipelineSucceeds,
|
||||
};
|
||||
|
||||
if (!this.commitMessageIsTouched) {
|
||||
this.commitMessage = mergeRequestMergeStatusUpdated.defaultMergeCommitMessage;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
<script>
|
||||
import { GlButton, GlAvatar, GlSprintf, GlTruncate } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
|
||||
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
uploadText: __('Drop or %{linkStart}upload%{linkEnd} an avatar.'),
|
||||
maxFileSize: __('Max file size is 200 KiB.'),
|
||||
removeAvatar: __('Remove avatar'),
|
||||
},
|
||||
AVATAR_SHAPE_OPTION_RECT,
|
||||
components: { GlButton, GlAvatar, GlSprintf, GlTruncate, UploadDropzone },
|
||||
props: {
|
||||
entity: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
value: {
|
||||
type: [String, File],
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
avatarObjectUrl: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
avatarSrc() {
|
||||
if (this.avatarObjectUrl) {
|
||||
return this.avatarObjectUrl;
|
||||
}
|
||||
|
||||
if (this.isValueAFile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.value;
|
||||
},
|
||||
isValueAFile() {
|
||||
return this.value instanceof File;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(newValue) {
|
||||
this.revokeAvatarObjectUrl();
|
||||
|
||||
if (newValue instanceof File) {
|
||||
this.avatarObjectUrl = URL.createObjectURL(newValue);
|
||||
} else {
|
||||
this.avatarObjectUrl = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.revokeAvatarObjectUrl();
|
||||
},
|
||||
methods: {
|
||||
revokeAvatarObjectUrl() {
|
||||
if (this.avatarObjectUrl === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
URL.revokeObjectURL(this.avatarObjectUrl);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-column-gap-5">
|
||||
<gl-avatar
|
||||
:entity-id="entity.id || null"
|
||||
:entity-name="entity.name || 'organization'"
|
||||
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
|
||||
:size="96"
|
||||
:src="avatarSrc"
|
||||
/>
|
||||
<div class="gl-min-w-0">
|
||||
<p class="gl-font-weight-bold gl-line-height-1 gl-mb-3">
|
||||
{{ label }}
|
||||
</p>
|
||||
<div v-if="value" class="gl-display-flex gl-align-items-center gl-column-gap-3">
|
||||
<gl-button @click="$emit('input', null)">{{ $options.i18n.removeAvatar }}</gl-button>
|
||||
<gl-truncate
|
||||
v-if="isValueAFile"
|
||||
class="gl-text-secondary gl-max-w-48 gl-min-w-0"
|
||||
position="middle"
|
||||
:text="value.name"
|
||||
/>
|
||||
</div>
|
||||
<upload-dropzone v-else single-file-selection @change="$emit('input', $event)">
|
||||
<template #upload-text>
|
||||
<gl-sprintf :message="$options.i18n.uploadText">
|
||||
<template #link="{ content }">
|
||||
<span class="gl-link gl-hover-text-decoration-underline">{{ content }}</span>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
</upload-dropzone>
|
||||
<p class="gl-mb-0 gl-mt-3 gl-text-secondary">{{ $options.i18n.maxFileSize }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
@import 'framework/animations';
|
||||
@import 'framework/vue_transitions';
|
||||
@import 'framework/blocks';
|
||||
@import 'framework/breadcrumbs';
|
||||
@import 'framework/buttons';
|
||||
@import 'framework/badges';
|
||||
@import 'framework/calendar';
|
||||
|
|
@ -23,6 +24,7 @@
|
|||
@import 'framework/gfm';
|
||||
@import 'framework/kbd';
|
||||
@import 'framework/header';
|
||||
@import 'framework/top_bar';
|
||||
@import 'framework/highlight';
|
||||
@import 'framework/lists';
|
||||
@import 'framework/logo';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
.breadcrumbs {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
align-self: center;
|
||||
color: $gl-text-color-secondary;
|
||||
|
||||
.avatar-tile {
|
||||
margin-right: 4px;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 50%;
|
||||
vertical-align: sub;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-item-text {
|
||||
text-decoration: inherit;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
@include str-truncated(128px);
|
||||
}
|
||||
}
|
||||
|
|
@ -806,28 +806,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
.navbar-gitlab {
|
||||
li.dropdown {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
header.navbar-gitlab .dropdown {
|
||||
.dropdown-menu {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
header.navbar-gitlab-new .header-content .dropdown {
|
||||
.dropdown-menu {
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-content-faded-mask {
|
||||
position: relative;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,119 +9,6 @@
|
|||
left: 0;
|
||||
right: 0;
|
||||
border-radius: 0;
|
||||
|
||||
.close-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
min-height: var(--header-height);
|
||||
padding-left: 0;
|
||||
|
||||
.title {
|
||||
padding-right: 0;
|
||||
color: currentColor;
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
|
||||
img {
|
||||
height: 24px;
|
||||
|
||||
+ .logo-text {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&.wrap {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
&.initializing {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
a:not(.canary-badge) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
margin: 4px 2px 4px -8px;
|
||||
border-radius: $border-radius-default;
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
@include gl-focus($focus-ring: $focus-ring-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown.open {
|
||||
> a {
|
||||
border-bottom-color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
padding: 0;
|
||||
|
||||
.nav > li {
|
||||
> a {
|
||||
will-change: color;
|
||||
margin: 4px 0;
|
||||
padding: 6px 8px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-bar-container {
|
||||
min-height: $top-bar-height;
|
||||
}
|
||||
|
||||
.top-bar-fixed {
|
||||
@include gl-inset-border-b-1-gray-100;
|
||||
background-color: $body-bg;
|
||||
left: var(--application-bar-left);
|
||||
position: fixed;
|
||||
right: var(--application-bar-right);
|
||||
top: $calc-application-bars-height;
|
||||
width: auto;
|
||||
z-index: $top-bar-z-index;
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: left $gl-transition-duration-medium, right $gl-transition-duration-medium;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
align-self: center;
|
||||
color: $gl-text-color-secondary;
|
||||
|
||||
.avatar-tile {
|
||||
margin-right: 4px;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 50%;
|
||||
vertical-align: sub;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-item-text {
|
||||
text-decoration: inherit;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
@include str-truncated(128px);
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-empty {
|
||||
|
|
@ -173,17 +60,6 @@
|
|||
@include media-breakpoint-down(xs) { margin-right: 3px; }
|
||||
}
|
||||
|
||||
.top-nav-menu-item {
|
||||
&.active,
|
||||
&:hover {
|
||||
background-color: $nav-active-bg !important;
|
||||
}
|
||||
|
||||
.gl-icon {
|
||||
color: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header-logged-out {
|
||||
z-index: $header-zindex;
|
||||
min-height: var(--header-height);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
.top-bar-container {
|
||||
min-height: $top-bar-height;
|
||||
}
|
||||
|
||||
.top-bar-fixed {
|
||||
@include gl-inset-border-b-1-gray-100;
|
||||
background-color: $body-bg;
|
||||
left: var(--application-bar-left);
|
||||
position: fixed;
|
||||
right: var(--application-bar-right);
|
||||
top: $calc-application-bars-height;
|
||||
width: auto;
|
||||
z-index: $top-bar-z-index;
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: left $gl-transition-duration-medium, right $gl-transition-duration-medium;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ class UploadsController < ApplicationController
|
|||
"projects/topic" => Projects::Topic,
|
||||
'alert_management_metric_image' => ::AlertManagement::MetricImage,
|
||||
"achievements/achievement" => Achievements::Achievement,
|
||||
"organizations/organization_detail" => Organizations::OrganizationDetail,
|
||||
"abuse_report" => AbuseReport,
|
||||
nil => PersonalSnippet
|
||||
}.freeze
|
||||
|
|
@ -65,6 +66,8 @@ class UploadsController < ApplicationController
|
|||
can?(current_user, :read_alert_management_metric_image, model.alert)
|
||||
when ::Achievements::Achievement
|
||||
true
|
||||
when Organizations::OrganizationDetail
|
||||
can?(current_user, :read_organization, model.organization)
|
||||
else
|
||||
can?(current_user, "read_#{model.class.underscore}".to_sym, model)
|
||||
end
|
||||
|
|
@ -96,7 +99,7 @@ class UploadsController < ApplicationController
|
|||
|
||||
def cache_settings
|
||||
case model
|
||||
when User, Appearance, Projects::Topic, Achievements::Achievement
|
||||
when User, Appearance, Projects::Topic, Achievements::Achievement, Organizations::OrganizationDetail
|
||||
[5.minutes, { public: true, must_revalidate: false }]
|
||||
when Project, Group
|
||||
[5.minutes, { private: true, must_revalidate: true }]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ module Organizations
|
|||
module OrganizationHelper
|
||||
def organization_show_app_data(organization)
|
||||
{
|
||||
organization: organization.slice(:id, :name),
|
||||
organization: organization.slice(:id, :name).merge({ avatar_url: organization.avatar_url(size: 128) }),
|
||||
groups_and_projects_organization_path: groups_and_projects_organization_path(organization),
|
||||
# TODO: Update counts to use real data
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/424531
|
||||
|
|
@ -25,7 +25,7 @@ module Organizations
|
|||
|
||||
def organization_settings_general_app_data(organization)
|
||||
{
|
||||
organization: organization.slice(:id, :name, :path),
|
||||
organization: organization.slice(:id, :name, :path).merge({ avatar: organization.avatar_url(size: 192) }),
|
||||
organizations_path: organizations_path,
|
||||
root_url: root_url
|
||||
}.to_json
|
||||
|
|
|
|||
|
|
@ -19,6 +19,23 @@ module ContainerRegistry
|
|||
validates :repository_path_pattern, presence: true, uniqueness: { scope: :project_id }, length: { maximum: 255 }
|
||||
validates :delete_protected_up_to_access_level, presence: true
|
||||
validates :push_protected_up_to_access_level, presence: true
|
||||
|
||||
scope :for_repository_path, ->(repository_path) do
|
||||
return none if repository_path.blank?
|
||||
|
||||
where(
|
||||
":repository_path ILIKE #{::Gitlab::SQL::Glob.to_like('repository_path_pattern')}",
|
||||
repository_path: repository_path
|
||||
)
|
||||
end
|
||||
|
||||
def self.for_push_exists?(access_level:, repository_path:)
|
||||
return false if access_level.blank? || repository_path.blank?
|
||||
|
||||
where(push_protected_up_to_access_level: access_level..)
|
||||
.for_repository_path(repository_path)
|
||||
.exists?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ module Organizations
|
|||
'organizations/path': true,
|
||||
length: { minimum: 2, maximum: 255 }
|
||||
|
||||
delegate :description, :avatar, :avatar_url, to: :organization_detail
|
||||
delegate :description, :avatar, :avatar_url, :remove_avatar!, to: :organization_detail
|
||||
|
||||
accepts_nested_attributes_for :organization_detail
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ module Organizations
|
|||
def execute
|
||||
return error_no_permissions unless allowed?
|
||||
|
||||
if params[:organization_detail_attributes].key?(:avatar) && params[:organization_detail_attributes][:avatar].nil?
|
||||
organization.remove_avatar!
|
||||
end
|
||||
|
||||
if organization.update(params)
|
||||
ServiceResponse.success(payload: { organization: organization })
|
||||
else
|
||||
|
|
|
|||
|
|
@ -104,7 +104,10 @@
|
|||
|
||||
- if todos_filter_empty?
|
||||
%p
|
||||
= (s_("Todos|Are you looking for things to do? Take a look at %{strongStart}%{openIssuesLinkStart}open issues%{openIssuesLinkEnd}%{strongEnd}, contribute to %{strongStart}%{mergeRequestLinkStart}a merge request%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}, or mention someone in a comment to automatically assign them a new to-do item.") % { strongStart: '<strong>', strongEnd: '</strong>', openIssuesLinkStart: "<a href=\"#{issues_dashboard_path}\">", openIssuesLinkEnd: '</a>', mergeRequestLinkStart: "<a href=\"#{merge_requests_dashboard_path}\">", mergeRequestLinkEnd: '</a>' }).html_safe
|
||||
= (s_("Todos|Not sure where to go next? Take a look at your %{strongStart}%{assignedIssuesLinkStart}assigned issues%{assignedIssuesLinkEnd}%{strongEnd} or %{strongStart}%{mergeRequestLinkStart}merge requests%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}.") % { strongStart: '<strong>', strongEnd: '</strong>', assignedIssuesLinkStart: "<a href=\"#{issues_dashboard_path(assignee_username: current_user.username)}\">", assignedIssuesLinkEnd: '</a>', mergeRequestLinkStart: "<a href=\"#{merge_requests_dashboard_path(assignee_username: current_user.username)}\">", mergeRequestLinkEnd: '</a>' }).html_safe
|
||||
%p
|
||||
= link_to s_("Todos| What actions create to-do items?"), help_page_path('user/todos', anchor: 'actions-that-create-to-do-items'), target: '_blank', rel: 'noopener noreferrer'
|
||||
|
||||
- elsif todos_has_filtered_results?
|
||||
%p
|
||||
= link_to s_("Todos|Do you want to remove the filters?"), todos_filter_path(without: [:project_id, :author_id, :type, :action_id])
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: ci_job_token_scope
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49750
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300821
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::container registry
|
||||
default_enabled: false
|
||||
|
|
@ -4,7 +4,10 @@ scope path: :uploads do
|
|||
# Note attachments and User/Group/Project/Topic avatars
|
||||
get "-/system/:model/:mounted_as/:id/:filename",
|
||||
to: "uploads#show",
|
||||
constraints: { model: %r{note|user|group|project|projects\/topic|achievements\/achievement}, mounted_as: /avatar|attachment/, filename: %r{[^/]+} }
|
||||
constraints: {
|
||||
model: %r{note|user|group|project|projects\/topic|achievements\/achievement|organizations\/organization_detail},
|
||||
mounted_as: /avatar|attachment/, filename: %r{[^/]+}
|
||||
}
|
||||
|
||||
# show uploads for models, snippets (notes) available for now
|
||||
get '-/system/:model/:id/:secret/:filename',
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ When administering the GitLab for Jira Cloud app for self-managed instances, you
|
|||
|
||||
For GitLab.com, see [GitLab for Jira Cloud app](../../integration/jira/connect-app.md#troubleshooting).
|
||||
|
||||
## Browser displays a sign-in message when already signed in
|
||||
## Sign-in message displayed when already signed in
|
||||
|
||||
You might get the following message prompting you to sign in to GitLab.com
|
||||
when you're already signed in:
|
||||
|
|
@ -26,7 +26,8 @@ To resolve this issue, set up [OAuth authentication](jira_cloud_app.md#set-up-oa
|
|||
|
||||
## Manual installation fails
|
||||
|
||||
You might see one of the following errors if you have installed the GitLab for Jira Cloud app from the official marketplace listing and replaced it with [manual installation](jira_cloud_app.md#install-the-gitlab-for-jira-cloud-app-manually):
|
||||
You might get one of the following errors if you've installed the GitLab for Jira Cloud app
|
||||
from the official marketplace listing and replaced it with [manual installation](jira_cloud_app.md#install-the-gitlab-for-jira-cloud-app-manually):
|
||||
|
||||
```plaintext
|
||||
The app "gitlab-jira-connect-gitlab.com" could not be installed as a local app as it has previously been installed from Atlassian Marketplace
|
||||
|
|
@ -51,7 +52,7 @@ To resolve this issue, disable the **Jira Connect Proxy URL** setting.
|
|||
1. Clear the **Jira Connect Proxy URL** text box.
|
||||
1. Select **Save changes**.
|
||||
|
||||
## Data sync fails with `Invalid JWT` error
|
||||
## Data sync fails with `Invalid JWT`
|
||||
|
||||
If the GitLab for Jira Cloud app continuously fails to sync data, it may be due to an outdated secret token. Atlassian can send new secret tokens that must be processed and stored by GitLab.
|
||||
If GitLab fails to store the token or misses the new token request, an `Invalid JWT` error occurs.
|
||||
|
|
@ -111,7 +112,8 @@ tools while reproducing the `Failed to update the GitLab instance` error to see
|
|||
|
||||
You should see a `GET` request to `https://gitlab.com/-/jira_connect/installations`.
|
||||
|
||||
This request should return a `200` status code, but it can return a `422` status code if there was a problem. The response body can be checked for the error.
|
||||
This request should return a `200 OK`, but it might return a `422 Unprocessable Entity` if there was a problem.
|
||||
You can check the response body for the error.
|
||||
|
||||
If you cannot resolve the problem and you are a GitLab customer, contact [GitLab Support](https://about.gitlab.com/support/) for assistance. Provide
|
||||
GitLab Support with:
|
||||
|
|
@ -123,7 +125,7 @@ GitLab Support with:
|
|||
|
||||
The GitLab Support team can then look up why this is failing in the GitLab.com server logs.
|
||||
|
||||
#### Process for GitLab Support
|
||||
#### GitLab Support
|
||||
|
||||
NOTE:
|
||||
These steps can only be completed by GitLab Support.
|
||||
|
|
@ -163,6 +165,6 @@ When you check the browser console, you might see the following message:
|
|||
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://gitlab.example.com/-/jira_connect/oauth_application_id. (Reason: CORS header 'Access-Control-Allow-Origin' missing). Status code: 403.
|
||||
```
|
||||
|
||||
`403` status code is returned if the user information cannot be fetched from Jira because of insufficient permissions.
|
||||
A `403 Forbidden` is returned if the user information cannot be fetched from Jira because of insufficient permissions.
|
||||
|
||||
To resolve this issue, ensure that the Jira user that installs and configures the GitLab for Jira Cloud app meets certain [requirements](jira_cloud_app.md#jira-user-requirements).
|
||||
|
|
|
|||
|
|
@ -6,28 +6,14 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Container registry API **(FREE ALL)**
|
||||
|
||||
> The use of `CI_JOB_TOKEN` scoped to the current project was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49750) in GitLab 13.12.
|
||||
> - The ability to authenticate with a CI/CD job token [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49750) in GitLab 13.12 [with a flag](../administration/feature_flags.md) named `ci_job_token_scope`. Disabled by default.
|
||||
> - CI/CD job token authentication [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/300821) in GitLab 16.8. Feature flag `ci_job_token_scope` removed.
|
||||
|
||||
This API documentation is about the [GitLab container registry](../user/packages/container_registry/index.md).
|
||||
Use these API endpoints to work with the [GitLab container registry](../user/packages/container_registry/index.md).
|
||||
|
||||
When the `ci_job_token_scope` feature flag is enabled (it is **disabled by default**), you can use the below endpoints
|
||||
from a CI/CD job, by passing the `$CI_JOB_TOKEN` variable as the `JOB-TOKEN` header.
|
||||
The job token only has access to its own project.
|
||||
|
||||
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
|
||||
can opt to enable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:ci_job_token_scope)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:ci_job_token_scope)
|
||||
```
|
||||
You can authenticate with these endpoints from a CI/CD job by passing the [`$CI_JOB_TOKEN`](../ci/jobs/ci_job_token.md)
|
||||
variable as the `JOB-TOKEN` header. The job token only has access to the container registry
|
||||
of the project that created the pipeline.
|
||||
|
||||
## Change the visibility of the container registry
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ You can use a GitLab CI/CD job token to authenticate with specific API endpoints
|
|||
- [Container registry](../../user/packages/container_registry/build_and_push_images.md#use-gitlab-cicd)
|
||||
(the `$CI_REGISTRY_PASSWORD` is `$CI_JOB_TOKEN`).
|
||||
- [Container registry API](../../api/container_registry.md)
|
||||
(scoped to the job's project, when the `ci_job_token_scope` feature flag is enabled).
|
||||
(scoped to the job's project).
|
||||
- [Get job artifacts](../../api/job_artifacts.md#get-job-artifacts).
|
||||
- [Get job token's job](../../api/jobs.md#get-job-tokens-job).
|
||||
- [Pipeline triggers](../../api/pipeline_triggers.md), using the `token=` parameter
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ namespace. Code Owners is an EE-only feature, so the files only exist in the `./
|
|||
### `ProtectedBranch`
|
||||
|
||||
The `ProtectedBranch` model is defined in `app/models/protected_branch.rb` and
|
||||
extended in `ee/app/ee/models/protected_branch.rb`. The EE version includes a column
|
||||
extended in `ee/app/models/concerns/ee/protected_branch.rb`. The EE version includes a column
|
||||
named `require_code_owner_approval` which prevents changes from being pushed directly
|
||||
to the branch being protected if the file is listed in `CODEOWNERS`.
|
||||
|
||||
|
|
|
|||
|
|
@ -1237,8 +1237,7 @@ and annoying for users.
|
|||
If you're describing a complicated interaction in the user interface and want to
|
||||
include a visual representation to help readers understand it, you can:
|
||||
|
||||
- Use a static image (screenshot) and if necessary, add callouts to emphasize an
|
||||
an area of the screen.
|
||||
- Use a static image (screenshot) and if necessary, add callouts to emphasize an area of the screen.
|
||||
- Create a short video of the interaction and link to it.
|
||||
|
||||
### Automatic screenshot generator
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Troubleshooting Jira DVCS connector **(FREE ALL)**
|
||||
|
||||
Refer to the items in this section if you're having problems with your Jira DVCS connector.
|
||||
When working with the [Jira DVCS connector](index.md), you might encounter the following issues.
|
||||
|
||||
## Jira cannot access GitLab server
|
||||
## Jira cannot access the GitLab server
|
||||
|
||||
If you complete the **Add New Account** form, authorize access, and you receive
|
||||
this error, Jira and GitLab cannot connect. No other error messages
|
||||
|
|
@ -68,7 +68,7 @@ The message `Successfully connected` indicates a successful TLS handshake.
|
|||
If there are problems, the Java TLS library generates errors that you can
|
||||
look up for more detail.
|
||||
|
||||
## Scope error when connecting to Jira using DVCS
|
||||
## Scope error when connecting to Jira with DVCS
|
||||
|
||||
```plaintext
|
||||
The requested scope is invalid, unknown, or malformed.
|
||||
|
|
@ -83,7 +83,7 @@ Potential resolutions:
|
|||
[GitLab account configuration](index.md#create-a-gitlab-application-for-dvcs). Review
|
||||
the **Scopes** field and ensure the `api` checkbox is selected.
|
||||
|
||||
## Jira error adding account and no repositories listed
|
||||
## Error when adding an account in Jira
|
||||
|
||||
After you complete the **Add New Account** form in Jira and authorize access, you might
|
||||
encounter these issues:
|
||||
|
|
@ -100,13 +100,13 @@ To resolve this issue:
|
|||
|
||||
[Contact GitLab Support](https://about.gitlab.com/support/) if none of these reasons apply.
|
||||
|
||||
## `410 : Gone` error when connecting to Jira
|
||||
## `410 Gone` when connecting to Jira
|
||||
|
||||
When you connect to Jira and synchronize repositories, you may receive a `410 : Gone` error.
|
||||
When you connect to Jira and synchronize repositories, you might get a `410 Gone` error.
|
||||
|
||||
This issue occurs when you use the Jira DVCS connector and your integration is configured to use **GitHub Enterprise**.
|
||||
|
||||
For more information and possible fixes, see [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/340160).
|
||||
For more information, see [issue 340160](https://gitlab.com/gitlab-org/gitlab/-/issues/340160).
|
||||
|
||||
## Synchronization issues
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ resynchronize the information:
|
|||
For more information, see the
|
||||
[Atlassian documentation](https://support.atlassian.com/jira-cloud-administration/docs/integrate-with-development-tools/).
|
||||
|
||||
## `Sync Failed` error when refreshing repository data
|
||||
## `Sync Failed` when refreshing repository data
|
||||
|
||||
If you get a `Sync Failed` error in Jira when [refreshing repository data](index.md#refresh-data-imported-to-jira) for specific projects, check your Jira DVCS connector logs. Look for errors that occur when executing requests to API resources in GitLab. For example:
|
||||
|
||||
|
|
@ -132,8 +132,8 @@ Failed to execute request [https://gitlab.com/api/v4/projects/:id/merge_requests
|
|||
{"message":"403 Forbidden"}
|
||||
```
|
||||
|
||||
If you find a `{"message":"403 Forbidden"}` error, it is possible that this specific project has some [GitLab features disabled](../../../user/project/settings/project_features_permissions.md#configure-project-features-and-permissions).
|
||||
In the example above, the merge requests feature is disabled.
|
||||
If you get a `403 Forbidden` error, this project might have some [GitLab features disabled](../../../user/project/settings/project_features_permissions.md#configure-project-features-and-permissions).
|
||||
In the previous example, the merge requests feature is disabled.
|
||||
|
||||
To resolve the issue, enable the relevant feature:
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Troubleshooting Jira issue integration **(FREE ALL)**
|
||||
|
||||
This page contains a list of common issues you might encounter when working with the [Jira issue integration](configure.md).
|
||||
When working with the [Jira issue integration](configure.md), you might encounter the following issues.
|
||||
|
||||
## GitLab cannot link to a Jira issue
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ To resolve this issue, see
|
|||
WARNING:
|
||||
Commands that change data can cause damage if not run correctly or under the right conditions. Always run commands in a test environment first and have a backup instance ready to restore.
|
||||
|
||||
### Change all projects on the instance
|
||||
### Change all projects on an instance
|
||||
|
||||
To change all Jira projects to use instance-level integration settings:
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ To change all Jira projects in a group (and its subgroups) to use group-level in
|
|||
end
|
||||
```
|
||||
|
||||
## Update the Jira issue integration password for all projects
|
||||
## Update the integration password for all projects
|
||||
|
||||
WARNING:
|
||||
Commands that change data can cause damage if not run correctly or under the right conditions. Always run commands in a test environment first and have a backup instance ready to restore.
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ When configuring the GitLab for Slack app on GitLab.com, you might encounter the
|
|||
|
||||
For self-managed GitLab, see [GitLab for Slack app administration](../../../administration/settings/slack_app.md#troubleshooting).
|
||||
|
||||
## The app does not appear in the list of integrations
|
||||
## App does not appear in the list of integrations
|
||||
|
||||
The GitLab for Slack app might not appear in the list of integrations. To have the GitLab for Slack app on your self-managed instance, an administrator must [enable the integration](../../../administration/settings/slack_app.md). On GitLab.com, the GitLab for Slack app is available by default.
|
||||
|
||||
The GitLab for Slack app is enabled at the project level only. Support for the app at the group and instance levels is proposed in [issue 391526](https://gitlab.com/gitlab-org/gitlab/-/issues/391526).
|
||||
|
||||
## Project or alias not found
|
||||
## `Project or alias not found`
|
||||
|
||||
Some Slack commands must have a project full path or alias and fail with the following error
|
||||
if the project cannot be found:
|
||||
|
|
@ -36,13 +36,13 @@ To resolve this issue, ensure:
|
|||
Slash commands might return `/gitlab failed with the error "dispatch_failed"` in Slack.
|
||||
To resolve this issue, ensure an administrator has properly configured the [GitLab for Slack app settings](../../../administration/settings/slack_app.md) on your self-managed instance.
|
||||
|
||||
## Notifications are not received to a channel
|
||||
## Notifications not received to a channel
|
||||
|
||||
If you're not receiving notifications to a Slack channel, ensure:
|
||||
|
||||
- The channel name you configured is correct.
|
||||
- If the channel is private, you've [added the GitLab for Slack app to the channel](gitlab_slack_application.md#receive-notifications-to-a-private-channel).
|
||||
|
||||
## The App Home does not display properly
|
||||
## App Home does not display properly
|
||||
|
||||
If the [App Home](https://api.slack.com/start/overview#app_home) does not display properly, ensure your [app is up to date](gitlab_slack_application.md#update-the-gitlab-for-slack-app).
|
||||
|
|
|
|||
|
|
@ -101,6 +101,15 @@ NOTE:
|
|||
The Max role does not elevate the privileges of users.
|
||||
For example, if a group member has the role of Developer, and the group is invited to a project with a Max role of Maintainer, the member's role is not elevated to Maintainer.
|
||||
|
||||
### Which roles you can assign
|
||||
|
||||
In GitLab [16.7](https://gitlab.com/gitlab-org/gitlab/-/issues/233408) and later, the maximum role you can assign depends on whether you have the Owner or Maintainer role for the project. The maximum role you can set is:
|
||||
|
||||
- Owner (`50`), if you have the Owner role for the project.
|
||||
- Maintainer (`40`), if you have the Maintainer role for the project.
|
||||
|
||||
In GitLab 16.6 and earlier, the maximum role you can assign to an invited group is Maintainer (`40`).
|
||||
|
||||
### View the member's Max role
|
||||
|
||||
To view the maximum role assigned to a member:
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
Projects and groups in GitLab can be private, internal, or public.
|
||||
|
||||
The visibility level of the group or project has no influence on whether members within the group or project can see each other.
|
||||
A group or project is an object to allow collaborative work. This is only possible if all members know about each other.
|
||||
The visibility level of the project or group does not affect whether members of the project or group can see each other.
|
||||
Projects and groups are intended for collaborative work. This work is only possible if all members know about each other.
|
||||
|
||||
Group or project members can see all members of the group or project they belong to.
|
||||
Group or project owners can see the origin of membership (the original group or project) of all members.
|
||||
Project or group members can see all members of the project or group they belong to.
|
||||
Project or group members can see the origin of membership (the original project or group) of all members for the projects and groups they have access to.
|
||||
|
||||
## Private projects and groups
|
||||
|
||||
|
|
@ -38,15 +38,9 @@ Only internal members can view internal content.
|
|||
|
||||
Internal groups can have internal or private subgroups.
|
||||
|
||||
NOTE:
|
||||
From July 2019, the `Internal` visibility setting is disabled for new projects, groups,
|
||||
and snippets on GitLab.com. Existing projects, groups, and snippets using the `Internal`
|
||||
visibility setting keep this setting. For more information, see
|
||||
[issue 12388](https://gitlab.com/gitlab-org/gitlab/-/issues/12388).
|
||||
|
||||
## Public projects and groups
|
||||
|
||||
For public projects, **users who are not authenticated**, including users with the Guest role, can:
|
||||
For public projects, **unauthenticated users**, including users with the Guest role, can:
|
||||
|
||||
- Clone the project.
|
||||
- View the public access directory (`/public`).
|
||||
|
|
@ -56,7 +50,7 @@ Public groups can have public, internal, or private subgroups.
|
|||
NOTE:
|
||||
If an administrator restricts the
|
||||
[**Public** visibility level](../administration/settings/visibility_and_access_controls.md#restrict-visibility-levels),
|
||||
then `/public` is visible only to authenticated users.
|
||||
then the public access directory (`/public`) is visible only to authenticated users.
|
||||
|
||||
## Change project visibility
|
||||
|
||||
|
|
@ -85,7 +79,7 @@ Prerequisites:
|
|||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Settings > General**.
|
||||
1. Expand **Visibility, project features, permissions**.
|
||||
1. To enable or disable a feature, turn on or off the feature toggle.
|
||||
1. To enable or disable a feature, turn on or turn off the feature toggle.
|
||||
1. Select **Save changes**.
|
||||
|
||||
## Change group visibility
|
||||
|
|
@ -95,9 +89,9 @@ You can change the visibility of all projects in a group.
|
|||
Prerequisites:
|
||||
|
||||
- You must have the Owner role for a group.
|
||||
- Subgroups and projects must already have visibility settings that are at least as
|
||||
- Projects and subgroups must already have visibility settings that are at least as
|
||||
restrictive as the new setting of the parent group. For example, you cannot set a group
|
||||
to private if a subgroup or project in that group is public.
|
||||
to private if a project or subgroup in that group is public.
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your group.
|
||||
1. Select **Settings > General**.
|
||||
|
|
|
|||
|
|
@ -184,8 +184,7 @@ module API
|
|||
return true unless job_token_authentication?
|
||||
return true unless route_authentication_setting[:job_token_scope] == :project
|
||||
|
||||
::Feature.enabled?(:ci_job_token_scope, project) &&
|
||||
current_authenticated_job.project == project
|
||||
current_authenticated_job.project == project
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ module Integrations
|
|||
GLGO_BASE_URL = if Gitlab.staging?
|
||||
'https://glgo.staging.runway.gitlab.net'
|
||||
else
|
||||
'http://glgo.runway.gitlab.net/'
|
||||
'https://glgo.runway.gitlab.net'
|
||||
end
|
||||
|
||||
def initialize(project:, user:)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ module Sidebars
|
|||
override :serialize_as_menu_item_args
|
||||
def serialize_as_menu_item_args
|
||||
super.merge({
|
||||
avatar: nil,
|
||||
avatar: context.container.avatar_url(size: 48),
|
||||
entity_id: context.container.id,
|
||||
super_sidebar_parent: ::Sidebars::StaticMenu,
|
||||
item_id: :organization_overview
|
||||
|
|
|
|||
|
|
@ -18066,6 +18066,9 @@ msgstr ""
|
|||
msgid "Drag your designs here or %{linkStart}click to upload%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Drop or %{linkStart}upload%{linkEnd} an avatar."
|
||||
msgstr ""
|
||||
|
||||
msgid "Drop or %{linkStart}upload%{linkEnd} file to attach"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -33869,6 +33872,9 @@ msgstr ""
|
|||
msgid "Organization|Organization URL successfully changed."
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization|Organization avatar"
|
||||
msgstr ""
|
||||
|
||||
msgid "Organization|Organization name"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -50808,6 +50814,9 @@ msgstr ""
|
|||
msgid "Today"
|
||||
msgstr ""
|
||||
|
||||
msgid "Todos| What actions create to-do items?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Todos|Added"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -50820,9 +50829,6 @@ msgstr ""
|
|||
msgid "Todos|Any Type"
|
||||
msgstr ""
|
||||
|
||||
msgid "Todos|Are you looking for things to do? Take a look at %{strongStart}%{openIssuesLinkStart}open issues%{openIssuesLinkEnd}%{strongEnd}, contribute to %{strongStart}%{mergeRequestLinkStart}a merge request%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}, or mention someone in a comment to automatically assign them a new to-do item."
|
||||
msgstr ""
|
||||
|
||||
msgid "Todos|Assigned"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -50880,6 +50886,9 @@ msgstr ""
|
|||
msgid "Todos|Merge request"
|
||||
msgstr ""
|
||||
|
||||
msgid "Todos|Not sure where to go next? Take a look at your %{strongStart}%{assignedIssuesLinkStart}assigned issues%{assignedIssuesLinkEnd}%{strongEnd} or %{strongStart}%{mergeRequestLinkStart}merge requests%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Todos|Nothing is on your to-do list. Nice work!"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Specs
|
||||
module Helpers
|
||||
class FastQuarantine
|
||||
include Support::API
|
||||
|
||||
class << self
|
||||
def configure!
|
||||
return unless ENV["CI"]
|
||||
return if ENV["FAST_QUARANTINE"] == "false"
|
||||
return if ENV["CI_MERGE_REQUEST_LABELS"]&.include?("pipeline:run-flaky-tests")
|
||||
|
||||
Runtime::Logger.debug("Running fast quarantine setup")
|
||||
setup = new
|
||||
setup.fetch_fq_file
|
||||
setup.configure_rspec
|
||||
rescue StandardError => e
|
||||
Runtime::Logger.error("Failed to setup FastQuarantine, error: '#{e.class} - #{e.message}'")
|
||||
end
|
||||
end
|
||||
|
||||
private_class_method :new
|
||||
|
||||
def initialize
|
||||
@logger = Runtime::Logger.logger
|
||||
@fq_filename = "fast_quarantine-gitlab.txt"
|
||||
end
|
||||
|
||||
# Fetch and save fast quarantine file
|
||||
#
|
||||
# @return [void]
|
||||
def fetch_fq_file
|
||||
download_fast_quarantine
|
||||
end
|
||||
|
||||
# Configure rspec
|
||||
#
|
||||
# @return [void]
|
||||
def configure_rspec
|
||||
# Shared tooling that adds relevant rspec configuration
|
||||
require_relative '../../../../spec/support/fast_quarantine'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :logger, :fq_filename
|
||||
|
||||
# Force path to be relative to ruby process in order to avoid issues when dealing with different execution
|
||||
# contexts of qa docker container and CI runner environment
|
||||
def fq_path
|
||||
@fq_path ||= ENV["RSPEC_FAST_QUARANTINE_PATH"] = File.join(Runtime::Path.qa_root, "tmp", fq_filename)
|
||||
end
|
||||
|
||||
def download_fast_quarantine
|
||||
logger.debug(" downloading fast quarantine file")
|
||||
response = get(
|
||||
"https://gitlab-org.gitlab.io/quality/engineering-productivity/fast-quarantine/rspec/#{fq_filename}",
|
||||
verify_ssl: true
|
||||
)
|
||||
raise "Failed to download fast quarantine file: #{response.code}" if response.code != HTTP_STATUS_OK
|
||||
|
||||
logger.debug(" saving fast quarantine file to '#{fq_path}'")
|
||||
File.write(fq_path, response.body)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -5,9 +5,6 @@ require 'factory_bot'
|
|||
|
||||
require_relative '../../qa'
|
||||
|
||||
# Require shared test tooling from Rails test suite
|
||||
require_relative '../../../spec/support/fast_quarantine'
|
||||
|
||||
QA::Specs::QaDeprecationToolkitEnv.configure!
|
||||
|
||||
Knapsack::Adapters::RSpecAdapter.bind if QA::Runtime::Env.knapsack?
|
||||
|
|
@ -16,10 +13,12 @@ Knapsack::Adapters::RSpecAdapter.bind if QA::Runtime::Env.knapsack?
|
|||
QA::Support::GitlabAddress.define_gitlab_address_attribute!
|
||||
QA::Runtime::Browser.configure!
|
||||
QA::Specs::Helpers::FeatureSetup.configure!
|
||||
QA::Specs::Helpers::FastQuarantine.configure!
|
||||
QA::Runtime::AllureReport.configure!
|
||||
QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes)
|
||||
QA::Service::DockerRun::Video.configure!
|
||||
|
||||
QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes)
|
||||
|
||||
# Enable zero monkey patching mode before loading any other RSpec code.
|
||||
RSpec.configure(&:disable_monkey_patching!)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
# QA framework unit tests
|
||||
|
||||
To run framework unit tests, following command can be used:
|
||||
To run all the unit tests under the framework, following command can be used:
|
||||
|
||||
```shell
|
||||
bundle exec rspec -O .rspec_internal
|
||||
```
|
||||
|
||||
To run individual unit test, following command can be used:
|
||||
|
||||
```shell
|
||||
bundle exec rspec -O .rspec_internal spec/spec_path/file_spec.rb
|
||||
```
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe QA::Specs::Helpers::FastQuarantine do
|
||||
include QA::Support::Helpers::StubEnv
|
||||
|
||||
let(:response) { instance_double(RestClient::Response, code: 200, body: fq_contents) }
|
||||
let(:fq_path) { File.join(QA::Runtime::Path.qa_root, "tmp", "fast_quarantine-gitlab.txt") }
|
||||
let(:fq_contents) { "fast_quarantine_contents" }
|
||||
|
||||
before do
|
||||
stub_env("CI", "true")
|
||||
|
||||
allow(RSpec).to receive(:configure)
|
||||
allow(File).to receive(:write).with(fq_path, fq_contents)
|
||||
allow(RestClient::Request).to receive(:execute).and_return(response)
|
||||
|
||||
# silence log messages during test execution
|
||||
allow(QA::Runtime::Logger).to receive(:logger).and_return(instance_double(ActiveSupport::Logger, debug: nil))
|
||||
allow(QA::Runtime::Logger).to receive(:debug)
|
||||
|
||||
described_class.configure!
|
||||
end
|
||||
|
||||
it "configures fast quarantine" do
|
||||
expect(RSpec).to have_received(:configure)
|
||||
expect(File).to have_received(:write).with(fq_path, fq_contents)
|
||||
expect(RestClient::Request).to have_received(:execute).with(
|
||||
cookies: {},
|
||||
method: :get,
|
||||
url: "https://gitlab-org.gitlab.io/quality/engineering-productivity/fast-quarantine/rspec/fast_quarantine-gitlab.txt",
|
||||
verify_ssl: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -776,6 +776,45 @@ RSpec.describe UploadsController, feature_category: :groups_and_projects do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when viewing an organization avatar' do
|
||||
let(:organization_detail) { create(:organization_detail) }
|
||||
let(:organization) { organization_detail.organization }
|
||||
|
||||
subject(:request) do
|
||||
get(
|
||||
:show,
|
||||
params: {
|
||||
model: 'organizations/organization_detail',
|
||||
mounted_as: 'avatar',
|
||||
id: organization.id,
|
||||
filename: 'dk.png'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
context 'when signed in' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'responds with status 200' do
|
||||
request
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it_behaves_like 'content publicly cached'
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
it 'responds with status 200' do
|
||||
request
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
|
||||
it_behaves_like 'content publicly cached'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def post_authorize(verified: true)
|
||||
|
|
|
|||
|
|
@ -33,11 +33,11 @@ RSpec.describe 'Dashboard Todos', :js, feature_category: :team_planning do
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'shows "Are you looking for things to do?" message' do
|
||||
it 'shows "Not sure where to go next?" message' do
|
||||
create(:todo, :assigned, :done, user: user, project: project, target: issue, author: user2)
|
||||
visit dashboard_todos_path
|
||||
|
||||
expect(page).to have_content 'Are you looking for things to do? Take a look at open issues, contribute to a merge request, or mention someone in a comment to automatically assign them a new to-do item.'
|
||||
expect(page).to have_content 'Not sure where to go next? Take a look at your assigned issues or merge requests.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
|||
import OrganizationSettings from '~/organizations/settings/general/components/organization_settings.vue';
|
||||
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
|
||||
import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
|
||||
import { FORM_FIELD_NAME, FORM_FIELD_ID } from '~/organizations/shared/constants';
|
||||
import {
|
||||
FORM_FIELD_NAME,
|
||||
FORM_FIELD_ID,
|
||||
FORM_FIELD_AVATAR,
|
||||
} from '~/organizations/shared/constants';
|
||||
import organizationUpdateMutation from '~/organizations/settings/general/graphql/mutations/organization_update.mutation.graphql';
|
||||
import {
|
||||
organizationUpdateResponse,
|
||||
|
|
@ -38,22 +42,27 @@ describe('OrganizationSettings', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const file = new File(['foo'], 'foo.jpg', {
|
||||
type: 'text/plain',
|
||||
});
|
||||
|
||||
const successfulResponseHandler = jest.fn().mockResolvedValue(organizationUpdateResponse);
|
||||
|
||||
const createComponent = ({
|
||||
handlers = [[organizationUpdateMutation, successfulResponseHandler]],
|
||||
provide = {},
|
||||
} = {}) => {
|
||||
mockApollo = createMockApollo(handlers);
|
||||
|
||||
wrapper = shallowMountExtended(OrganizationSettings, {
|
||||
provide: defaultProvide,
|
||||
provide: { ...defaultProvide, ...provide },
|
||||
apolloProvider: mockApollo,
|
||||
});
|
||||
};
|
||||
|
||||
const findForm = () => wrapper.findComponent(NewEditForm);
|
||||
const submitForm = async () => {
|
||||
findForm().vm.$emit('submit', { name: 'Foo bar', path: 'foo-bar' });
|
||||
const submitForm = async (data = {}) => {
|
||||
findForm().vm.$emit('submit', { name: 'Foo bar', path: 'foo-bar', avatar: file, ...data });
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
|
|
@ -75,7 +84,7 @@ describe('OrganizationSettings', () => {
|
|||
expect(findForm().props()).toMatchObject({
|
||||
loading: false,
|
||||
initialFormValues: defaultProvide.organization,
|
||||
fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID],
|
||||
fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_AVATAR],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -108,6 +117,7 @@ describe('OrganizationSettings', () => {
|
|||
input: {
|
||||
id: 'gid://gitlab/Organizations::Organization/1',
|
||||
name: 'Foo bar',
|
||||
avatar: file,
|
||||
},
|
||||
});
|
||||
expect(visitUrlWithAlerts).toHaveBeenCalledWith(window.location.href, [
|
||||
|
|
@ -162,5 +172,46 @@ describe('OrganizationSettings', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when organization has avatar', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
provide: { organization: { ...defaultProvide.organization, avatar: 'avatar.jpg' } },
|
||||
});
|
||||
});
|
||||
|
||||
describe('when avatar is explicitly removed', () => {
|
||||
beforeEach(async () => {
|
||||
await submitForm({ avatar: null });
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('sets `avatar` argument to `null`', () => {
|
||||
expect(successfulResponseHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: 'gid://gitlab/Organizations::Organization/1',
|
||||
name: 'Foo bar',
|
||||
avatar: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when avatar is not changed', () => {
|
||||
beforeEach(async () => {
|
||||
await submitForm({ avatar: 'avatar.jpg' });
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('does not pass `avatar` argument', () => {
|
||||
expect(successfulResponseHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: 'gid://gitlab/Organizations::Organization/1',
|
||||
name: 'Foo bar',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,13 @@ import { nextTick } from 'vue';
|
|||
|
||||
import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
|
||||
import OrganizationUrlField from '~/organizations/shared/components/organization_url_field.vue';
|
||||
import { FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_PATH } from '~/organizations/shared/constants';
|
||||
import AvatarUploadDropzone from '~/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue';
|
||||
import {
|
||||
FORM_FIELD_NAME,
|
||||
FORM_FIELD_ID,
|
||||
FORM_FIELD_PATH,
|
||||
FORM_FIELD_AVATAR,
|
||||
} from '~/organizations/shared/constants';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
describe('NewEditForm', () => {
|
||||
|
|
@ -32,6 +38,7 @@ describe('NewEditForm', () => {
|
|||
const findNameField = () => wrapper.findByLabelText('Organization name');
|
||||
const findIdField = () => wrapper.findByLabelText('Organization ID');
|
||||
const findUrlField = () => wrapper.findComponent(OrganizationUrlField);
|
||||
const findAvatarField = () => wrapper.findComponent(AvatarUploadDropzone);
|
||||
|
||||
const setUrlFieldValue = async (value) => {
|
||||
findUrlField().vm.$emit('input', value);
|
||||
|
|
@ -53,6 +60,32 @@ describe('NewEditForm', () => {
|
|||
expect(findUrlField().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders `Organization avatar` field', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findAvatarField().props()).toMatchObject({
|
||||
value: null,
|
||||
entity: { [FORM_FIELD_NAME]: '', [FORM_FIELD_PATH]: '', [FORM_FIELD_AVATAR]: null },
|
||||
label: 'Organization avatar',
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `Organization avatar` field is changed', () => {
|
||||
const file = new File(['foo'], 'foo.jpg', {
|
||||
type: 'text/plain',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
window.URL.revokeObjectURL = jest.fn();
|
||||
createComponent();
|
||||
findAvatarField().vm.$emit('input', file);
|
||||
});
|
||||
|
||||
it('updates `value` prop', () => {
|
||||
expect(findAvatarField().props('value')).toEqual(file);
|
||||
});
|
||||
});
|
||||
|
||||
it('requires `Organization URL` field to be a minimum of two characters', async () => {
|
||||
createComponent();
|
||||
|
||||
|
|
@ -125,7 +158,9 @@ describe('NewEditForm', () => {
|
|||
});
|
||||
|
||||
it('emits `submit` event with form values', () => {
|
||||
expect(wrapper.emitted('submit')).toEqual([[{ name: 'Foo bar', path: 'foo-bar' }]]);
|
||||
expect(wrapper.emitted('submit')).toEqual([
|
||||
[{ name: 'Foo bar', path: 'foo-bar', avatar: null }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -858,6 +858,42 @@ describe('ReadyToMerge', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('only allow merge if pipeline succeeds', () => {
|
||||
beforeEach(() => {
|
||||
const response = JSON.parse(JSON.stringify(readyToMergeResponse));
|
||||
response.data.project.onlyAllowMergeIfPipelineSucceeds = true;
|
||||
response.data.project.mergeRequest.headPipeline = {
|
||||
id: 1,
|
||||
active: true,
|
||||
status: '',
|
||||
path: '',
|
||||
};
|
||||
|
||||
readyToMergeResponseSpy = jest.fn().mockResolvedValueOnce(response);
|
||||
});
|
||||
|
||||
it('hides merge immediately dropdown when subscription returns', async () => {
|
||||
createComponent({ mr: { id: 1 } });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findMergeImmediatelyDropdown().exists()).toBe(false);
|
||||
|
||||
mockedSubscription.next({
|
||||
data: {
|
||||
mergeRequestMergeStatusUpdated: {
|
||||
...readyToMergeResponse.data.project.mergeRequest,
|
||||
headPipeline: { id: 1, active: true, status: '', path: '' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findMergeImmediatelyDropdown().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('commit message', () => {
|
||||
it('updates commit message from subscription', async () => {
|
||||
createComponent({ mr: { id: 1 } });
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
import { GlAvatar, GlButton, GlTruncate } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import AvatarUploadDropzone from '~/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue';
|
||||
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
|
||||
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
|
||||
|
||||
describe('AvatarUploadDropzone', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultPropsData = {
|
||||
entity: { id: 1, name: 'Foo' },
|
||||
value: null,
|
||||
label: 'Avatar',
|
||||
};
|
||||
|
||||
const file = new File(['foo'], 'foo.jpg', {
|
||||
type: 'text/plain',
|
||||
});
|
||||
const file2 = new File(['bar'], 'bar.jpg', {
|
||||
type: 'text/plain',
|
||||
});
|
||||
const blob = 'blob:http://127.0.0.1:3000/0046cf8c-ea21-4720-91ef-2e354d570c75';
|
||||
|
||||
const createComponent = ({ propsData = {} } = {}) => {
|
||||
wrapper = shallowMountExtended(AvatarUploadDropzone, {
|
||||
propsData: {
|
||||
...defaultPropsData,
|
||||
...propsData,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findUploadDropzone = () => wrapper.findComponent(UploadDropzone);
|
||||
const findButton = () => wrapper.findComponent(GlButton);
|
||||
|
||||
beforeEach(() => {
|
||||
window.URL.createObjectURL = jest.fn().mockImplementation(() => blob);
|
||||
window.URL.revokeObjectURL = jest.fn();
|
||||
});
|
||||
|
||||
it('renders `GlAvatar` with correct props', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.findComponent(GlAvatar).props()).toMatchObject({
|
||||
entityId: defaultPropsData.entity.id,
|
||||
entityName: defaultPropsData.entity.name,
|
||||
shape: AVATAR_SHAPE_OPTION_RECT,
|
||||
size: 96,
|
||||
src: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders label', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.findByText(defaultPropsData.label).exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when `value` prop is updated', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
||||
// setProps is justified here because we are testing the component's
|
||||
// reactive behavior which constitutes an exception
|
||||
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
||||
wrapper.setProps({ value: file });
|
||||
});
|
||||
|
||||
it('updates `GlAvatar` `src` prop', () => {
|
||||
expect(wrapper.findComponent(GlAvatar).props('src')).toBe(blob);
|
||||
});
|
||||
|
||||
it('renders remove button', () => {
|
||||
expect(findButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders truncated file name', () => {
|
||||
expect(wrapper.findComponent(GlTruncate).props('text')).toBe('foo.jpg');
|
||||
});
|
||||
|
||||
it('does not render upload dropzone', () => {
|
||||
expect(findUploadDropzone().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when `value` prop is updated a second time', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({ value: file2 });
|
||||
});
|
||||
|
||||
it('revokes the object URL of the previous avatar', () => {
|
||||
expect(window.URL.revokeObjectURL).toHaveBeenCalledWith(blob);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when avatar is removed', () => {
|
||||
beforeEach(() => {
|
||||
findButton().vm.$emit('click');
|
||||
});
|
||||
|
||||
it('emits `input` event with `null` payload', () => {
|
||||
expect(wrapper.emitted('input')).toEqual([[null]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when `UploadDropzone` emits `change` event', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
findUploadDropzone().vm.$emit('change', file);
|
||||
});
|
||||
|
||||
it('emits `input` event', () => {
|
||||
expect(wrapper.emitted('input')).toEqual([[file]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -31,13 +31,18 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
|
|||
end
|
||||
|
||||
it 'returns expected json' do
|
||||
expect(organization).to receive(:avatar_url).with(size: 128).and_return('avatar.jpg')
|
||||
expect(
|
||||
Gitlab::Json.parse(
|
||||
helper.organization_show_app_data(organization)
|
||||
)
|
||||
).to eq(
|
||||
{
|
||||
'organization' => { 'id' => organization.id, 'name' => organization.name },
|
||||
'organization' => {
|
||||
'id' => organization.id,
|
||||
'name' => organization.name,
|
||||
'avatar_url' => 'avatar.jpg'
|
||||
},
|
||||
'groups_and_projects_organization_path' => '/-/organizations/default/groups_and_projects',
|
||||
'new_group_path' => new_group_path,
|
||||
'new_project_path' => new_project_path,
|
||||
|
|
@ -107,12 +112,14 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
|
|||
|
||||
describe '#organization_settings_general_app_data' do
|
||||
it 'returns expected json' do
|
||||
expect(organization).to receive(:avatar_url).with(size: 192).and_return('avatar.jpg')
|
||||
expect(Gitlab::Json.parse(helper.organization_settings_general_app_data(organization))).to eq(
|
||||
{
|
||||
'organization' => {
|
||||
'id' => organization.id,
|
||||
'name' => organization.name,
|
||||
'path' => organization.path
|
||||
'path' => organization.path,
|
||||
'avatar' => 'avatar.jpg'
|
||||
},
|
||||
'organizations_path' => organizations_path,
|
||||
'root_url' => root_url
|
||||
|
|
|
|||
|
|
@ -674,23 +674,15 @@ RSpec.describe API::Helpers, feature_category: :shared do
|
|||
|
||||
let(:send_authorized_project_scope) { helper.authorized_project_scope?(project) }
|
||||
|
||||
where(:job_token_authentication, :route_setting, :feature_flag, :same_job_project, :expected_result) do
|
||||
false | false | false | false | true
|
||||
false | false | false | true | true
|
||||
false | false | true | false | true
|
||||
false | false | true | true | true
|
||||
false | true | false | false | true
|
||||
false | true | false | true | true
|
||||
false | true | true | false | true
|
||||
false | true | true | true | true
|
||||
true | false | false | false | true
|
||||
true | false | false | true | true
|
||||
true | false | true | false | true
|
||||
true | false | true | true | true
|
||||
true | true | false | false | false
|
||||
true | true | false | true | false
|
||||
true | true | true | false | false
|
||||
true | true | true | true | true
|
||||
where(:job_token_authentication, :route_setting, :same_job_project, :expected_result) do
|
||||
false | false | false | true
|
||||
false | false | true | true
|
||||
false | true | false | true
|
||||
false | true | true | true
|
||||
true | false | false | true
|
||||
true | false | true | true
|
||||
true | true | false | false
|
||||
true | true | true | true
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
|
@ -699,9 +691,6 @@ RSpec.describe API::Helpers, feature_category: :shared do
|
|||
allow(helper).to receive(:route_authentication_setting).and_return(job_token_scope: route_setting ? :project : nil)
|
||||
allow(helper).to receive(:current_authenticated_job).and_return(job)
|
||||
allow(job).to receive(:project).and_return(same_job_project ? project : other_project)
|
||||
|
||||
stub_feature_flags(ci_job_token_scope: false)
|
||||
stub_feature_flags(ci_job_token_scope: project) if feature_flag
|
||||
end
|
||||
|
||||
it 'returns the expected result' do
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge, feature_category: :continuous_
|
|||
end
|
||||
end
|
||||
|
||||
describe '.visible?' do
|
||||
it 'always returns true' do
|
||||
expect(described_class.visible?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '.matching?' do
|
||||
subject { described_class.matching?(name, config) }
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::Organizations::Menus::ScopeMenu, feature_category: :navigation do
|
||||
let_it_be(:organization) { build(:organization) }
|
||||
let_it_be(:organization_detail) { build(:organization_detail) }
|
||||
let_it_be(:organization) { organization_detail.organization }
|
||||
let_it_be(:user) { build(:user) }
|
||||
let_it_be(:context) { Sidebars::Context.new(current_user: user, container: organization) }
|
||||
|
||||
|
|
@ -11,7 +12,7 @@ RSpec.describe Sidebars::Organizations::Menus::ScopeMenu, feature_category: :nav
|
|||
let(:menu) { described_class.new(context) }
|
||||
let(:extra_attrs) do
|
||||
{
|
||||
avatar: nil,
|
||||
avatar: organization.avatar_url(size: 48),
|
||||
entity_id: organization.id,
|
||||
super_sidebar_parent: ::Sidebars::StaticMenu,
|
||||
item_id: :organization_overview
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ContainerRegistry::Protection::Rule, type: :model, feature_category: :container_registry do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
it_behaves_like 'having unique enum values'
|
||||
|
||||
describe 'relationships' do
|
||||
|
|
@ -51,4 +53,192 @@ RSpec.describe ContainerRegistry::Protection::Rule, type: :model, feature_catego
|
|||
it { is_expected.to validate_presence_of(:push_protected_up_to_access_level) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.for_repository_path' do
|
||||
let_it_be(:container_registry_protection_rule) do
|
||||
create(:container_registry_protection_rule, repository_path_pattern: 'my-scope/my_container')
|
||||
end
|
||||
|
||||
let_it_be(:protection_rule_with_wildcard_start) do
|
||||
create(:container_registry_protection_rule, repository_path_pattern: '*my-scope/my_container-with-wildcard-start')
|
||||
end
|
||||
|
||||
let_it_be(:protection_rule_with_wildcard_end) do
|
||||
create(:container_registry_protection_rule, repository_path_pattern: 'my-scope/my_container-with-wildcard-end*')
|
||||
end
|
||||
|
||||
let_it_be(:protection_rule_with_wildcard_middle) do
|
||||
create(:container_registry_protection_rule,
|
||||
repository_path_pattern: 'my-scope/*my_container-with-wildcard-middle')
|
||||
end
|
||||
|
||||
let_it_be(:protection_rule_with_wildcard_double) do
|
||||
create(:container_registry_protection_rule,
|
||||
repository_path_pattern: '**my-scope/**my_container-with-wildcard-double**')
|
||||
end
|
||||
|
||||
let_it_be(:protection_rule_with_underscore) do
|
||||
create(:container_registry_protection_rule, repository_path_pattern: 'my-scope/my_container-with_____underscore')
|
||||
end
|
||||
|
||||
let_it_be(:protection_rule_with_regex_chars) do
|
||||
create(:container_registry_protection_rule, repository_path_pattern: 'my-scope/my_container-with-regex-chars.+')
|
||||
end
|
||||
|
||||
let(:repository_path) { container_registry_protection_rule.repository_path_pattern }
|
||||
|
||||
subject { described_class.for_repository_path(repository_path) }
|
||||
|
||||
context 'with several container registry protection rule scenarios' do
|
||||
where(:repository_path, :expected_container_registry_protection_rules) do
|
||||
'my-scope/my_container' | [ref(:container_registry_protection_rule)]
|
||||
'my-scope/my2container' | []
|
||||
'my-scope/my_container-2' | []
|
||||
|
||||
# With wildcard pattern at the start
|
||||
'my-scope/my_container-with-wildcard-start' | [ref(:protection_rule_with_wildcard_start)]
|
||||
'my-scope/my_container-with-wildcard-start-any' | []
|
||||
'prefix-my-scope/my_container-with-wildcard-start' | [ref(:protection_rule_with_wildcard_start)]
|
||||
'prefix-my-scope/my_container-with-wildcard-start-any' | []
|
||||
|
||||
# With wildcard pattern at the end
|
||||
'my-scope/my_container-with-wildcard-end' | [ref(:protection_rule_with_wildcard_end)]
|
||||
'my-scope/my_container-with-wildcard-end:1234567890' | [ref(:protection_rule_with_wildcard_end)]
|
||||
'prefix-my-scope/my_container-with-wildcard-end' | []
|
||||
'prefix-my-scope/my_container-with-wildcard-end:1234567890' | []
|
||||
|
||||
# With wildcard pattern in the middle
|
||||
'my-scope/my_container-with-wildcard-middle' | [ref(:protection_rule_with_wildcard_middle)]
|
||||
'my-scope/any-my_container-with-wildcard-middle' | [ref(:protection_rule_with_wildcard_middle)]
|
||||
'my-scope/any-my_container-my_container-wildcard-middle-any' | []
|
||||
|
||||
# With double wildcard pattern
|
||||
'my-scope/my_container-with-wildcard-double' | [ref(:protection_rule_with_wildcard_double)]
|
||||
'prefix-my-scope/any-my_container-with-wildcard-double-any' | [ref(:protection_rule_with_wildcard_double)]
|
||||
'****my-scope/****my_container-with-wildcard-double****' | [ref(:protection_rule_with_wildcard_double)]
|
||||
'prefix-@other-scope/any-my_container-with-wildcard-double-any' | []
|
||||
|
||||
# With underscore
|
||||
'my-scope/my_container-with_____underscore' | [ref(:protection_rule_with_underscore)]
|
||||
'my-scope/my_container-with_any_underscore' | []
|
||||
|
||||
'my-scope/my_container-with-regex-chars.+' | [ref(:protection_rule_with_regex_chars)]
|
||||
'my-scope/my_container-with-regex-chars.' | []
|
||||
'my-scope/my_container-with-regex-chars' | []
|
||||
'my-scope/my_container-with-regex-chars-any' | []
|
||||
|
||||
# Special cases
|
||||
nil | []
|
||||
'' | []
|
||||
'any_container' | []
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { is_expected.to match_array(expected_container_registry_protection_rules) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple matching container registry protection rules' do
|
||||
let!(:container_registry_protection_rule_second_match) do
|
||||
create(:container_registry_protection_rule, repository_path_pattern: "#{repository_path}*")
|
||||
end
|
||||
|
||||
it {
|
||||
is_expected.to contain_exactly(container_registry_protection_rule_second_match,
|
||||
container_registry_protection_rule)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe '.for_push_exists?' do
|
||||
subject do
|
||||
project
|
||||
.container_registry_protection_rules
|
||||
.for_push_exists?(
|
||||
access_level: access_level,
|
||||
repository_path: repository_path
|
||||
)
|
||||
end
|
||||
|
||||
context 'when the repository path matches multiple protection rules' do
|
||||
# The abbreviation `crpr` stands for container registry protection rule
|
||||
let_it_be(:project_with_crpr) { create(:project) }
|
||||
let_it_be(:project_without_crpr) { create(:project) }
|
||||
|
||||
let_it_be(:protection_rule_for_developer) do
|
||||
create(:container_registry_protection_rule,
|
||||
repository_path_pattern: 'my-scope/my-container-stage*',
|
||||
project: project_with_crpr,
|
||||
push_protected_up_to_access_level: :developer
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:protection_rule_for_maintainer) do
|
||||
create(:container_registry_protection_rule,
|
||||
repository_path_pattern: 'my-scope/my-container-prod*',
|
||||
project: project_with_crpr,
|
||||
push_protected_up_to_access_level: :maintainer
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:protection_rule_for_owner) do
|
||||
create(:container_registry_protection_rule,
|
||||
repository_path_pattern: 'my-scope/my-container-release*',
|
||||
project: project_with_crpr,
|
||||
push_protected_up_to_access_level: :owner
|
||||
)
|
||||
end
|
||||
|
||||
let_it_be(:protection_rule_overlapping_for_developer) do
|
||||
create(:container_registry_protection_rule,
|
||||
repository_path_pattern: 'my-scope/my-container-*',
|
||||
project: project_with_crpr,
|
||||
push_protected_up_to_access_level: :developer
|
||||
)
|
||||
end
|
||||
|
||||
where(:project, :access_level, :repository_path, :push_protected) do
|
||||
ref(:project_with_crpr) | Gitlab::Access::REPORTER | 'my-scope/my-container-stage-sha-1234' | true
|
||||
ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | 'my-scope/my-container-stage-sha-1234' | true
|
||||
ref(:project_with_crpr) | Gitlab::Access::MAINTAINER | 'my-scope/my-container-stage-sha-1234' | false
|
||||
ref(:project_with_crpr) | Gitlab::Access::MAINTAINER | 'my-scope/my-container-stage-sha-1234' | false
|
||||
ref(:project_with_crpr) | Gitlab::Access::OWNER | 'my-scope/my-container-stage-sha-1234' | false
|
||||
ref(:project_with_crpr) | Gitlab::Access::ADMIN | 'my-scope/my-container-stage-sha-1234' | false
|
||||
|
||||
ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | 'my-scope/my-container-prod-sha-1234' | true
|
||||
ref(:project_with_crpr) | Gitlab::Access::MAINTAINER | 'my-scope/my-container-prod-sha-1234' | true
|
||||
ref(:project_with_crpr) | Gitlab::Access::OWNER | 'my-scope/my-container-prod-sha-1234' | false
|
||||
ref(:project_with_crpr) | Gitlab::Access::ADMIN | 'my-scope/my-container-prod-sha-1234' | false
|
||||
|
||||
ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | 'my-scope/my-container-release-v1' | true
|
||||
ref(:project_with_crpr) | Gitlab::Access::OWNER | 'my-scope/my-container-release-v1' | true
|
||||
ref(:project_with_crpr) | Gitlab::Access::ADMIN | 'my-scope/my-container-release-v1' | false
|
||||
|
||||
ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | 'my-scope/my-container-any-suffix' | true
|
||||
ref(:project_with_crpr) | Gitlab::Access::MAINTAINER | 'my-scope/my-container-any-suffix' | false
|
||||
ref(:project_with_crpr) | Gitlab::Access::OWNER | 'my-scope/my-container-any-suffix' | false
|
||||
|
||||
# For non-matching repository_path
|
||||
ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | 'my-scope/non-matching-container' | false
|
||||
|
||||
# For no access level
|
||||
ref(:project_with_crpr) | Gitlab::Access::NO_ACCESS | 'my-scope/my-container-prod-sha-1234' | true
|
||||
|
||||
# Edge cases
|
||||
ref(:project_with_crpr) | 0 | '' | false
|
||||
ref(:project_with_crpr) | nil | nil | false
|
||||
ref(:project_with_crpr) | Gitlab::Access::DEVELOPER | nil | false
|
||||
ref(:project_with_crpr) | nil | 'my-scope/non-matching-container' | false
|
||||
|
||||
# For projects that have no container registry protection rules
|
||||
ref(:project_without_crpr) | Gitlab::Access::DEVELOPER | 'my-scope/my-container-prod-sha-1234' | false
|
||||
ref(:project_without_crpr) | Gitlab::Access::MAINTAINER | 'my-scope/my-container-prod-sha-1234' | false
|
||||
ref(:project_without_crpr) | Gitlab::Access::OWNER | 'my-scope/my-container-prod-sha-1234' | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { is_expected.to eq push_protected }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ RSpec.describe Organizations::Organization, type: :model, feature_category: :cel
|
|||
it { is_expected.to delegate_method(:description).to(:organization_detail) }
|
||||
it { is_expected.to delegate_method(:avatar).to(:organization_detail) }
|
||||
it { is_expected.to delegate_method(:avatar_url).to(:organization_detail) }
|
||||
it { is_expected.to delegate_method(:remove_avatar!).to(:organization_detail) }
|
||||
end
|
||||
|
||||
describe 'nested attributes' do
|
||||
|
|
|
|||
|
|
@ -115,8 +115,8 @@ RSpec.describe 'get board lists', feature_category: :team_planning do
|
|||
let(:issue_params) { { filters: { or: { assignee_usernames: [user.username, another_user.username] } } } }
|
||||
|
||||
it 'returns correctly filtered issues' do
|
||||
issue1.assignee_ids = user.id
|
||||
issue2.assignee_ids = another_user.id
|
||||
IssueAssignee.create!(issue_id: issue1.id, user_id: user.id)
|
||||
IssueAssignee.create!(issue_id: issue2.id, user_id: another_user.id)
|
||||
|
||||
subject
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r
|
|||
shared_context 'using job token' do
|
||||
before do
|
||||
stub_exclusive_lease
|
||||
stub_feature_flags(ci_job_token_scope: true)
|
||||
end
|
||||
|
||||
subject { public_send(method, api(url), params: params.merge({ job_token: job.token })) }
|
||||
|
|
@ -74,29 +73,15 @@ RSpec.describe API::ProjectContainerRepositories, feature_category: :container_r
|
|||
shared_context 'using job token from another project' do
|
||||
before do
|
||||
stub_exclusive_lease
|
||||
stub_feature_flags(ci_job_token_scope: true)
|
||||
end
|
||||
|
||||
subject { public_send(method, api(url), params: { job_token: job2.token }) }
|
||||
end
|
||||
|
||||
shared_context 'using job token while ci_job_token_scope feature flag is disabled' do
|
||||
before do
|
||||
stub_exclusive_lease
|
||||
stub_feature_flags(ci_job_token_scope: false)
|
||||
end
|
||||
|
||||
subject { public_send(method, api(url), params: params.merge({ job_token: job.token })) }
|
||||
end
|
||||
|
||||
shared_examples 'rejected job token scopes' do
|
||||
include_context 'using job token from another project' do
|
||||
it_behaves_like 'rejected container repository access', :maintainer, :forbidden
|
||||
end
|
||||
|
||||
include_context 'using job token while ci_job_token_scope feature flag is disabled' do
|
||||
it_behaves_like 'rejected container repository access', :maintainer, :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /projects/:id/registry/repositories' do
|
||||
|
|
|
|||
|
|
@ -79,6 +79,19 @@ RSpec.describe 'Uploads', 'routing' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'for organizations' do
|
||||
it 'allows fetching organization avatars' do
|
||||
expect(get('/uploads/-/system/organizations/organization_detail/avatar/1/test.jpg')).to route_to(
|
||||
controller: 'uploads',
|
||||
action: 'show',
|
||||
model: 'organizations/organization_detail',
|
||||
id: '1',
|
||||
filename: 'test.jpg',
|
||||
mounted_as: 'avatar'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not allow creating uploads for other models' do
|
||||
unroutable_models = UploadsController::MODEL_CLASSES.keys.compact - %w[personal_snippet user abuse_report]
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,14 @@ RSpec.describe Organizations::UpdateService, feature_category: :cell do
|
|||
it_behaves_like 'updating an organization'
|
||||
end
|
||||
|
||||
context 'when avatar is set to nil' do
|
||||
let_it_be(:organization_detail) { create(:organization_detail, organization: organization) }
|
||||
let(:extra_params) { { avatar: nil } }
|
||||
let(:description) { organization_detail.description }
|
||||
|
||||
it_behaves_like 'updating an organization'
|
||||
end
|
||||
|
||||
include_examples 'updating an organization'
|
||||
|
||||
context 'when the organization is not updated' do
|
||||
|
|
|
|||
Loading…
Reference in New Issue