Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f4ea1f8998
commit
cdda3d117c
|
@ -59,7 +59,7 @@ workflow:
|
|||
|
||||
variables:
|
||||
PG_VERSION: "12"
|
||||
DEFAULT_CI_IMAGE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-2.7.patched-golang-1.17-node-16.14-postgresql-${PG_VERSION}:git-2.36-lfs-2.9-chrome-101-yarn-1.22-graphicsmagick-1.3.36"
|
||||
DEFAULT_CI_IMAGE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-2.7.patched-golang-1.17-node-16.14-postgresql-${PG_VERSION}:git-2.36-lfs-2.9-chrome-${CHROME_VERSION}-yarn-1.22-graphicsmagick-1.3.36"
|
||||
RAILS_ENV: "test"
|
||||
NODE_ENV: "test"
|
||||
BUNDLE_WITHOUT: "production:development"
|
||||
|
@ -73,6 +73,8 @@ variables:
|
|||
GIT_SUBMODULE_STRATEGY: "none"
|
||||
GET_SOURCES_ATTEMPTS: "3"
|
||||
DEBIAN_VERSION: "bullseye"
|
||||
CHROME_VERSION: "101"
|
||||
DOCKER_VERSION: "20.10.14"
|
||||
|
||||
TMP_TEST_FOLDER: "${CI_PROJECT_DIR}/tmp/tests"
|
||||
GITLAB_WORKHORSE_FOLDER: "gitlab-workhorse"
|
||||
|
@ -89,7 +91,6 @@ variables:
|
|||
|
||||
ES_JAVA_OPTS: "-Xms256m -Xmx256m"
|
||||
ELASTIC_URL: "http://elastic:changeme@elasticsearch:9200"
|
||||
DOCKER_VERSION: "20.10.1"
|
||||
CACHE_CLASSES: "true"
|
||||
CHECK_PRECOMPILED_ASSETS: "true"
|
||||
FF_USE_FASTZIP: "true"
|
||||
|
|
|
@ -29,7 +29,15 @@ build-qa-image:
|
|||
- !reference [.base-image-build, script]
|
||||
- echo $QA_IMAGE
|
||||
- echo $QA_IMAGE_BRANCH
|
||||
- /kaniko/executor --context=${CI_PROJECT_DIR} --dockerfile=${CI_PROJECT_DIR}/qa/Dockerfile --destination=${QA_IMAGE} --destination=${QA_IMAGE_BRANCH} --cache=true
|
||||
- |
|
||||
/kaniko/executor \
|
||||
--context=${CI_PROJECT_DIR} \
|
||||
--dockerfile=${CI_PROJECT_DIR}/qa/Dockerfile \
|
||||
--destination=${QA_IMAGE} \
|
||||
--destination=${QA_IMAGE_BRANCH} \
|
||||
--build-arg=CHROME_VERSION=${CHROME_VERSION} \
|
||||
--build-arg=DOCKER_VERSION=${DOCKER_VERSION} \
|
||||
--cache=true
|
||||
|
||||
# This image is used by:
|
||||
# - The `CNG` pipelines (via the `review-build-cng` job): https://gitlab.com/gitlab-org/build/CNG/-/blob/cfc67136d711e1c8c409bf8e57427a644393da2f/.gitlab-ci.yml#L335
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.qa-job-base:
|
||||
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-chrome-99
|
||||
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-chrome-${CHROME_VERSION}
|
||||
extends:
|
||||
- .default-retry
|
||||
- .qa-cache
|
||||
|
@ -12,7 +12,7 @@
|
|||
before_script:
|
||||
- !reference [.default-before_script, before_script]
|
||||
- cd qa/
|
||||
- bundle_install_script
|
||||
- bundle install
|
||||
|
||||
qa:internal:
|
||||
extends:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
include:
|
||||
- project: gitlab-org/quality/pipeline-common
|
||||
ref: 0.6.0
|
||||
ref: 0.13.0
|
||||
file:
|
||||
- /ci/allure-report.yml
|
||||
- /ci/knapsack-report.yml
|
||||
|
@ -28,7 +28,7 @@ include:
|
|||
- .qa-cache
|
||||
- .test_variables
|
||||
- .bundler_variables
|
||||
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-lfs-2.9-chrome-99-docker-20.10.14-gcloud-383-kubectl-1.23
|
||||
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-lfs-2.9-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-gcloud-383-kubectl-1.23
|
||||
stage: qa
|
||||
needs:
|
||||
- review-deploy
|
||||
|
@ -81,7 +81,7 @@ include:
|
|||
|
||||
# Store knapsack report as artifact so the same report is reused across all jobs
|
||||
download-knapsack-report:
|
||||
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-chrome-99
|
||||
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-chrome-${CHROME_VERSION}
|
||||
extends:
|
||||
- .qa-cache
|
||||
- .bundler_variables
|
||||
|
|
|
@ -42,6 +42,20 @@ const bodyTrClass =
|
|||
export default {
|
||||
i18n,
|
||||
typeSet,
|
||||
modal: {
|
||||
actionPrimary: {
|
||||
text: i18n.deleteIntegration,
|
||||
attributes: {
|
||||
variant: 'danger',
|
||||
},
|
||||
},
|
||||
actionSecondary: {
|
||||
text: __('Cancel'),
|
||||
attributes: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
GlButtonGroup,
|
||||
GlButton,
|
||||
|
@ -204,8 +218,8 @@ export default {
|
|||
<gl-modal
|
||||
modal-id="deleteIntegration"
|
||||
:title="$options.i18n.deleteIntegration"
|
||||
:ok-title="$options.i18n.deleteIntegration"
|
||||
ok-variant="danger"
|
||||
:action-primary="$options.modal.actionPrimary"
|
||||
:action-secondary="$options.modal.actionSecondary"
|
||||
@ok="deleteIntegration"
|
||||
>
|
||||
<gl-sprintf
|
||||
|
|
|
@ -374,6 +374,33 @@
|
|||
"type": "array",
|
||||
"description": "Command or script that should be executed as the container's entrypoint. It will be translated to Docker's --entrypoint option while creating the container. The syntax is similar to Dockerfile's ENTRYPOINT directive, where each shell token is a separate string in the array.",
|
||||
"minItems": 1
|
||||
},
|
||||
"pull_policy": {
|
||||
"markdownDescription": "Specifies how to pull the image in Runner. It can be one of `always`, `never` or `if-not-present`. The default value is `always`. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#imagepull_policy).",
|
||||
"default": "always",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"always",
|
||||
"never",
|
||||
"if-not-present"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"always",
|
||||
"never",
|
||||
"if-not-present"
|
||||
]
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
|
||||
const GROUP_SUBGROUPS_PATH = '/-/autocomplete/group_subgroups.json';
|
||||
|
||||
const buildUrl = (urlRoot, url) => {
|
||||
return joinPaths(urlRoot, url);
|
||||
};
|
||||
|
||||
export const getSubGroups = () => {
|
||||
return axios.get(buildUrl(gon.relative_url_root || '', GROUP_SUBGROUPS_PATH), {
|
||||
params: {
|
||||
group_id: gon.current_group_id,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,194 @@
|
|||
<script>
|
||||
import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
|
||||
import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } from 'lodash';
|
||||
import createFlash from '~/flash';
|
||||
import { __, s__, n__ } from '~/locale';
|
||||
import { getSubGroups } from '../api/access_dropdown_api';
|
||||
import { LEVEL_TYPES } from '../constants';
|
||||
|
||||
export const i18n = {
|
||||
selectUsers: s__('ProtectedEnvironment|Select groups'),
|
||||
groupsSectionHeader: s__('AccessDropdown|Groups'),
|
||||
};
|
||||
|
||||
export default {
|
||||
i18n,
|
||||
components: {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownSectionHeader,
|
||||
GlSearchBoxByType,
|
||||
},
|
||||
props: {
|
||||
hasLicense: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: i18n.selectUsers,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
preselectedItems: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
initialLoading: false,
|
||||
query: '',
|
||||
groups: [],
|
||||
selected: {
|
||||
[LEVEL_TYPES.GROUP]: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
preselected() {
|
||||
return groupBy(this.preselectedItems, 'type');
|
||||
},
|
||||
toggleLabel() {
|
||||
const counts = Object.fromEntries(
|
||||
Object.entries(this.selected).map(([key, value]) => [key, value.length]),
|
||||
);
|
||||
|
||||
const labelPieces = [];
|
||||
|
||||
if (counts[LEVEL_TYPES.GROUP] > 0) {
|
||||
labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP]));
|
||||
}
|
||||
|
||||
return labelPieces.join(', ') || this.label;
|
||||
},
|
||||
toggleClass() {
|
||||
return this.toggleLabel === this.label ? 'gl-text-gray-500!' : '';
|
||||
},
|
||||
selection() {
|
||||
return [...this.getDataForSave(LEVEL_TYPES.GROUP, 'group_id')];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
query: debounce(function debouncedSearch() {
|
||||
return this.getData();
|
||||
}, 500),
|
||||
},
|
||||
created() {
|
||||
this.getData({ initial: true });
|
||||
},
|
||||
methods: {
|
||||
focusInput() {
|
||||
this.$refs.search.focusInput();
|
||||
},
|
||||
getData({ initial = false } = {}) {
|
||||
this.initialLoading = initial;
|
||||
this.loading = true;
|
||||
|
||||
if (this.hasLicense) {
|
||||
Promise.all([this.groups.length ? Promise.resolve({ data: this.groups }) : getSubGroups()])
|
||||
.then(([groupsResponse]) => {
|
||||
this.consolidateData(groupsResponse.data);
|
||||
this.setSelected({ initial });
|
||||
})
|
||||
.catch(() => createFlash({ message: __('Failed to load groups.') }))
|
||||
.finally(() => {
|
||||
this.initialLoading = false;
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
consolidateData(groupsResponse = []) {
|
||||
if (this.hasLicense) {
|
||||
this.groups = groupsResponse.map((group) => ({ ...group, type: LEVEL_TYPES.GROUP }));
|
||||
}
|
||||
},
|
||||
setSelected({ initial } = {}) {
|
||||
if (initial) {
|
||||
const selectedGroups = intersectionWith(
|
||||
this.groups,
|
||||
this.preselectedItems,
|
||||
(group, selected) => {
|
||||
return selected.type === LEVEL_TYPES.GROUP && group.id === selected.group_id;
|
||||
},
|
||||
);
|
||||
this.selected[LEVEL_TYPES.GROUP] = selectedGroups;
|
||||
}
|
||||
},
|
||||
getDataForSave(accessType, key) {
|
||||
const selected = this.selected[accessType].map(({ id }) => ({ [key]: id }));
|
||||
const preselected = this.preselected[accessType];
|
||||
const added = differenceBy(selected, preselected, key);
|
||||
const preserved = intersectionBy(preselected, selected, key).map(({ id, [key]: keyId }) => ({
|
||||
id,
|
||||
[key]: keyId,
|
||||
}));
|
||||
const removed = differenceBy(preselected, selected, key).map(({ id, [key]: keyId }) => ({
|
||||
id,
|
||||
[key]: keyId,
|
||||
_destroy: true,
|
||||
}));
|
||||
return [...added, ...removed, ...preserved];
|
||||
},
|
||||
onItemClick(item) {
|
||||
this.toggleSelection(this.selected[item.type], item);
|
||||
this.emitUpdate();
|
||||
},
|
||||
toggleSelection(arr, item) {
|
||||
const itemIndex = arr.findIndex(({ id }) => id === item.id);
|
||||
if (itemIndex > -1) {
|
||||
arr.splice(itemIndex, 1);
|
||||
} else arr.push(item);
|
||||
},
|
||||
isSelected(item) {
|
||||
return this.selected[item.type].some((selected) => selected.id === item.id);
|
||||
},
|
||||
emitUpdate() {
|
||||
this.$emit('select', this.selection);
|
||||
},
|
||||
onHide() {
|
||||
this.$emit('hidden', this.selection);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-dropdown
|
||||
:disabled="disabled || initialLoading"
|
||||
:text="toggleLabel"
|
||||
class="gl-min-w-20"
|
||||
:toggle-class="toggleClass"
|
||||
aria-labelledby="allowed-users-label"
|
||||
@shown="focusInput"
|
||||
@hidden="onHide"
|
||||
>
|
||||
<template #header>
|
||||
<gl-search-box-by-type ref="search" v-model.trim="query" :is-loading="loading" />
|
||||
</template>
|
||||
<template v-if="groups.length">
|
||||
<gl-dropdown-section-header>{{
|
||||
$options.i18n.groupsSectionHeader
|
||||
}}</gl-dropdown-section-header>
|
||||
<gl-dropdown-item
|
||||
v-for="group in groups"
|
||||
:key="`${group.id}${group.name}`"
|
||||
fingerprint
|
||||
data-testid="group-dropdown-item"
|
||||
:avatar-url="group.avatar_url"
|
||||
is-check-item
|
||||
:is-checked="isSelected(group)"
|
||||
@click.native.capture.stop="onItemClick(group)"
|
||||
>
|
||||
{{ group.name }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
</gl-dropdown>
|
||||
</template>
|
|
@ -0,0 +1,3 @@
|
|||
export const LEVEL_TYPES = {
|
||||
GROUP: 'group',
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
import * as Sentry from '@sentry/browser';
|
||||
import Vue from 'vue';
|
||||
import AccessDropdown from './components/access_dropdown.vue';
|
||||
|
||||
export const initAccessDropdown = (el) => {
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { label, disabled, preselectedItems } = el.dataset;
|
||||
let preselected = [];
|
||||
try {
|
||||
preselected = JSON.parse(preselectedItems);
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
}
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
render(createElement) {
|
||||
const vm = this;
|
||||
return createElement(AccessDropdown, {
|
||||
props: {
|
||||
preselectedItems: preselected,
|
||||
label,
|
||||
disabled,
|
||||
},
|
||||
on: {
|
||||
select(selected) {
|
||||
vm.$emit('select', selected);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -14,6 +14,7 @@ import ExperimentTracking from '~/experimentation/experiment_tracking';
|
|||
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
|
||||
import { getParameterValues } from '~/lib/utils/url_utility';
|
||||
import {
|
||||
CLOSE_TO_LIMIT_COUNT,
|
||||
USERS_FILTER_ALL,
|
||||
INVITE_MEMBERS_FOR_TASK,
|
||||
MEMBER_MODAL_LABELS,
|
||||
|
@ -151,6 +152,16 @@ export default {
|
|||
isOnLearnGitlab() {
|
||||
return this.source === LEARN_GITLAB;
|
||||
},
|
||||
closeToLimit() {
|
||||
if (this.usersLimitDataset.freeUsersLimit && this.usersLimitDataset.membersCount) {
|
||||
return (
|
||||
this.usersLimitDataset.membersCount >=
|
||||
this.usersLimitDataset.freeUsersLimit - CLOSE_TO_LIMIT_COUNT
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
reachedLimit() {
|
||||
if (this.usersLimitDataset.freeUsersLimit && this.usersLimitDataset.membersCount) {
|
||||
return this.usersLimitDataset.membersCount >= this.usersLimitDataset.freeUsersLimit;
|
||||
|
@ -297,6 +308,7 @@ export default {
|
|||
:is-loading="isLoading"
|
||||
:new-users-to-invite="newUsersToInvite"
|
||||
:root-group-id="rootId"
|
||||
:close-to-limit="closeToLimit"
|
||||
:reached-limit="reachedLimit"
|
||||
:users-limit-dataset="usersLimitDataset"
|
||||
@reset="resetFields"
|
||||
|
@ -314,6 +326,7 @@ export default {
|
|||
|
||||
<template #user-limit-notification>
|
||||
<user-limit-notification
|
||||
:close-to-limit="closeToLimit"
|
||||
:reached-limit="reachedLimit"
|
||||
:users-limit-dataset="usersLimitDataset"
|
||||
/>
|
||||
|
|
|
@ -131,6 +131,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
closeToLimit: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
reachedLimit: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -183,6 +188,17 @@ export default {
|
|||
actionCancel() {
|
||||
if (this.reachedLimit && this.usersLimitDataset.userNamespace) return undefined;
|
||||
|
||||
if (this.closeToLimit && this.usersLimitDataset.userNamespace) {
|
||||
return {
|
||||
text: INVITE_BUTTON_TEXT_DISABLED,
|
||||
attributes: {
|
||||
href: this.usersLimitDataset.membersPath,
|
||||
category: 'secondary',
|
||||
variant: 'confirm',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
text: this.reachedLimit ? CANCEL_BUTTON_TEXT_DISABLED : this.cancelButtonText,
|
||||
...(this.reachedLimit && { attributes: { href: this.usersLimitDataset.purchasePath } }),
|
||||
|
|
|
@ -8,15 +8,20 @@ import {
|
|||
REACHED_LIMIT_MESSAGE,
|
||||
REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
|
||||
CLOSE_TO_LIMIT_MESSAGE,
|
||||
CLOSE_TO_LIMIT_MESSAGE_PERSONAL_NAMESPACE,
|
||||
DANGER_ALERT_TITLE_PERSONAL_NAMESPACE,
|
||||
WARNING_ALERT_TITLE_PERSONAL_NAMESPACE,
|
||||
} from '../constants';
|
||||
|
||||
const CLOSE_TO_LIMIT_COUNT = 2;
|
||||
|
||||
export default {
|
||||
name: 'UserLimitNotification',
|
||||
components: { GlAlert, GlSprintf, GlLink },
|
||||
inject: ['name'],
|
||||
props: {
|
||||
closeToLimit: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
reachedLimit: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
@ -40,14 +45,14 @@ export default {
|
|||
purchasePath() {
|
||||
return this.usersLimitDataset.purchasePath;
|
||||
},
|
||||
closeToLimit() {
|
||||
if (this.freeUsersLimit && this.membersCount) {
|
||||
return this.membersCount >= this.freeUsersLimit - CLOSE_TO_LIMIT_COUNT;
|
||||
warningAlertTitle() {
|
||||
if (this.usersLimitDataset.userNamespace) {
|
||||
return sprintf(WARNING_ALERT_TITLE_PERSONAL_NAMESPACE, {
|
||||
count: this.freeUsersLimit - this.membersCount,
|
||||
members: this.pluralMembers(this.freeUsersLimit - this.membersCount),
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
warningAlertTitle() {
|
||||
return sprintf(WARNING_ALERT_TITLE, {
|
||||
count: this.freeUsersLimit - this.membersCount,
|
||||
members: this.pluralMembers(this.freeUsersLimit - this.membersCount),
|
||||
|
@ -55,6 +60,13 @@ export default {
|
|||
});
|
||||
},
|
||||
dangerAlertTitle() {
|
||||
if (this.usersLimitDataset.userNamespace) {
|
||||
return sprintf(DANGER_ALERT_TITLE_PERSONAL_NAMESPACE, {
|
||||
count: this.freeUsersLimit,
|
||||
members: this.pluralMembers(this.freeUsersLimit),
|
||||
});
|
||||
}
|
||||
|
||||
return sprintf(DANGER_ALERT_TITLE, {
|
||||
count: this.freeUsersLimit,
|
||||
members: this.pluralMembers(this.freeUsersLimit),
|
||||
|
@ -79,6 +91,10 @@ export default {
|
|||
return this.reachedLimitMessage;
|
||||
}
|
||||
|
||||
if (this.usersLimitDataset.userNamespace) {
|
||||
return this.$options.i18n.closeToLimitMessagePersonalNamespace;
|
||||
}
|
||||
|
||||
return this.$options.i18n.closeToLimitMessage;
|
||||
},
|
||||
},
|
||||
|
@ -91,6 +107,7 @@ export default {
|
|||
reachedLimitMessage: REACHED_LIMIT_MESSAGE,
|
||||
reachedLimitUpgradeSuggestionMessage: REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
|
||||
closeToLimitMessage: CLOSE_TO_LIMIT_MESSAGE,
|
||||
closeToLimitMessagePersonalNamespace: CLOSE_TO_LIMIT_MESSAGE_PERSONAL_NAMESPACE,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { s__ } from '~/locale';
|
||||
|
||||
export const CLOSE_TO_LIMIT_COUNT = 2;
|
||||
export const SEARCH_DELAY = 200;
|
||||
|
||||
export const INVITE_MEMBERS_FOR_TASK = {
|
||||
minimum_access_level: 30,
|
||||
name: 'invite_members_for_task',
|
||||
|
@ -132,10 +132,17 @@ export const ON_SUBMIT_TRACK_LABEL = 'manage_members_clicked';
|
|||
export const WARNING_ALERT_TITLE = s__(
|
||||
'InviteMembersModal|You only have space for %{count} more %{members} in %{name}',
|
||||
);
|
||||
export const WARNING_ALERT_TITLE_PERSONAL_NAMESPACE = s__(
|
||||
'InviteMembersModal|You only have space for %{count} more %{members} in your personal projects',
|
||||
);
|
||||
export const DANGER_ALERT_TITLE = s__(
|
||||
"InviteMembersModal|You've reached your %{count} %{members} limit for %{name}",
|
||||
);
|
||||
|
||||
export const DANGER_ALERT_TITLE_PERSONAL_NAMESPACE = s__(
|
||||
"InviteMembersModal|You've reached your %{count} %{members} limit for your personal projects",
|
||||
);
|
||||
|
||||
export const REACHED_LIMIT_MESSAGE = s__(
|
||||
'InviteMembersModal|You cannot add more members, but you can remove members who no longer need access.',
|
||||
);
|
||||
|
@ -149,3 +156,6 @@ export const REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE = REACHED_LIMIT_MESSAGE.co
|
|||
export const CLOSE_TO_LIMIT_MESSAGE = s__(
|
||||
'InviteMembersModal|To get more members an owner of this namespace can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.',
|
||||
);
|
||||
export const CLOSE_TO_LIMIT_MESSAGE_PERSONAL_NAMESPACE = s__(
|
||||
'InviteMembersModal|To make more space, you can remove members who no longer need access.',
|
||||
);
|
||||
|
|
|
@ -10,7 +10,7 @@ const PERSISTENT_USER_CALLOUTS = [
|
|||
'.js-new-user-signups-cap-reached',
|
||||
'.js-eoa-bronze-plan-banner',
|
||||
'.js-security-newsletter-callout',
|
||||
'.js-approaching-seats-count-threshold',
|
||||
'.js-approaching-seat-count-threshold',
|
||||
'.js-storage-enforcement-banner',
|
||||
'.js-user-over-limit-free-plan-alert',
|
||||
'.js-minute-limit-banner',
|
||||
|
|
|
@ -103,9 +103,9 @@ export default {
|
|||
},
|
||||
expandedIcon() {
|
||||
if (this.isUpstream) {
|
||||
return this.expanded ? 'angle-right' : 'angle-left';
|
||||
return this.expanded ? 'chevron-lg-right' : 'chevron-lg-left';
|
||||
}
|
||||
return this.expanded ? 'angle-left' : 'angle-right';
|
||||
return this.expanded ? 'chevron-lg-left' : 'chevron-lg-right';
|
||||
},
|
||||
expandBtnText() {
|
||||
return this.expanded ? __('Collapse jobs') : __('Expand jobs');
|
||||
|
|
|
@ -126,6 +126,7 @@ export default {
|
|||
}) => {
|
||||
toast(__('Marked as ready. Merging is now allowed.'));
|
||||
$('.merge-request .detail-page-description .title').text(title);
|
||||
eventHub.$emit('MRWidgetUpdateRequested');
|
||||
},
|
||||
)
|
||||
.catch(() =>
|
||||
|
|
|
@ -27,7 +27,8 @@
|
|||
}
|
||||
|
||||
.toggle-sidebar-button {
|
||||
width: $contextual-sidebar-collapsed-width;
|
||||
width: #{$contextual-sidebar-collapsed-width - 1px};
|
||||
padding: 0 21px;
|
||||
|
||||
.collapse-text {
|
||||
display: none;
|
||||
|
@ -81,7 +82,7 @@
|
|||
@include gl-px-0;
|
||||
@include gl-pb-2;
|
||||
@include gl-pt-0;
|
||||
background-color: $gray-10;
|
||||
@include gl-bg-gray-10;
|
||||
box-shadow: 0 $gl-spacing-scale-2 $gl-spacing-scale-5 $t-gray-a-24, 0 0 $gl-spacing-scale-1 $t-gray-a-24;
|
||||
border-style: none;
|
||||
border-radius: $border-radius-default;
|
||||
|
@ -128,7 +129,7 @@
|
|||
|
||||
@include gl-p-2;
|
||||
@include gl-mb-2;
|
||||
@include gl-mt-0;
|
||||
@include gl-mt-1;
|
||||
|
||||
.avatar-container {
|
||||
@include gl-font-weight-normal;
|
||||
|
@ -246,7 +247,8 @@
|
|||
z-index: 600;
|
||||
width: $contextual-sidebar-width;
|
||||
top: $header-height;
|
||||
background-color: $gray-50;
|
||||
@include gl-bg-gray-10;
|
||||
border-right: 1px solid $gray-50;
|
||||
transform: translate3d(0, 0, 0);
|
||||
|
||||
&.sidebar-collapsed-desktop {
|
||||
|
@ -352,7 +354,6 @@
|
|||
}
|
||||
|
||||
.sidebar-top-level-items {
|
||||
@include gl-mt-2;
|
||||
margin-bottom: 60px;
|
||||
|
||||
.context-header a {
|
||||
|
@ -410,11 +411,10 @@
|
|||
.toggle-sidebar-button,
|
||||
.close-nav-button {
|
||||
@include side-panel-toggle;
|
||||
background-color: $gray-50;
|
||||
border-top: 1px solid $border-color;
|
||||
@include gl-bg-gray-10;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: $contextual-sidebar-width;
|
||||
width: #{$contextual-sidebar-width - 1px};
|
||||
|
||||
.collapse-text,
|
||||
.icon-chevron-double-lg-left,
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
margin: 4px 2px 4px -12px;
|
||||
margin: 4px 2px 4px -8px;
|
||||
border-radius: $border-radius-default;
|
||||
|
||||
&:active,
|
||||
|
|
|
@ -592,6 +592,14 @@
|
|||
@include email-code-block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Links
|
||||
*
|
||||
*/
|
||||
a:focus-visible {
|
||||
@include gl-focus($outline: true, $outline-offset: $outline-width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Headers
|
||||
*
|
||||
|
|
|
@ -9,7 +9,7 @@ $sidebar-transition-duration: 0.3s;
|
|||
$sidebar-breakpoint: 1024px;
|
||||
$default-transition-duration: 0.15s;
|
||||
$contextual-sidebar-width: 256px;
|
||||
$contextual-sidebar-collapsed-width: 48px;
|
||||
$contextual-sidebar-collapsed-width: 56px;
|
||||
$toggle-sidebar-height: 48px;
|
||||
|
||||
/**
|
||||
|
@ -580,7 +580,7 @@ $sidebar-toggle-height: 60px;
|
|||
$sidebar-toggle-width: 40px;
|
||||
$sidebar-milestone-toggle-bottom-margin: 10px;
|
||||
$sidebar-avatar-size: 32px;
|
||||
$sidebar-top-item-lr-margin: 4px;
|
||||
$sidebar-top-item-lr-margin: 8px;
|
||||
$sidebar-top-item-tb-margin: 1px;
|
||||
|
||||
/*
|
||||
|
|
|
@ -5,14 +5,6 @@
|
|||
line-height: 34px;
|
||||
display: flex;
|
||||
|
||||
a {
|
||||
color: $gl-text-color;
|
||||
|
||||
&.link {
|
||||
color: $blue-600;
|
||||
}
|
||||
}
|
||||
|
||||
.author-link {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
@ -22,6 +14,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.detail-page-header a {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
.detail-page-header a.link,
|
||||
.detail-page-header .title a {
|
||||
color: $blue-600;
|
||||
}
|
||||
|
||||
.detail-page-header-body {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
|
@ -214,8 +214,7 @@ $tabs-holder-z-index: 250;
|
|||
}
|
||||
}
|
||||
|
||||
.merge-request-tabs-holder,
|
||||
.epic-tabs-holder {
|
||||
.merge-request-tabs-holder {
|
||||
top: $header-height;
|
||||
z-index: $tabs-holder-z-index;
|
||||
background-color: $body-bg;
|
||||
|
@ -248,14 +247,12 @@ $tabs-holder-z-index: 250;
|
|||
}
|
||||
|
||||
.with-performance-bar {
|
||||
.merge-request-tabs-holder,
|
||||
.epic-tabs-holder {
|
||||
.merge-request-tabs-holder {
|
||||
top: calc(#{$header-height} + #{$performance-bar-height});
|
||||
}
|
||||
}
|
||||
|
||||
.merge-request-tabs,
|
||||
.epic-tabs {
|
||||
.merge-request-tabs {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
margin-bottom: 0;
|
||||
|
@ -263,8 +260,7 @@ $tabs-holder-z-index: 250;
|
|||
}
|
||||
|
||||
.limit-container-width {
|
||||
.merge-request-tabs-container,
|
||||
.epic-tabs-container {
|
||||
.merge-request-tabs-container {
|
||||
max-width: $limited-layout-width;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
@ -277,8 +273,7 @@ $tabs-holder-z-index: 250;
|
|||
}
|
||||
}
|
||||
|
||||
.merge-request-tabs-container,
|
||||
.epic-tabs-container {
|
||||
.merge-request-tabs-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
|
@ -308,16 +303,14 @@ $tabs-holder-z-index: 250;
|
|||
|
||||
// Wrap MR tabs/buttons so you don't have to scroll on desktop
|
||||
@include media-breakpoint-down(md) {
|
||||
.merge-request-tabs-container,
|
||||
.epic-tabs-container {
|
||||
.merge-request-tabs-container {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
.right-sidebar-expanded {
|
||||
.merge-request-tabs-container,
|
||||
.epic-tabs-container {
|
||||
.merge-request-tabs-container {
|
||||
flex-direction: column-reverse;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
|
|
@ -780,7 +780,7 @@ input {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
margin: 4px 2px 4px -12px;
|
||||
margin: 4px 2px 4px -8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.navbar-gitlab .header-content .title a:active {
|
||||
|
@ -1014,7 +1014,7 @@ input {
|
|||
}
|
||||
@media (min-width: 768px) {
|
||||
.page-with-contextual-sidebar {
|
||||
padding-left: 48px;
|
||||
padding-left: 56px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
|
@ -1024,7 +1024,7 @@ input {
|
|||
}
|
||||
@media (min-width: 768px) {
|
||||
.page-with-icon-sidebar {
|
||||
padding-left: 48px;
|
||||
padding-left: 56px;
|
||||
}
|
||||
}
|
||||
.nav-sidebar {
|
||||
|
@ -1034,11 +1034,12 @@ input {
|
|||
z-index: 600;
|
||||
width: 256px;
|
||||
top: var(--header-height, 48px);
|
||||
background-color: #303030;
|
||||
background-color: #1f1f1f;
|
||||
border-right: 1px solid #303030;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
.nav-sidebar.sidebar-collapsed-desktop {
|
||||
width: 48px;
|
||||
width: 56px;
|
||||
}
|
||||
.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll {
|
||||
overflow-x: hidden;
|
||||
|
@ -1091,7 +1092,7 @@ input {
|
|||
border-radius: 0.25rem;
|
||||
width: auto;
|
||||
line-height: 1rem;
|
||||
margin: 1px 4px;
|
||||
margin: 1px 8px;
|
||||
}
|
||||
.nav-sidebar li.active > a {
|
||||
font-weight: 600;
|
||||
|
@ -1227,7 +1228,7 @@ input {
|
|||
}
|
||||
@media (min-width: 768px) and (max-width: 1199px) {
|
||||
.nav-sidebar:not(.sidebar-expanded-mobile) {
|
||||
width: 48px;
|
||||
width: 56px;
|
||||
}
|
||||
.nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll {
|
||||
overflow-x: hidden;
|
||||
|
@ -1262,7 +1263,7 @@ input {
|
|||
}
|
||||
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
|
||||
height: 60px;
|
||||
width: 48px;
|
||||
width: 56px;
|
||||
}
|
||||
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
|
||||
padding: 10px 4px;
|
||||
|
@ -1294,7 +1295,8 @@ input {
|
|||
margin-right: 0;
|
||||
}
|
||||
.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button {
|
||||
width: 48px;
|
||||
width: 55px;
|
||||
padding: 0 21px;
|
||||
}
|
||||
.nav-sidebar:not(.sidebar-expanded-mobile)
|
||||
.toggle-sidebar-button
|
||||
|
@ -1327,10 +1329,10 @@ input {
|
|||
border-radius: 0.25rem;
|
||||
width: auto;
|
||||
line-height: 1rem;
|
||||
margin: 1px 4px;
|
||||
margin: 1px 8px;
|
||||
padding: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-top: 0;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
|
||||
font-weight: 400;
|
||||
|
@ -1349,13 +1351,12 @@ input {
|
|||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.sidebar-top-level-items {
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.sidebar-top-level-items .context-header a {
|
||||
padding: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-top: 0;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
.sidebar-top-level-items .context-header a .avatar-container {
|
||||
font-weight: 400;
|
||||
|
@ -1401,11 +1402,10 @@ input {
|
|||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #303030;
|
||||
border-top: 1px solid #404040;
|
||||
background-color: #1f1f1f;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 256px;
|
||||
width: 255px;
|
||||
}
|
||||
.toggle-sidebar-button .collapse-text,
|
||||
.toggle-sidebar-button .icon-chevron-double-lg-left,
|
||||
|
@ -1419,7 +1419,7 @@ input {
|
|||
}
|
||||
.sidebar-collapsed-desktop .context-header {
|
||||
height: 60px;
|
||||
width: 48px;
|
||||
width: 56px;
|
||||
}
|
||||
.sidebar-collapsed-desktop .context-header a {
|
||||
padding: 10px 4px;
|
||||
|
@ -1451,7 +1451,8 @@ input {
|
|||
margin-right: 0;
|
||||
}
|
||||
.sidebar-collapsed-desktop .toggle-sidebar-button {
|
||||
width: 48px;
|
||||
width: 55px;
|
||||
padding: 0 21px;
|
||||
}
|
||||
.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text {
|
||||
display: none;
|
||||
|
@ -1787,6 +1788,11 @@ body.gl-dark {
|
|||
--svg-status-bg: #333;
|
||||
--nav-active-bg: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.nav-sidebar,
|
||||
.toggle-sidebar-button,
|
||||
.close-nav-button {
|
||||
background-color: #262626;
|
||||
}
|
||||
.nav-sidebar li a {
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
|
|
@ -765,7 +765,7 @@ input {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
margin: 4px 2px 4px -12px;
|
||||
margin: 4px 2px 4px -8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.navbar-gitlab .header-content .title a:active {
|
||||
|
@ -999,7 +999,7 @@ input {
|
|||
}
|
||||
@media (min-width: 768px) {
|
||||
.page-with-contextual-sidebar {
|
||||
padding-left: 48px;
|
||||
padding-left: 56px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
|
@ -1009,7 +1009,7 @@ input {
|
|||
}
|
||||
@media (min-width: 768px) {
|
||||
.page-with-icon-sidebar {
|
||||
padding-left: 48px;
|
||||
padding-left: 56px;
|
||||
}
|
||||
}
|
||||
.nav-sidebar {
|
||||
|
@ -1019,11 +1019,12 @@ input {
|
|||
z-index: 600;
|
||||
width: 256px;
|
||||
top: var(--header-height, 48px);
|
||||
background-color: #f0f0f0;
|
||||
background-color: #fafafa;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
.nav-sidebar.sidebar-collapsed-desktop {
|
||||
width: 48px;
|
||||
width: 56px;
|
||||
}
|
||||
.nav-sidebar.sidebar-collapsed-desktop .nav-sidebar-inner-scroll {
|
||||
overflow-x: hidden;
|
||||
|
@ -1076,7 +1077,7 @@ input {
|
|||
border-radius: 0.25rem;
|
||||
width: auto;
|
||||
line-height: 1rem;
|
||||
margin: 1px 4px;
|
||||
margin: 1px 8px;
|
||||
}
|
||||
.nav-sidebar li.active > a {
|
||||
font-weight: 600;
|
||||
|
@ -1212,7 +1213,7 @@ input {
|
|||
}
|
||||
@media (min-width: 768px) and (max-width: 1199px) {
|
||||
.nav-sidebar:not(.sidebar-expanded-mobile) {
|
||||
width: 48px;
|
||||
width: 56px;
|
||||
}
|
||||
.nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll {
|
||||
overflow-x: hidden;
|
||||
|
@ -1247,7 +1248,7 @@ input {
|
|||
}
|
||||
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header {
|
||||
height: 60px;
|
||||
width: 48px;
|
||||
width: 56px;
|
||||
}
|
||||
.nav-sidebar:not(.sidebar-expanded-mobile) .context-header a {
|
||||
padding: 10px 4px;
|
||||
|
@ -1279,7 +1280,8 @@ input {
|
|||
margin-right: 0;
|
||||
}
|
||||
.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button {
|
||||
width: 48px;
|
||||
width: 55px;
|
||||
padding: 0 21px;
|
||||
}
|
||||
.nav-sidebar:not(.sidebar-expanded-mobile)
|
||||
.toggle-sidebar-button
|
||||
|
@ -1312,10 +1314,10 @@ input {
|
|||
border-radius: 0.25rem;
|
||||
width: auto;
|
||||
line-height: 1rem;
|
||||
margin: 1px 4px;
|
||||
margin: 1px 8px;
|
||||
padding: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-top: 0;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
|
||||
font-weight: 400;
|
||||
|
@ -1334,13 +1336,12 @@ input {
|
|||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.sidebar-top-level-items {
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
.sidebar-top-level-items .context-header a {
|
||||
padding: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-top: 0;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
.sidebar-top-level-items .context-header a .avatar-container {
|
||||
font-weight: 400;
|
||||
|
@ -1386,11 +1387,10 @@ input {
|
|||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f0f0f0;
|
||||
border-top: 1px solid #dbdbdb;
|
||||
background-color: #fafafa;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 256px;
|
||||
width: 255px;
|
||||
}
|
||||
.toggle-sidebar-button .collapse-text,
|
||||
.toggle-sidebar-button .icon-chevron-double-lg-left,
|
||||
|
@ -1404,7 +1404,7 @@ input {
|
|||
}
|
||||
.sidebar-collapsed-desktop .context-header {
|
||||
height: 60px;
|
||||
width: 48px;
|
||||
width: 56px;
|
||||
}
|
||||
.sidebar-collapsed-desktop .context-header a {
|
||||
padding: 10px 4px;
|
||||
|
@ -1436,7 +1436,8 @@ input {
|
|||
margin-right: 0;
|
||||
}
|
||||
.sidebar-collapsed-desktop .toggle-sidebar-button {
|
||||
width: 48px;
|
||||
width: 55px;
|
||||
padding: 0 21px;
|
||||
}
|
||||
.sidebar-collapsed-desktop .toggle-sidebar-button .collapse-text {
|
||||
display: none;
|
||||
|
|
|
@ -41,6 +41,12 @@
|
|||
border-color: $gray-800;
|
||||
}
|
||||
|
||||
.nav-sidebar,
|
||||
.toggle-sidebar-button,
|
||||
.close-nav-button {
|
||||
background-color: darken($gray-50, 4%);
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
li {
|
||||
a {
|
||||
|
@ -68,6 +74,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
aside.right-sidebar:not(.right-sidebar-merge-requests) {
|
||||
background-color: $gray-10;
|
||||
border-left-color: $gray-50;
|
||||
}
|
||||
|
||||
body.gl-dark {
|
||||
@include gitlab-theme($gray-900, $gray-400, $gray-500, $gray-900, $gray-900, $white);
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ module Groups
|
|||
before_action :authorize_update_max_artifacts_size!, only: [:update]
|
||||
before_action :define_variables, only: [:show]
|
||||
before_action :push_licensed_features, only: [:show]
|
||||
before_action :assign_variables_to_gon, only: [:show]
|
||||
|
||||
feature_category :continuous_integration
|
||||
urgency :low
|
||||
|
@ -81,6 +82,10 @@ module Groups
|
|||
# Overridden in EE
|
||||
def push_licensed_features
|
||||
end
|
||||
|
||||
# Overridden in EE
|
||||
def assign_variables_to_gon
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -142,7 +142,7 @@ class IssuableFinder
|
|||
projects_public_or_visible_to_user
|
||||
end
|
||||
|
||||
projects.with_feature_available_for_user(klass, current_user).reorder(nil) # rubocop: disable CodeReuse/ActiveRecord
|
||||
projects.with_feature_available_for_user(klass.base_class, current_user).reorder(nil) # rubocop: disable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -215,7 +215,7 @@ class IssuableFinder
|
|||
end
|
||||
|
||||
def min_access_level
|
||||
ProjectFeature.required_minimum_access_level(klass)
|
||||
ProjectFeature.required_minimum_access_level(klass.base_class)
|
||||
end
|
||||
|
||||
def method_missing(method_name, *args, &block)
|
||||
|
|
|
@ -27,7 +27,7 @@ module Issuables
|
|||
def by_label(issuables)
|
||||
return issuables unless label_names_from_params.present?
|
||||
|
||||
target_model = issuables.model
|
||||
target_model = issuables.base_class
|
||||
|
||||
if filter_by_no_label?
|
||||
issuables.where(label_link_query(target_model).arel.exists.not)
|
||||
|
@ -55,7 +55,7 @@ module Issuables
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def issuables_with_selected_labels(issuables, label_names)
|
||||
target_model = issuables.model
|
||||
target_model = issuables.base_class
|
||||
|
||||
if root_namespace
|
||||
all_label_ids = find_label_ids(label_names)
|
||||
|
@ -77,7 +77,7 @@ module Issuables
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def issuables_without_selected_labels(issuables, label_names)
|
||||
target_model = issuables.model
|
||||
target_model = issuables.base_class
|
||||
|
||||
if root_namespace
|
||||
label_ids = find_label_ids(label_names).flatten(1)
|
||||
|
|
|
@ -37,7 +37,7 @@ class IssuesFinder < IssuableFinder
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def klass
|
||||
Issue.includes(:author)
|
||||
model_class.includes(:author)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
|
@ -47,10 +47,10 @@ class IssuesFinder < IssuableFinder
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def with_confidentiality_access_check
|
||||
return Issue.all if params.user_can_see_all_issues?
|
||||
return model_class.all if params.user_can_see_all_issues?
|
||||
|
||||
# Only admins can see hidden issues, so for non-admins, we filter out any hidden issues
|
||||
issues = Issue.without_hidden
|
||||
issues = model_class.without_hidden
|
||||
|
||||
return issues.all if params.user_can_see_all_confidential_issues?
|
||||
|
||||
|
@ -77,7 +77,7 @@ class IssuesFinder < IssuableFinder
|
|||
|
||||
def init_collection
|
||||
if params.public_only?
|
||||
Issue.public_only
|
||||
model_class.public_only
|
||||
else
|
||||
with_confidentiality_access_check
|
||||
end
|
||||
|
@ -129,7 +129,7 @@ class IssuesFinder < IssuableFinder
|
|||
def by_issue_types(items)
|
||||
issue_type_params = Array(params[:issue_types]).map(&:to_s)
|
||||
return items if issue_type_params.blank?
|
||||
return Issue.none unless (WorkItems::Type.base_types.keys & issue_type_params).sort == issue_type_params.sort
|
||||
return model_class.none unless (WorkItems::Type.base_types.keys & issue_type_params).sort == issue_type_params.sort
|
||||
|
||||
items.with_issue_type(params[:issue_types])
|
||||
end
|
||||
|
@ -140,6 +140,10 @@ class IssuesFinder < IssuableFinder
|
|||
|
||||
items.without_issue_type(issue_type_params)
|
||||
end
|
||||
|
||||
def model_class
|
||||
Issue
|
||||
end
|
||||
end
|
||||
|
||||
IssuesFinder.prepend_mod_with('IssuesFinder')
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# WorkItem model inherits from Issue model. It's planned to be its extension
|
||||
# with widgets support. Because WorkItems are internally Issues, WorkItemsFinder
|
||||
# can be almost identical to IssuesFinder, except it should return instances of
|
||||
# WorkItems instead of Issues
|
||||
module WorkItems
|
||||
class WorkItemsFinder < IssuesFinder
|
||||
def params_class
|
||||
::IssuesFinder::Params
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def model_class
|
||||
WorkItem
|
||||
end
|
||||
end
|
||||
end
|
|
@ -569,7 +569,7 @@ module Ci
|
|||
end
|
||||
|
||||
def stop_action_successful?
|
||||
Feature.disabled?(:env_stopped_on_stop_success, project) || success?
|
||||
success?
|
||||
end
|
||||
|
||||
##
|
||||
|
|
|
@ -18,7 +18,7 @@ module Awardable
|
|||
inner_query = award_emoji_table
|
||||
.project('true')
|
||||
.where(award_emoji_table[:user_id].eq(user.id))
|
||||
.where(award_emoji_table[:awardable_type].eq(self.name))
|
||||
.where(award_emoji_table[:awardable_type].eq(base_class.name))
|
||||
.where(award_emoji_table[:awardable_id].eq(self.arel_table[:id]))
|
||||
|
||||
inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present?
|
||||
|
@ -31,7 +31,7 @@ module Awardable
|
|||
inner_query = award_emoji_table
|
||||
.project('true')
|
||||
.where(award_emoji_table[:user_id].eq(user.id))
|
||||
.where(award_emoji_table[:awardable_type].eq(self.name))
|
||||
.where(award_emoji_table[:awardable_type].eq(base_class.name))
|
||||
.where(award_emoji_table[:awardable_id].eq(self.arel_table[:id]))
|
||||
|
||||
inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present?
|
||||
|
@ -56,13 +56,11 @@ module Awardable
|
|||
awardable_table = self.arel_table
|
||||
awards_table = AwardEmoji.arel_table
|
||||
|
||||
join_clause = awardable_table.join(awards_table, Arel::Nodes::OuterJoin).on(
|
||||
awards_table[:awardable_id].eq(awardable_table[:id]).and(
|
||||
awards_table[:awardable_type].eq(self.name).and(
|
||||
awards_table[:name].eq(emoji_name)
|
||||
)
|
||||
)
|
||||
).join_sources
|
||||
join_clause = awardable_table
|
||||
.join(awards_table, Arel::Nodes::OuterJoin)
|
||||
.on(awards_table[:awardable_id].eq(awardable_table[:id])
|
||||
.and(awards_table[:awardable_type].eq(base_class.name).and(awards_table[:name].eq(emoji_name))))
|
||||
.join_sources
|
||||
|
||||
joins(join_clause).group(awardable_table[:id]).reorder(
|
||||
Arel.sql("COUNT(award_emoji.id) #{direction}")
|
||||
|
|
|
@ -106,23 +106,23 @@ module Issuable
|
|||
scope :closed, -> { with_state(:closed) }
|
||||
|
||||
# rubocop:disable GitlabSecurity/SqlInjection
|
||||
# The `to_ability_name` method is not an user input.
|
||||
# The `assignee_association_name` method is not an user input.
|
||||
scope :assigned, -> do
|
||||
where("EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)")
|
||||
where("EXISTS (SELECT TRUE FROM #{assignee_association_name}_assignees WHERE #{assignee_association_name}_id = #{assignee_association_name}s.id)")
|
||||
end
|
||||
scope :unassigned, -> do
|
||||
where("NOT EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)")
|
||||
where("NOT EXISTS (SELECT TRUE FROM #{assignee_association_name}_assignees WHERE #{assignee_association_name}_id = #{assignee_association_name}s.id)")
|
||||
end
|
||||
scope :assigned_to, ->(users) do
|
||||
assignees_class = self.reflect_on_association("#{to_ability_name}_assignees").klass
|
||||
assignees_class = self.reflect_on_association("#{assignee_association_name}_assignees").klass
|
||||
|
||||
condition = assignees_class.where(user_id: users).where(Arel.sql("#{to_ability_name}_id = #{to_ability_name}s.id"))
|
||||
condition = assignees_class.where(user_id: users).where(Arel.sql("#{assignee_association_name}_id = #{assignee_association_name}s.id"))
|
||||
where(condition.arel.exists)
|
||||
end
|
||||
scope :not_assigned_to, ->(users) do
|
||||
assignees_class = self.reflect_on_association("#{to_ability_name}_assignees").klass
|
||||
assignees_class = self.reflect_on_association("#{assignee_association_name}_assignees").klass
|
||||
|
||||
condition = assignees_class.where(user_id: users).where(Arel.sql("#{to_ability_name}_id = #{to_ability_name}s.id"))
|
||||
condition = assignees_class.where(user_id: users).where(Arel.sql("#{assignee_association_name}_id = #{assignee_association_name}s.id"))
|
||||
where(condition.arel.exists.not)
|
||||
end
|
||||
# rubocop:enable GitlabSecurity/SqlInjection
|
||||
|
@ -412,6 +412,10 @@ module Issuable
|
|||
def parent_class
|
||||
::Project
|
||||
end
|
||||
|
||||
def assignee_association_name
|
||||
to_ability_name
|
||||
end
|
||||
end
|
||||
|
||||
def state
|
||||
|
|
|
@ -15,17 +15,29 @@ module Limitable
|
|||
validate :validate_plan_limit_not_exceeded, on: :create
|
||||
end
|
||||
|
||||
def exceeds_limits?
|
||||
limits, relation = fetch_plan_limit_data
|
||||
|
||||
limits&.exceeded?(limit_name, relation)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_plan_limit_not_exceeded
|
||||
limits, relation = fetch_plan_limit_data
|
||||
|
||||
check_plan_limit_not_exceeded(limits, relation)
|
||||
end
|
||||
|
||||
def fetch_plan_limit_data
|
||||
if GLOBAL_SCOPE == limit_scope
|
||||
validate_global_plan_limit_not_exceeded
|
||||
global_plan_limits
|
||||
else
|
||||
validate_scoped_plan_limit_not_exceeded
|
||||
scoped_plan_limits
|
||||
end
|
||||
end
|
||||
|
||||
def validate_scoped_plan_limit_not_exceeded
|
||||
def scoped_plan_limits
|
||||
scope_relation = self.public_send(limit_scope) # rubocop:disable GitlabSecurity/PublicSend
|
||||
return unless scope_relation
|
||||
return if limit_feature_flag && ::Feature.disabled?(limit_feature_flag, scope_relation)
|
||||
|
@ -34,18 +46,18 @@ module Limitable
|
|||
relation = limit_relation ? self.public_send(limit_relation) : self.class.where(limit_scope => scope_relation) # rubocop:disable GitlabSecurity/PublicSend
|
||||
limits = scope_relation.actual_limits
|
||||
|
||||
check_plan_limit_not_exceeded(limits, relation)
|
||||
[limits, relation]
|
||||
end
|
||||
|
||||
def validate_global_plan_limit_not_exceeded
|
||||
def global_plan_limits
|
||||
relation = self.class.all
|
||||
limits = Plan.default.actual_limits
|
||||
|
||||
check_plan_limit_not_exceeded(limits, relation)
|
||||
[limits, relation]
|
||||
end
|
||||
|
||||
def check_plan_limit_not_exceeded(limits, relation)
|
||||
return unless limits.exceeded?(limit_name, relation)
|
||||
return unless limits&.exceeded?(limit_name, relation)
|
||||
|
||||
errors.add(:base, _("Maximum number of %{name} (%{count}) exceeded") %
|
||||
{ name: limit_name.humanize(capitalize: false), count: limits.public_send(limit_name) }) # rubocop:disable GitlabSecurity/PublicSend
|
||||
|
|
|
@ -300,7 +300,7 @@ class Environment < ApplicationRecord
|
|||
end
|
||||
|
||||
def wait_for_stop?
|
||||
stop_actions.present? && Feature.enabled?(:env_stopped_on_stop_success, project)
|
||||
stop_actions.present?
|
||||
end
|
||||
|
||||
def stop_with_actions!(current_user)
|
||||
|
|
|
@ -13,6 +13,10 @@ class WorkItem < Issue
|
|||
|
||||
scope :inc_relations_for_permission_check, -> { includes(:author, project: :project_feature) }
|
||||
|
||||
def self.assignee_association_name
|
||||
'issue'
|
||||
end
|
||||
|
||||
def noteable_target_type_name
|
||||
'issue'
|
||||
end
|
||||
|
|
|
@ -669,6 +669,7 @@ class ProjectPolicy < BasePolicy
|
|||
enable :read_design
|
||||
enable :read_design_activity
|
||||
enable :read_issue_link
|
||||
enable :read_work_item
|
||||
end
|
||||
|
||||
rule { can?(:developer_access) }.policy do
|
||||
|
|
|
@ -8,4 +8,9 @@ class WorkItemPolicy < IssuePolicy
|
|||
rule { can?(:update_issue) }.enable :update_work_item
|
||||
|
||||
rule { can?(:read_issue) }.enable :read_work_item
|
||||
# because IssuePolicy delegates to ProjectPolicy and
|
||||
# :read_work_item is enabled in ProjectPolicy too, we
|
||||
# need to make sure we also prevent this rule if read_issue
|
||||
# is prevented
|
||||
rule { ~can?(:read_issue) }.prevent :read_work_item
|
||||
end
|
||||
|
|
|
@ -32,6 +32,8 @@ module BulkImports
|
|||
destination_filepath = File.join(export_path, lfs_object.oid)
|
||||
|
||||
if lfs_object.local_store?
|
||||
return unless File.exist?(lfs_object.file.path)
|
||||
|
||||
copy_files(lfs_object.file.path, destination_filepath)
|
||||
else
|
||||
download(lfs_object.file.url, destination_filepath)
|
||||
|
|
|
@ -22,7 +22,7 @@ module ResourceEvents
|
|||
end
|
||||
|
||||
def build_resource_args
|
||||
key = resource.class.name.foreign_key
|
||||
key = resource.class.base_class.name.foreign_key
|
||||
|
||||
{
|
||||
user_id: user.id,
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
%fieldset
|
||||
%legend
|
||||
%legend.gl-border-bottom-0
|
||||
= s_('AdminUsers|Access')
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
.col-12
|
||||
= f.label :projects_limit
|
||||
.col-sm-10
|
||||
= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control gl-form-input'
|
||||
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label.gl-pt-0
|
||||
.col-12.gl-pt-0
|
||||
= f.label :can_create_group
|
||||
.col-sm-10
|
||||
= f.gitlab_ui_checkbox_component :can_create_group, ''
|
||||
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label.gl-pt-0
|
||||
.col-12.gl-pt-0
|
||||
= f.label :access_level
|
||||
.col-sm-10
|
||||
- editing_current_user = (current_user == @user)
|
||||
|
||||
= f.gitlab_ui_radio_component :access_level, :regular,
|
||||
|
@ -35,10 +32,10 @@
|
|||
|
||||
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label.gl-pt-0
|
||||
.col-12.gl-pt-0
|
||||
= f.label :external
|
||||
.hidden{ data: user_internal_regex_data }
|
||||
.col-sm-10.gl-display-flex.gl-align-items-baseline
|
||||
.col-12.gl-display-flex.gl-align-items-baseline
|
||||
= f.gitlab_ui_checkbox_component :external, s_('AdminUsers|External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects, groups, or personal snippets.')
|
||||
%row.hidden#warning_external_automatically_set
|
||||
= gl_badge_tag s_('AdminUsers|Automatically marked as default internal user'), variant: :warning
|
||||
|
@ -46,9 +43,9 @@
|
|||
.form-group.row
|
||||
- @user.credit_card_validation || @user.build_credit_card_validation
|
||||
= f.fields_for :credit_card_validation do |ff|
|
||||
.col-sm-2.col-form-label.gl-pt-0
|
||||
.col-12.gl-pt-0
|
||||
= ff.label s_('AdminUsers|Validate user account')
|
||||
.col-sm-10.gl-display-flex.gl-align-items-baseline
|
||||
.col-12.gl-display-flex.gl-align-items-baseline
|
||||
= ff.gitlab_ui_checkbox_component :credit_card_validated_at,
|
||||
s_('AdminUsers|User is validated and can use free CI minutes on shared runners.'),
|
||||
help_text: s_('AdminUsers|A user can validate themselves by inputting a credit/debit card, or an admin can manually validate a user.'),
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
%fieldset
|
||||
%legend= _('Admin notes')
|
||||
%legend.gl-border-bottom-0= _('Admin notes')
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
.col-12
|
||||
= f.label :note, s_('Admin|Note')
|
||||
.col-sm-10
|
||||
= f.text_area :note, class: 'form-control gl-form-input gl-form-textarea'
|
||||
|
|
|
@ -3,47 +3,39 @@
|
|||
= form_errors(@user)
|
||||
|
||||
%fieldset
|
||||
%legend= _('Account')
|
||||
%legend.gl-border-bottom-0= _('Account')
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
= f.label :name
|
||||
.col-sm-10
|
||||
.col-12
|
||||
= f.label "#{:name} (required)"
|
||||
= f.text_field :name, required: true, autocomplete: 'off', class: 'form-control gl-form-input'
|
||||
%span.help-inline * #{_('required')}
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
= f.label :username
|
||||
.col-sm-10
|
||||
.col-12
|
||||
= f.label "#{:username} (required)"
|
||||
= f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control gl-form-input'
|
||||
%span.help-inline * #{_('required')}
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
= f.label :email
|
||||
.col-sm-10
|
||||
.col-12
|
||||
= f.label "#{:email} (required)"
|
||||
= f.text_field :email, required: true, autocomplete: 'off', class: 'form-control gl-form-input'
|
||||
%span.help-inline * #{_('required')}
|
||||
|
||||
- if @user.new_record?
|
||||
%fieldset
|
||||
%legend= _('Password')
|
||||
%legend.gl-border-bottom-0= _('Password')
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
= f.label :password
|
||||
.col-sm-10
|
||||
.col-12
|
||||
%strong
|
||||
= _('Reset link will be generated and sent to the user. %{break} User will be forced to set the password on first sign in.').html_safe % { break: '<br />'.html_safe }
|
||||
- else
|
||||
%fieldset
|
||||
%legend= _('Password')
|
||||
%legend.gl-border-bottom-0= _('Password')
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
.col-12
|
||||
= f.label :password
|
||||
.col-sm-10
|
||||
.col-12
|
||||
= f.password_field :password, disabled: f.object.force_random_password, autocomplete: 'new-password', class: 'form-control gl-form-input'
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
.col-12
|
||||
= f.label :password_confirmation
|
||||
.col-sm-10
|
||||
.col-12
|
||||
= f.password_field :password_confirmation, disabled: f.object.force_random_password, autocomplete: 'new-password', class: 'form-control gl-form-input'
|
||||
|
||||
= render partial: 'access_levels', locals: { f: f }
|
||||
|
@ -53,37 +45,33 @@
|
|||
= render_if_exists 'admin/users/limits', f: f
|
||||
|
||||
%fieldset
|
||||
%legend= _('Profile')
|
||||
%legend.gl-border-bottom-0= _('Profile')
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
.col-12
|
||||
= f.label :avatar
|
||||
.col-sm-10
|
||||
.col-12
|
||||
= f.file_field :avatar
|
||||
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
.col-12
|
||||
= f.label :skype
|
||||
.col-sm-10
|
||||
= f.text_field :skype, class: 'form-control gl-form-input'
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
.col-12
|
||||
= f.label :linkedin
|
||||
.col-sm-10
|
||||
= f.text_field :linkedin, class: 'form-control gl-form-input'
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
.col-12
|
||||
= f.label :twitter
|
||||
.col-sm-10
|
||||
= f.text_field :twitter, class: 'form-control gl-form-input'
|
||||
.form-group.row
|
||||
.col-sm-2.col-form-label
|
||||
.col-12
|
||||
= f.label :website_url
|
||||
.col-sm-10
|
||||
= f.text_field :website_url, class: 'form-control gl-form-input'
|
||||
|
||||
= render 'admin/users/admin_notes', f: f
|
||||
|
||||
.form-actions
|
||||
%div
|
||||
- if @user.new_record?
|
||||
= f.submit _('Create user'), class: "btn gl-button btn-confirm"
|
||||
= link_to _('Cancel'), admin_users_path, class: "gl-button btn btn-default btn-cancel"
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
- page_title _("Edit"), @user.name, _("Users")
|
||||
%h1.page-title
|
||||
%h1.page-title.gl-font-size-h-display.gl-mb-6
|
||||
= _("Edit user: %{user_name}") % { user_name: @user.name }
|
||||
%hr
|
||||
= render 'form'
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
- page_title _("New User")
|
||||
%h1.page-title
|
||||
%h1.page-title.gl-font-size-h-display.gl-mb-6
|
||||
= s_('AdminUsers|New user')
|
||||
%hr
|
||||
= render 'form'
|
||||
|
|
|
@ -51,3 +51,6 @@
|
|||
|
||||
.settings-content
|
||||
= render 'groups/settings/ci_cd/auto_devops_form', group: @group
|
||||
|
||||
- if ::Feature.enabled?(:group_level_protected_environment, @group)
|
||||
= render_if_exists 'groups/settings/ci_cd/protected_environments', expanded: expanded
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
= yield :flash_message
|
||||
= dispensable_render "shared/service_ping_consent"
|
||||
= dispensable_render_if_exists "layouts/header/ee_subscribable_banner"
|
||||
= dispensable_render_if_exists "layouts/header/seats_count_alert"
|
||||
= dispensable_render_if_exists "layouts/header/seat_count_alert"
|
||||
= dispensable_render_if_exists "shared/namespace_storage_limit_alert"
|
||||
= dispensable_render_if_exists "shared/namespace_user_cap_reached_alert"
|
||||
= dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
%a.toggle-sidebar-button.js-toggle-sidebar.qa-toggle-sidebar.rspec-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
|
||||
= sprite_icon('chevron-double-lg-left', css_class: 'icon-chevron-double-lg-left')
|
||||
= sprite_icon('chevron-double-lg-left', size: 12, css_class: 'icon-chevron-double-lg-left')
|
||||
%span.collapse-text.gl-ml-3= _("Collapse sidebar")
|
||||
|
||||
= button_tag class: 'close-nav-button', type: 'button' do
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
const coreJSVersion = require('./node_modules/core-js/package.json').version;
|
||||
|
||||
const BABEL_ENV = process.env.BABEL_ENV || process.env.NODE_ENV || null;
|
||||
|
||||
let presets = [
|
||||
|
@ -5,7 +7,7 @@ let presets = [
|
|||
'@babel/preset-env',
|
||||
{
|
||||
useBuiltIns: 'usage',
|
||||
corejs: { version: 3, proposals: true },
|
||||
corejs: { version: coreJSVersion, proposals: true },
|
||||
modules: false,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_docker_image_pull_policy
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85588
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363186
|
||||
milestone: '15.1'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: env_stopped_on_stop_success
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86478
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/361473
|
||||
milestone: '15.0'
|
||||
name: seat_count_alerts
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89204
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/362041
|
||||
milestone: '15.1'
|
||||
type: development
|
||||
group: group::release
|
||||
group: group::purchase
|
||||
default_enabled: false
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddSecurityPolicyScanExecutionSchedulesToPlanLimits < Gitlab::Database::Migration[2.0]
|
||||
def up
|
||||
add_column(:plan_limits, :security_policy_scan_execution_schedules, :integer, default: 0, null: false)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column(:plan_limits, :security_policy_scan_execution_schedules)
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
4b979c4ae290efdbc7c4bfe7105f0b30d00e532ac11c579db7417a317fd35db8
|
|
@ -18773,7 +18773,8 @@ CREATE TABLE plan_limits (
|
|||
dotenv_size integer DEFAULT 5120 NOT NULL,
|
||||
pipeline_triggers integer DEFAULT 25000 NOT NULL,
|
||||
project_ci_secure_files integer DEFAULT 100 NOT NULL,
|
||||
repository_size bigint DEFAULT 0 NOT NULL
|
||||
repository_size bigint DEFAULT 0 NOT NULL,
|
||||
security_policy_scan_execution_schedules integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE plan_limits_id_seq
|
||||
|
|
|
@ -19,7 +19,7 @@ The steps below should be followed in the order they appear. **Make sure the Git
|
|||
|
||||
If you installed GitLab using the Omnibus packages (highly recommended):
|
||||
|
||||
1. [Install GitLab Enterprise Edition](https://about.gitlab.com/install/) on the nodes that will serve as the **secondary** site. Do not create an account or log in to the new **secondary** site.
|
||||
1. [Install GitLab Enterprise Edition](https://about.gitlab.com/install/) on the nodes that will serve as the **secondary** site. Do not create an account or log in to the new **secondary** site. The **GitLab version must match** across primary and secondary sites.
|
||||
1. [Add the GitLab License](../../../user/admin_area/license.md) on the **primary** site to unlock Geo. The license must be for [GitLab Premium](https://about.gitlab.com/pricing/) or higher.
|
||||
1. [Set up the database replication](database.md) (`primary (read-write) <-> secondary (read-only)` topology).
|
||||
1. [Configure fast lookup of authorized SSH keys in the database](../../operations/fast_ssh_key_lookup.md). This step is required and needs to be done on **both** the **primary** and **secondary** sites.
|
||||
|
|
|
@ -554,6 +554,26 @@ Plan.default.actual_limits.update!(ci_daily_pipeline_schedule_triggers: 1440)
|
|||
|
||||
This limit is [enabled on GitLab.com](../user/gitlab_com/index.md#gitlab-cicd).
|
||||
|
||||
### Limit the number of schedule rules defined for security policy project
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/335659) in GitLab 15.1.
|
||||
|
||||
You can limit the total number of schedule rules per security policy project. This limit is
|
||||
checked each time policies with schedule rules are updated. If a new schedule rule would
|
||||
cause the total number of schedule rules to exceed the limit, the new schedule rule is
|
||||
not processed.
|
||||
|
||||
By default, self-managed instances do not limit the number of processable schedule rules.
|
||||
|
||||
To set this limit for a self-managed installation, run the following in the
|
||||
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
|
||||
|
||||
```ruby
|
||||
Plan.default.actual_limits.update!(security_policy_scan_execution_schedules: 100)
|
||||
```
|
||||
|
||||
This limit is [enabled on GitLab.com](../user/gitlab_com/index.md#gitlab-cicd).
|
||||
|
||||
### Number of instance level variables
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216097) in GitLab 13.1.
|
||||
|
|
|
@ -1883,6 +1883,52 @@ image:
|
|||
|
||||
- [Override the entrypoint of an image](../docker/using_docker_images.md#override-the-entrypoint-of-an-image).
|
||||
|
||||
#### `image:pull_policy`
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21619) in GitLab 15.1 [with a flag](../../administration/feature_flags.md) named `ci_docker_image_pull_policy`. Disabled by default.
|
||||
> - Requires GitLab Runner 15.1 or later.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available,
|
||||
ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `ci_docker_image_pull_policy`.
|
||||
The feature is not ready for production use.
|
||||
|
||||
The pull policy that the runner uses to fetch the Docker image.
|
||||
|
||||
**Keyword type**: Job keyword. You can use it only as part of a job or in the [`default` section](#default).
|
||||
|
||||
**Possible inputs**:
|
||||
|
||||
- A single pull policy, or multiple pull policies in an array.
|
||||
Can be `always`, `if-not-present`, or `never`.
|
||||
|
||||
**Examples of `image:pull_policy`**:
|
||||
|
||||
```yaml
|
||||
job1:
|
||||
script: echo "A single pull policy."
|
||||
image:
|
||||
name: ruby:3.0
|
||||
pull_policy: if-not-present
|
||||
|
||||
job2:
|
||||
script: echo "Multiple pull policies."
|
||||
image:
|
||||
name: ruby:3.0
|
||||
pull_policy: [always, if-not-present]
|
||||
```
|
||||
|
||||
**Additional details**:
|
||||
|
||||
- If the runner does not support the defined pull policy, the job fails with an error similar to:
|
||||
`ERROR: Job failed (system failure): the configured PullPolicies ([always]) are not allowed by AllowedPullPolicies ([never])`.
|
||||
|
||||
**Related topics**:
|
||||
|
||||
- [Run your CI/CD jobs in Docker containers](../docker/using_docker_images.md).
|
||||
- [How runner pull policies work](https://docs.gitlab.com/runner/executors/docker.html#how-pull-policies-work).
|
||||
- [Using multiple pull policies](https://docs.gitlab.com/runner/executors/docker.html#using-multiple-pull-policies).
|
||||
|
||||
### `inherit`
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207484) in GitLab 12.9.
|
||||
|
|
|
@ -23,7 +23,7 @@ The following outline re-uses the [maturity metric](https://about.gitlab.com/dir
|
|||
- [Release management](#release-management)
|
||||
- [Enabled on GitLab.com](feature_flags/controls.md#enabling-a-feature-for-gitlabcom)
|
||||
- Complete
|
||||
- [Configurable by the GitLab orchestrator](https://gitlab.com/gitlab-org/gitlab-orchestrator)
|
||||
- [Configurable by the GitLab Environment Toolkit](https://gitlab.com/gitlab-org/gitlab-environment-toolkit)
|
||||
- Lovable
|
||||
- Enabled by default for the majority of users
|
||||
|
||||
|
|
|
@ -185,6 +185,7 @@ See the [test engineering process](https://about.gitlab.com/handbook/engineering
|
|||
##### Observability instrumentation
|
||||
|
||||
1. I have included enough instrumentation to facilitate debugging and proactive performance improvements through observability.
|
||||
See [example](https://gitlab.com/gitlab-org/gitlab/-/issues/346124#expectations) of adding feature flags, logging, and instrumentation.
|
||||
|
||||
##### Documentation
|
||||
|
||||
|
|
|
@ -185,7 +185,7 @@ gitlab_rails['omniauth_providers'] = [
|
|||
name: 'saml_1',
|
||||
args: {
|
||||
name: 'saml_1', # This is mandatory and must match the provider name
|
||||
strategy_class: 'OmniAuth::Strategies::SAML'
|
||||
strategy_class: 'OmniAuth::Strategies::SAML',
|
||||
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_1/callback', # URL must match the name of the provider
|
||||
... # Put here all the required arguments similar to a single provider
|
||||
},
|
||||
|
@ -195,7 +195,7 @@ gitlab_rails['omniauth_providers'] = [
|
|||
name: 'saml_2',
|
||||
args: {
|
||||
name: 'saml_2', # This is mandatory and must match the provider name
|
||||
strategy_class: 'OmniAuth::Strategies::SAML'
|
||||
strategy_class: 'OmniAuth::Strategies::SAML',
|
||||
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml_2/callback', # URL must match the name of the provider
|
||||
... # Put here all the required arguments similar to a single provider
|
||||
},
|
||||
|
|
|
@ -156,6 +156,7 @@ the related documentation.
|
|||
| Maximum number of pipeline triggers in a project | `25000` for Free tier, Unlimited for all paid tiers | See [Limit the number of pipeline triggers](../../administration/instance_limits.md#limit-the-number-of-pipeline-triggers) |
|
||||
| Maximum pipeline schedules in projects | `10` for Free tier, `50` for all paid tiers | See [Number of pipeline schedules](../../administration/instance_limits.md#number-of-pipeline-schedules) |
|
||||
| Maximum pipelines per schedule | `24` for Free tier, `288` for all paid tiers | See [Limit the number of pipelines created by a pipeline schedule per day](../../administration/instance_limits.md#limit-the-number-of-pipelines-created-by-a-pipeline-schedule-per-day) |
|
||||
| Maximum number of schedule rules defined for each security policy project | Unlimited for all paid tiers | See [Number of schedule rules defined for each security policy project](../../administration/instance_limits.md#limit-the-number-of-schedule-rules-defined-for-security-policy-project) |
|
||||
| Scheduled job archiving | 3 months (from June 22, 2020). Jobs created before that date were archived after September 22, 2020. | Never |
|
||||
| Maximum test cases per [unit test report](../../ci/testing/unit_test_reports.md) | `500000` | Unlimited |
|
||||
| Maximum registered runners | Free tier: `50` per-group / `50` per-project<br/>All paid tiers: `1000` per-group / `1000` per-project | See [Number of registered runners per scope](../../administration/instance_limits.md#number-of-registered-runners-per-scope) |
|
||||
|
|
|
@ -7,6 +7,8 @@ module API
|
|||
class Image < Grape::Entity
|
||||
expose :name, :entrypoint
|
||||
expose :ports, using: Entities::Ci::JobRequest::Port
|
||||
|
||||
expose :pull_policy, if: ->(_) { ::Feature.enabled?(:ci_docker_image_pull_policy) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,10 @@ module API
|
|||
module Entities
|
||||
module Ci
|
||||
module JobRequest
|
||||
class Service < Entities::Ci::JobRequest::Image
|
||||
class Service < Grape::Entity
|
||||
expose :name, :entrypoint
|
||||
expose :ports, using: Entities::Ci::JobRequest::Port
|
||||
|
||||
expose :alias, :command
|
||||
expose :variables
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ module Gitlab
|
|||
module Ci
|
||||
module Build
|
||||
class Image
|
||||
attr_reader :alias, :command, :entrypoint, :name, :ports, :variables
|
||||
attr_reader :alias, :command, :entrypoint, :name, :ports, :variables, :pull_policy
|
||||
|
||||
class << self
|
||||
def from_image(job)
|
||||
|
@ -34,6 +34,7 @@ module Gitlab
|
|||
@name = image[:name]
|
||||
@ports = build_ports(image).select(&:valid?)
|
||||
@variables = build_variables(image)
|
||||
@pull_policy = image[:pull_policy]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -12,11 +12,13 @@ module Gitlab
|
|||
include ::Gitlab::Config::Entry::Attributable
|
||||
include ::Gitlab::Config::Entry::Configurable
|
||||
|
||||
ALLOWED_KEYS = %i[name entrypoint ports].freeze
|
||||
ALLOWED_KEYS = %i[name entrypoint ports pull_policy].freeze
|
||||
LEGACY_ALLOWED_KEYS = %i[name entrypoint ports].freeze
|
||||
|
||||
validations do
|
||||
validates :config, hash_or_string: true
|
||||
validates :config, allowed_keys: ALLOWED_KEYS
|
||||
validates :config, allowed_keys: ALLOWED_KEYS, if: :ci_docker_image_pull_policy_enabled?
|
||||
validates :config, allowed_keys: LEGACY_ALLOWED_KEYS, unless: :ci_docker_image_pull_policy_enabled?
|
||||
validates :config, disallowed_keys: %i[ports], unless: :with_image_ports?
|
||||
|
||||
validates :name, type: String, presence: true
|
||||
|
@ -26,7 +28,10 @@ module Gitlab
|
|||
entry :ports, Entry::Ports,
|
||||
description: 'Ports used to expose the image'
|
||||
|
||||
attributes :ports
|
||||
entry :pull_policy, Entry::PullPolicy,
|
||||
description: 'Pull policy for the image'
|
||||
|
||||
attributes :ports, :pull_policy
|
||||
|
||||
def name
|
||||
value[:name]
|
||||
|
@ -37,16 +42,28 @@ module Gitlab
|
|||
end
|
||||
|
||||
def value
|
||||
return { name: @config } if string?
|
||||
return @config if hash?
|
||||
|
||||
{}
|
||||
if string?
|
||||
{ name: @config }
|
||||
elsif hash?
|
||||
{
|
||||
name: @config[:name],
|
||||
entrypoint: @config[:entrypoint],
|
||||
ports: ports_value,
|
||||
pull_policy: (ci_docker_image_pull_policy_enabled? ? pull_policy_value : nil)
|
||||
}.compact
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
def with_image_ports?
|
||||
opt(:with_image_ports)
|
||||
end
|
||||
|
||||
def ci_docker_image_pull_policy_enabled?
|
||||
::Feature.enabled?(:ci_docker_image_pull_policy)
|
||||
end
|
||||
|
||||
def skip_config_hash_validation?
|
||||
true
|
||||
end
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Ci
|
||||
class Config
|
||||
module Entry
|
||||
##
|
||||
# Entry that represents a configuration of the pull policies of an image.
|
||||
#
|
||||
class PullPolicy < ::Gitlab::Config::Entry::Node
|
||||
include ::Gitlab::Config::Entry::Validatable
|
||||
|
||||
ALLOWED_POLICIES = %w[always never if-not-present].freeze
|
||||
|
||||
validations do
|
||||
validates :config, array_of_strings_or_string: true
|
||||
validates :config,
|
||||
allowed_array_values: { in: ALLOWED_POLICIES },
|
||||
presence: true,
|
||||
if: :array?
|
||||
validates :config,
|
||||
inclusion: { in: ALLOWED_POLICIES },
|
||||
if: :string?
|
||||
end
|
||||
|
||||
def value
|
||||
# We either return an array with policies or nothing
|
||||
Array(@config).presence
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -106,6 +106,10 @@ module Gitlab
|
|||
@config.is_a?(Hash)
|
||||
end
|
||||
|
||||
def array?
|
||||
@config.is_a?(Array)
|
||||
end
|
||||
|
||||
def string?
|
||||
@config.is_a?(String)
|
||||
end
|
||||
|
|
|
@ -56,7 +56,11 @@ module Gitlab
|
|||
end
|
||||
|
||||
def copy_file_for_lfs_object(lfs_object)
|
||||
copy_files(lfs_object.file.path, destination_path_for_object(lfs_object))
|
||||
file_path = lfs_object.file.path
|
||||
|
||||
return unless File.exist?(file_path)
|
||||
|
||||
copy_files(file_path, destination_path_for_object(lfs_object))
|
||||
end
|
||||
|
||||
def append_lfs_json_for_batch(lfs_objects_batch)
|
||||
|
|
|
@ -4,6 +4,23 @@ databases = ActiveRecord::Tasks::DatabaseTasks.setup_initial_database_yaml
|
|||
|
||||
namespace :gitlab do
|
||||
namespace :db do
|
||||
DB_CONFIG_NAME_KEY = 'gitlab_db_config_name'
|
||||
|
||||
DB_IDENTIFIER_SQL = <<-SQL
|
||||
SELECT system_identifier, current_database()
|
||||
FROM pg_control_system()
|
||||
SQL
|
||||
|
||||
# We fetch timestamp as a way to properly handle race conditions
|
||||
# fail in such cases, which should not really happen in production environment
|
||||
DB_IDENTIFIER_WITH_DB_CONFIG_NAME_SQL = <<-SQL
|
||||
SELECT
|
||||
system_identifier, current_database(),
|
||||
value as db_config_name, created_at as timestamp
|
||||
FROM pg_control_system()
|
||||
LEFT JOIN ar_internal_metadata ON ar_internal_metadata.key=$1
|
||||
SQL
|
||||
|
||||
desc 'Validates `config/database.yml` to ensure a correct behavior is configured'
|
||||
task validate_config: :environment do
|
||||
original_db_config = ActiveRecord::Base.connection_db_config # rubocop:disable Database/MultipleDatabases
|
||||
|
@ -14,26 +31,22 @@ namespace :gitlab do
|
|||
db_configs = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, include_replicas: true)
|
||||
db_configs = db_configs.reject(&:replica?)
|
||||
|
||||
# Map each database connection into unique identifier of system+database
|
||||
# rubocop:disable Database/MultipleDatabases
|
||||
all_connections = db_configs.map do |db_config|
|
||||
identifier =
|
||||
begin
|
||||
ActiveRecord::Base.establish_connection(db_config) # rubocop: disable Database/EstablishConnection
|
||||
ActiveRecord::Base.connection.select_one("SELECT system_identifier, current_database() FROM pg_control_system()")
|
||||
rescue ActiveRecord::ConnectionNotEstablished, PG::ConnectionBad => err
|
||||
warn "WARNING: Could not establish database connection for #{db_config.name}: #{err.message}"
|
||||
rescue ActiveRecord::NoDatabaseError
|
||||
end
|
||||
# The `pg_control_system()` is not enough to properly discover matching database systems
|
||||
# since in case of cluster promotion it will return the same identifier as main cluster
|
||||
# We instead set an `ar_internal_metadata` information with configured database name
|
||||
db_configs.reverse_each do |db_config|
|
||||
insert_db_identifier(db_config)
|
||||
end
|
||||
|
||||
# Map each database connection into unique identifier of system+database
|
||||
all_connections = db_configs.map do |db_config|
|
||||
{
|
||||
name: db_config.name,
|
||||
config: db_config,
|
||||
database_tasks?: db_config.database_tasks?,
|
||||
identifier: identifier
|
||||
identifier: get_db_identifier(db_config)
|
||||
}
|
||||
end.compact
|
||||
# rubocop:enable Database/MultipleDatabases
|
||||
end
|
||||
|
||||
unique_connections = all_connections.group_by { |connection| connection[:identifier] }
|
||||
primary_connection = all_connections.find { |connection| ActiveRecord::Base.configurations.primary?(connection[:name]) }
|
||||
|
@ -111,5 +124,39 @@ namespace :gitlab do
|
|||
Rake::Task["db:schema:load:#{name}"].enhance(['gitlab:db:validate_config'])
|
||||
Rake::Task["db:schema:dump:#{name}"].enhance(['gitlab:db:validate_config'])
|
||||
end
|
||||
|
||||
def insert_db_identifier(db_config)
|
||||
ActiveRecord::Base.establish_connection(db_config) # rubocop: disable Database/EstablishConnection
|
||||
|
||||
if ActiveRecord::InternalMetadata.table_exists?
|
||||
ts = Time.zone.now
|
||||
|
||||
ActiveRecord::InternalMetadata.upsert(
|
||||
{ key: DB_CONFIG_NAME_KEY,
|
||||
value: db_config.name,
|
||||
created_at: ts,
|
||||
updated_at: ts }
|
||||
)
|
||||
end
|
||||
rescue ActiveRecord::ConnectionNotEstablished, PG::ConnectionBad => err
|
||||
warn "WARNING: Could not establish database connection for #{db_config.name}: #{err.message}"
|
||||
rescue ActiveRecord::NoDatabaseError
|
||||
end
|
||||
|
||||
def get_db_identifier(db_config)
|
||||
ActiveRecord::Base.establish_connection(db_config) # rubocop: disable Database/EstablishConnection
|
||||
|
||||
# rubocop:disable Database/MultipleDatabases
|
||||
if ActiveRecord::InternalMetadata.table_exists?
|
||||
ActiveRecord::Base.connection.select_one(
|
||||
DB_IDENTIFIER_WITH_DB_CONFIG_NAME_SQL, nil, [DB_CONFIG_NAME_KEY])
|
||||
else
|
||||
ActiveRecord::Base.connection.select_one(DB_IDENTIFIER_SQL)
|
||||
end
|
||||
# rubocop:enable Database/MultipleDatabases
|
||||
rescue ActiveRecord::ConnectionNotEstablished, PG::ConnectionBad => err
|
||||
warn "WARNING: Could not establish database connection for #{db_config.name}: #{err.message}"
|
||||
rescue ActiveRecord::NoDatabaseError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14996,6 +14996,9 @@ msgstr ""
|
|||
msgid "Estimated"
|
||||
msgstr ""
|
||||
|
||||
msgid "Even if you reach the number of seats in your subscription, you can continue to add users, and GitLab will bill you for the overage."
|
||||
msgstr ""
|
||||
|
||||
msgid "EventFilterBy|Filter by all"
|
||||
msgstr ""
|
||||
|
||||
|
@ -15543,6 +15546,9 @@ msgstr ""
|
|||
msgid "Failed to load groups, users and deploy keys."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load groups."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load iteration cadences."
|
||||
msgstr ""
|
||||
|
||||
|
@ -20995,6 +21001,9 @@ msgstr ""
|
|||
msgid "InviteMembersModal|To get more members an owner of this namespace can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier."
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|To make more space, you can remove members who no longer need access."
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|Username or email address"
|
||||
msgstr ""
|
||||
|
||||
|
@ -21004,6 +21013,9 @@ msgstr ""
|
|||
msgid "InviteMembersModal|You only have space for %{count} more %{members} in %{name}"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|You only have space for %{count} more %{members} in your personal projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group."
|
||||
msgstr ""
|
||||
|
||||
|
@ -21019,6 +21031,9 @@ msgstr ""
|
|||
msgid "InviteMembersModal|You've reached your %{count} %{members} limit for %{name}"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembersModal|You've reached your %{count} %{members} limit for your personal projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "InviteMembers|Invite a group"
|
||||
msgstr ""
|
||||
|
||||
|
@ -30785,6 +30800,9 @@ msgstr ""
|
|||
msgid "ProtectedEnvironment|Environment"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedEnvironment|Only specified groups can execute deployments in protected environments."
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedEnvironment|Only specified users can execute deployments in a protected environment."
|
||||
msgstr ""
|
||||
|
||||
|
@ -30803,6 +30821,9 @@ msgstr ""
|
|||
msgid "ProtectedEnvironment|Select an environment"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedEnvironment|Select groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedEnvironment|Select users"
|
||||
msgstr ""
|
||||
|
||||
|
@ -33789,6 +33810,9 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|%{scanners} %{severities} in an open merge request targeting %{branches}."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|+%{count} more"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityOrchestration|.yaml mode"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34122,9 +34146,6 @@ msgstr ""
|
|||
msgid "SecurityOrchestration|vulnerability"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityPolicies|+%{count} more"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|%{count} Selected"
|
||||
msgstr ""
|
||||
|
||||
|
@ -44169,8 +44190,10 @@ msgstr ""
|
|||
msgid "Your subscription expired!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your subscription has %{remaining_seats_count} out of %{total_seats_count} seats remaining. Even if you reach the number of seats in your subscription, you can continue to add users, and GitLab will bill you for the overage."
|
||||
msgstr ""
|
||||
msgid "Your subscription has %{remaining_seat_count} out of %{total_seat_count} seat remaining."
|
||||
msgid_plural "Your subscription has %{remaining_seat_count} out of %{total_seat_count} seats remaining."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Your subscription is now expired. To renew, export your license usage file and email it to %{renewal_service_email}. A new license will be emailed to the email address registered in the %{customers_dot}. You can add this license to your instance. To use Free tier, remove your current license."
|
||||
msgstr ""
|
||||
|
@ -45885,9 +45908,6 @@ msgstr ""
|
|||
msgid "repository:"
|
||||
msgstr ""
|
||||
|
||||
msgid "required"
|
||||
msgstr ""
|
||||
|
||||
msgid "satisfied"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
FROM registry.gitlab.com/gitlab-org/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-lfs-2.9-chrome-99-docker-20.10.14-gcloud-383-kubectl-1.23
|
||||
ARG DOCKER_VERSION=20.10.14
|
||||
ARG CHROME_VERSION=101
|
||||
|
||||
FROM registry.gitlab.com/gitlab-org/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-lfs-2.9-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-gcloud-383-kubectl-1.23
|
||||
LABEL maintainer="GitLab Quality Department <quality@gitlab.com>"
|
||||
|
||||
ENV DEBIAN_FRONTEND="noninteractive"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WorkItems::WorkItemsFinder do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
include_context 'WorkItemsFinder context'
|
||||
|
||||
it_behaves_like 'issues or work items finder', :work_item, 'WorkItemsFinder#execute context'
|
||||
end
|
|
@ -200,6 +200,30 @@ describe('InviteModalBase', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when user limit is close on a personal namespace', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(
|
||||
{
|
||||
closeToLimit: true,
|
||||
reachedLimit: false,
|
||||
usersLimitDataset: { membersPath, userNamespace: true },
|
||||
},
|
||||
{ GlModal, GlFormGroup },
|
||||
);
|
||||
});
|
||||
|
||||
it('renders correct buttons', () => {
|
||||
const cancelButton = findCancelButton();
|
||||
const actionButton = findActionButton();
|
||||
|
||||
expect(cancelButton.text()).toBe(INVITE_BUTTON_TEXT_DISABLED);
|
||||
expect(cancelButton.attributes('href')).toBe(membersPath);
|
||||
|
||||
expect(actionButton.text()).toBe(INVITE_BUTTON_TEXT);
|
||||
expect(actionButton.attributes('href')).toBe(); // default submit button
|
||||
});
|
||||
});
|
||||
|
||||
describe('when users limit is not reached', () => {
|
||||
const textRegex = /Select a role.+Read more about role permissions Access expiration date \(optional\)/;
|
||||
|
||||
|
|
|
@ -14,9 +14,15 @@ describe('UserLimitNotification', () => {
|
|||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
|
||||
const createComponent = (reachedLimit = false, usersLimitDataset = {}) => {
|
||||
const createComponent = (
|
||||
closeToLimit = false,
|
||||
reachedLimit = false,
|
||||
usersLimitDataset = {},
|
||||
props = {},
|
||||
) => {
|
||||
wrapper = shallowMountExtended(UserLimitNotification, {
|
||||
propsData: {
|
||||
closeToLimit,
|
||||
reachedLimit,
|
||||
usersLimitDataset: {
|
||||
freeUsersLimit,
|
||||
|
@ -25,6 +31,7 @@ describe('UserLimitNotification', () => {
|
|||
purchasePath: 'purchasePath',
|
||||
...usersLimitDataset,
|
||||
},
|
||||
...props,
|
||||
},
|
||||
provide: { name: 'my group' },
|
||||
stubs: { GlSprintf },
|
||||
|
@ -43,9 +50,26 @@ describe('UserLimitNotification', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when close to limit with a personal namepace', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(true, false, { membersCount: 3, userNamespace: true });
|
||||
});
|
||||
|
||||
it('renders the limit for a personal namespace', () => {
|
||||
const alert = findAlert();
|
||||
|
||||
expect(alert.attributes('title')).toEqual(
|
||||
'You only have space for 2 more members in your personal projects',
|
||||
);
|
||||
expect(alert.text()).toEqual(
|
||||
'To make more space, you can remove members who no longer need access.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when close to limit', () => {
|
||||
it("renders user's limit notification", () => {
|
||||
createComponent(false, { membersCount: 3 });
|
||||
createComponent(true, false, { membersCount: 3 });
|
||||
|
||||
const alert = findAlert();
|
||||
|
||||
|
@ -61,7 +85,7 @@ describe('UserLimitNotification', () => {
|
|||
|
||||
describe('when limit is reached', () => {
|
||||
it("renders user's limit notification", () => {
|
||||
createComponent(true);
|
||||
createComponent(true, true);
|
||||
|
||||
const alert = findAlert();
|
||||
|
||||
|
@ -71,12 +95,12 @@ describe('UserLimitNotification', () => {
|
|||
|
||||
describe('when free user namespace', () => {
|
||||
it("renders user's limit notification", () => {
|
||||
createComponent(true, { userNamespace: true });
|
||||
createComponent(true, true, { userNamespace: true });
|
||||
|
||||
const alert = findAlert();
|
||||
|
||||
expect(alert.attributes('title')).toEqual(
|
||||
"You've reached your 5 members limit for my group",
|
||||
"You've reached your 5 members limit for your personal projects",
|
||||
);
|
||||
|
||||
expect(alert.text()).toEqual(REACHED_LIMIT_MESSAGE);
|
||||
|
|
|
@ -373,16 +373,16 @@ describe('Linked pipeline', () => {
|
|||
|
||||
describe('expand button', () => {
|
||||
it.each`
|
||||
pipelineType | anglePosition | buttonBorderClasses | expanded
|
||||
${downstreamProps} | ${'angle-right'} | ${'gl-border-l-0!'} | ${false}
|
||||
${downstreamProps} | ${'angle-left'} | ${'gl-border-l-0!'} | ${true}
|
||||
${upstreamProps} | ${'angle-left'} | ${'gl-border-r-0!'} | ${false}
|
||||
${upstreamProps} | ${'angle-right'} | ${'gl-border-r-0!'} | ${true}
|
||||
pipelineType | chevronPosition | buttonBorderClasses | expanded
|
||||
${downstreamProps} | ${'chevron-lg-right'} | ${'gl-border-l-0!'} | ${false}
|
||||
${downstreamProps} | ${'chevron-lg-left'} | ${'gl-border-l-0!'} | ${true}
|
||||
${upstreamProps} | ${'chevron-lg-left'} | ${'gl-border-r-0!'} | ${false}
|
||||
${upstreamProps} | ${'chevron-lg-right'} | ${'gl-border-r-0!'} | ${true}
|
||||
`(
|
||||
'$pipelineType.columnTitle pipeline button icon should be $anglePosition with $buttonBorderClasses if expanded state is $expanded',
|
||||
({ pipelineType, anglePosition, buttonBorderClasses, expanded }) => {
|
||||
'$pipelineType.columnTitle pipeline button icon should be $chevronPosition with $buttonBorderClasses if expanded state is $expanded',
|
||||
({ pipelineType, chevronPosition, buttonBorderClasses, expanded }) => {
|
||||
createWrapper({ propsData: { ...pipelineType, expanded } });
|
||||
expect(findExpandButton().props('icon')).toBe(anglePosition);
|
||||
expect(findExpandButton().props('icon')).toBe(chevronPosition);
|
||||
expect(findExpandButton().classes()).toContain(buttonBorderClasses);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe API::Entities::Ci::JobRequest::Image do
|
||||
let(:ports) { [{ number: 80, protocol: 'http', name: 'name' }]}
|
||||
let(:image) { double(name: 'image_name', entrypoint: ['foo'], ports: ports)}
|
||||
let(:image) { double(name: 'image_name', entrypoint: ['foo'], ports: ports, pull_policy: ['if-not-present']) }
|
||||
let(:entity) { described_class.new(image) }
|
||||
|
||||
subject { entity.as_json }
|
||||
|
@ -28,4 +28,18 @@ RSpec.describe API::Entities::Ci::JobRequest::Image do
|
|||
expect(subject[:ports]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns the pull policy' do
|
||||
expect(subject[:pull_policy]).to eq(['if-not-present'])
|
||||
end
|
||||
|
||||
context 'when the FF ci_docker_image_pull_policy is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_docker_image_pull_policy: false)
|
||||
end
|
||||
|
||||
it 'does not return the pull policy' do
|
||||
expect(subject).not_to have_key(:pull_policy)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,8 +28,14 @@ RSpec.describe Gitlab::Ci::Build::Image do
|
|||
|
||||
context 'when image is defined as hash' do
|
||||
let(:entrypoint) { '/bin/sh' }
|
||||
let(:pull_policy) { %w[always if-not-present] }
|
||||
|
||||
let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint, ports: [80] } } ) }
|
||||
let(:job) do
|
||||
create(:ci_build, options: { image: { name: image_name,
|
||||
entrypoint: entrypoint,
|
||||
ports: [80],
|
||||
pull_policy: pull_policy } } )
|
||||
end
|
||||
|
||||
it 'fabricates an object of the proper class' do
|
||||
is_expected.to be_kind_of(described_class)
|
||||
|
@ -38,6 +44,7 @@ RSpec.describe Gitlab::Ci::Build::Image do
|
|||
it 'populates fabricated object with the proper attributes' do
|
||||
expect(subject.name).to eq(image_name)
|
||||
expect(subject.entrypoint).to eq(entrypoint)
|
||||
expect(subject.pull_policy).to eq(pull_policy)
|
||||
end
|
||||
|
||||
it 'populates the ports' do
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'fast_spec_helper'
|
||||
require 'support/helpers/stubbed_feature'
|
||||
require 'support/helpers/stub_feature_flags'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Config::Entry::Image do
|
||||
include StubFeatureFlags
|
||||
|
||||
before do
|
||||
stub_feature_flags(ci_docker_image_pull_policy: true)
|
||||
end
|
||||
|
||||
let(:entry) { described_class.new(config) }
|
||||
|
||||
context 'when configuration is a string' do
|
||||
|
@ -43,6 +51,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
|
|||
expect(entry.ports).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#pull_policy' do
|
||||
it "returns nil" do
|
||||
expect(entry.pull_policy).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when configuration is a hash' do
|
||||
|
@ -109,6 +123,56 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when configuration has pull_policy' do
|
||||
let(:config) { { name: 'image:1.0', pull_policy: 'if-not-present' } }
|
||||
|
||||
describe '#valid?' do
|
||||
it 'is valid' do
|
||||
entry.compose!
|
||||
|
||||
expect(entry).to be_valid
|
||||
end
|
||||
|
||||
context 'when the feature flag ci_docker_image_pull_policy is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_docker_image_pull_policy: false)
|
||||
end
|
||||
|
||||
it 'is not valid' do
|
||||
entry.compose!
|
||||
|
||||
expect(entry).not_to be_valid
|
||||
expect(entry.errors).to include('image config contains unknown keys: pull_policy')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#value' do
|
||||
it "returns value" do
|
||||
entry.compose!
|
||||
|
||||
expect(entry.value).to eq(
|
||||
name: 'image:1.0',
|
||||
pull_policy: ['if-not-present']
|
||||
)
|
||||
end
|
||||
|
||||
context 'when the feature flag ci_docker_image_pull_policy is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_docker_image_pull_policy: false)
|
||||
end
|
||||
|
||||
it 'is not valid' do
|
||||
entry.compose!
|
||||
|
||||
expect(entry.value).to eq(
|
||||
name: 'image:1.0'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when entry value is not correct' do
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Config::Entry::PullPolicy do
|
||||
let(:entry) { described_class.new(config) }
|
||||
|
||||
describe '#value' do
|
||||
subject(:value) { entry.value }
|
||||
|
||||
context 'when config value is nil' do
|
||||
let(:config) { nil }
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when retry value is an empty array' do
|
||||
let(:config) { [] }
|
||||
|
||||
it { is_expected.to eq(nil) }
|
||||
end
|
||||
|
||||
context 'when retry value is string' do
|
||||
let(:config) { "always" }
|
||||
|
||||
it { is_expected.to eq(%w[always]) }
|
||||
end
|
||||
|
||||
context 'when retry value is array' do
|
||||
let(:config) { %w[always if-not-present] }
|
||||
|
||||
it { is_expected.to eq(%w[always if-not-present]) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validation' do
|
||||
subject(:valid?) { entry.valid? }
|
||||
|
||||
context 'when retry value is nil' do
|
||||
let(:config) { nil }
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
end
|
||||
|
||||
context 'when retry value is an empty array' do
|
||||
let(:config) { [] }
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
end
|
||||
|
||||
context 'when retry value is a hash' do
|
||||
let(:config) { {} }
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
end
|
||||
|
||||
context 'when retry value is string' do
|
||||
let(:config) { "always" }
|
||||
|
||||
it { is_expected.to eq(true) }
|
||||
|
||||
context 'when it is an invalid policy' do
|
||||
let(:config) { "invalid" }
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
end
|
||||
|
||||
context 'when it is an empty string' do
|
||||
let(:config) { "" }
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when retry value is array' do
|
||||
let(:config) { %w[always if-not-present] }
|
||||
|
||||
it { is_expected.to eq(true) }
|
||||
|
||||
context 'when config contains an invalid policy' do
|
||||
let(:config) { %w[always invalid] }
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,7 +7,7 @@ module Gitlab
|
|||
RSpec.describe YamlProcessor do
|
||||
include StubRequests
|
||||
|
||||
subject { described_class.new(config, user: nil).execute }
|
||||
subject(:processor) { described_class.new(config, user: nil).execute }
|
||||
|
||||
shared_examples 'returns errors' do |error_message|
|
||||
it 'adds a message when an error is encountered' do
|
||||
|
@ -965,6 +965,51 @@ module Gitlab
|
|||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when image has pull_policy' do
|
||||
let(:config) do
|
||||
<<~YAML
|
||||
image:
|
||||
name: ruby:2.7
|
||||
pull_policy: if-not-present
|
||||
|
||||
test:
|
||||
script: exit 0
|
||||
YAML
|
||||
end
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
|
||||
it "returns image and service when defined" do
|
||||
expect(processor.stage_builds_attributes("test")).to contain_exactly({
|
||||
stage: "test",
|
||||
stage_idx: 2,
|
||||
name: "test",
|
||||
only: { refs: %w[branches tags] },
|
||||
options: {
|
||||
script: ["exit 0"],
|
||||
image: { name: "ruby:2.7", pull_policy: ["if-not-present"] }
|
||||
},
|
||||
allow_failure: false,
|
||||
when: "on_success",
|
||||
job_variables: [],
|
||||
root_variables_inheritance: true,
|
||||
scheduling_type: :stage
|
||||
})
|
||||
end
|
||||
|
||||
context 'when the feature flag ci_docker_image_pull_policy is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_docker_image_pull_policy: false)
|
||||
end
|
||||
|
||||
it { is_expected.not_to be_valid }
|
||||
|
||||
it "returns no job" do
|
||||
expect(processor.jobs).to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Variables' do
|
||||
|
|
|
@ -45,6 +45,18 @@ RSpec.describe Gitlab::ImportExport::LfsSaver do
|
|||
expect(File).to exist("#{shared.export_path}/lfs-objects/#{lfs_object.oid}")
|
||||
end
|
||||
|
||||
context 'when lfs object has file on disk missing' do
|
||||
it 'does not attempt to copy non-existent file' do
|
||||
FileUtils.rm(lfs_object.file.path)
|
||||
expect(saver).not_to receive(:copy_files)
|
||||
|
||||
saver.save # rubocop:disable Rails/SaveBang
|
||||
|
||||
expect(shared.errors).to be_empty
|
||||
expect(File).not_to exist("#{shared.export_path}/lfs-objects/#{lfs_object.oid}")
|
||||
end
|
||||
end
|
||||
|
||||
describe 'saving a json file' do
|
||||
before do
|
||||
# Create two more LfsObjectProject records with different `repository_type`s
|
||||
|
|
|
@ -31,7 +31,7 @@ RSpec.describe Limitable do
|
|||
it 'triggers scoped validations' do
|
||||
instance = MinimalTestClass.new
|
||||
|
||||
expect(instance).to receive(:validate_scoped_plan_limit_not_exceeded)
|
||||
expect(instance).to receive(:scoped_plan_limits)
|
||||
|
||||
instance.valid?(:create)
|
||||
end
|
||||
|
@ -94,7 +94,7 @@ RSpec.describe Limitable do
|
|||
it 'triggers scoped validations' do
|
||||
instance = MinimalTestClass.new
|
||||
|
||||
expect(instance).to receive(:validate_global_plan_limit_not_exceeded)
|
||||
expect(instance).to receive(:global_plan_limits)
|
||||
|
||||
instance.valid?(:create)
|
||||
end
|
||||
|
|
|
@ -587,22 +587,10 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
|
|||
expect(action.user).to eq(user)
|
||||
end
|
||||
|
||||
context 'env_stopped_on_stop_success feature flag' do
|
||||
it 'environment is not stopped when flag is enabled' do
|
||||
stub_feature_flags(env_stopped_on_stop_success: true)
|
||||
it 'environment is not stopped' do
|
||||
subject
|
||||
|
||||
subject
|
||||
|
||||
expect(environment).not_to be_stopped
|
||||
end
|
||||
|
||||
it 'environment is stopped when flag is disabled' do
|
||||
stub_feature_flags(env_stopped_on_stop_success: false)
|
||||
|
||||
subject
|
||||
|
||||
expect(environment).to be_stopped
|
||||
end
|
||||
expect(environment).not_to be_stopped
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -215,6 +215,7 @@ RSpec.describe PlanLimits do
|
|||
web_hook_calls
|
||||
ci_daily_pipeline_schedule_triggers
|
||||
repository_size
|
||||
security_policy_scan_execution_schedules
|
||||
] + disabled_max_artifact_size_columns
|
||||
end
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ RSpec.describe ProjectPolicy do
|
|||
end
|
||||
end
|
||||
|
||||
it 'does not include the read_issue permission when the issue author is not a member of the private project' do
|
||||
it 'does not include the read permissions when the issue author is not a member of the private project' do
|
||||
project = create(:project, :private)
|
||||
issue = create(:issue, project: project, author: create(:user))
|
||||
user = issue.author
|
||||
|
@ -40,6 +40,7 @@ RSpec.describe ProjectPolicy do
|
|||
expect(project.team.member?(issue.author)).to be false
|
||||
|
||||
expect(Ability).not_to be_allowed(user, :read_issue, project)
|
||||
expect(Ability).not_to be_allowed(user, :read_work_item, project)
|
||||
end
|
||||
|
||||
it_behaves_like 'model with wiki policies' do
|
||||
|
@ -61,7 +62,7 @@ RSpec.describe ProjectPolicy do
|
|||
end
|
||||
|
||||
it 'does not include the issues permissions' do
|
||||
expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task
|
||||
expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task, :read_work_item
|
||||
end
|
||||
|
||||
it 'disables boards and lists permissions' do
|
||||
|
@ -73,7 +74,7 @@ RSpec.describe ProjectPolicy do
|
|||
it 'does not include the issues permissions' do
|
||||
create(:jira_integration, project: project)
|
||||
|
||||
expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task
|
||||
expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task, :read_work_item
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -752,14 +753,14 @@ RSpec.describe ProjectPolicy do
|
|||
allow(project).to receive(:service_desk_enabled?).and_return(true)
|
||||
end
|
||||
|
||||
it { expect_allowed(:reporter_access, :create_note, :read_issue) }
|
||||
it { expect_allowed(:reporter_access, :create_note, :read_issue, :read_work_item) }
|
||||
|
||||
context 'when issues are protected members only' do
|
||||
before do
|
||||
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
|
||||
end
|
||||
|
||||
it { expect_allowed(:reporter_access, :create_note, :read_issue) }
|
||||
it { expect_allowed(:reporter_access, :create_note, :read_issue, :read_work_item) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -37,6 +37,12 @@ RSpec.describe WorkItemPolicy do
|
|||
let(:current_user) { guest_author }
|
||||
|
||||
it { is_expected.to be_allowed(:read_work_item) }
|
||||
|
||||
context 'when work_item is confidential' do
|
||||
let(:work_item_subject) { create(:work_item, confidential: true, project: project) }
|
||||
|
||||
it { is_expected.not_to be_allowed(:read_work_item) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -216,7 +216,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
expect(json_response['token']).to eq(job.token)
|
||||
expect(json_response['job_info']).to eq(expected_job_info)
|
||||
expect(json_response['git_info']).to eq(expected_git_info)
|
||||
expect(json_response['image']).to eq({ 'name' => 'image:1.0', 'entrypoint' => '/bin/sh', 'ports' => [] })
|
||||
expect(json_response['image']).to eq({ 'name' => 'image:1.0', 'entrypoint' => '/bin/sh', 'ports' => [], 'pull_policy' => nil })
|
||||
expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil,
|
||||
'alias' => nil, 'command' => nil, 'ports' => [], 'variables' => nil },
|
||||
{ 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh',
|
||||
|
@ -810,6 +810,45 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when image has pull_policy' do
|
||||
let(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) }
|
||||
|
||||
let(:options) do
|
||||
{
|
||||
image: {
|
||||
name: 'ruby',
|
||||
pull_policy: ['if-not-present']
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns the image with pull policy' do
|
||||
request_job
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response).to include(
|
||||
'id' => job.id,
|
||||
'image' => { 'name' => 'ruby', 'pull_policy' => ['if-not-present'], 'entrypoint' => nil, 'ports' => [] }
|
||||
)
|
||||
end
|
||||
|
||||
context 'when the FF ci_docker_image_pull_policy is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_docker_image_pull_policy: false)
|
||||
end
|
||||
|
||||
it 'returns the image without pull policy' do
|
||||
request_job
|
||||
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
expect(json_response).to include(
|
||||
'id' => job.id,
|
||||
'image' => { 'name' => 'ruby', 'entrypoint' => nil, 'ports' => [] }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a job with excluded artifacts' do
|
||||
context 'when excluded paths are defined' do
|
||||
let(:job) do
|
||||
|
|
|
@ -53,6 +53,18 @@ RSpec.describe BulkImports::LfsObjectsExportService do
|
|||
)
|
||||
end
|
||||
|
||||
context 'when lfs object has file on disk missing' do
|
||||
it 'does not attempt to copy non-existent file' do
|
||||
FileUtils.rm(lfs_object.file.path)
|
||||
|
||||
expect(service).not_to receive(:copy_files)
|
||||
|
||||
service.execute
|
||||
|
||||
expect(File).not_to exist(File.join(export_path, lfs_object.oid))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when lfs object is remotely stored' do
|
||||
let(:lfs_object) { create(:lfs_object, :object_storage) }
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ RSpec.shared_context 'IssuesFinder context' do
|
|||
let_it_be(:milestone) { create(:milestone, project: project1, releases: [release]) }
|
||||
let_it_be(:label) { create(:label, project: project2) }
|
||||
let_it_be(:label2) { create(:label, project: project2) }
|
||||
let_it_be(:issue1, reload: true) do
|
||||
let_it_be(:item1, reload: true) do
|
||||
create(:issue,
|
||||
author: user,
|
||||
assignees: [user],
|
||||
|
@ -23,7 +23,7 @@ RSpec.shared_context 'IssuesFinder context' do
|
|||
updated_at: 1.week.ago)
|
||||
end
|
||||
|
||||
let_it_be(:issue2, reload: true) do
|
||||
let_it_be(:item2, reload: true) do
|
||||
create(:issue,
|
||||
author: user,
|
||||
assignees: [user],
|
||||
|
@ -33,7 +33,7 @@ RSpec.shared_context 'IssuesFinder context' do
|
|||
updated_at: 1.week.from_now)
|
||||
end
|
||||
|
||||
let_it_be(:issue3, reload: true) do
|
||||
let_it_be(:item3, reload: true) do
|
||||
create(:issue,
|
||||
author: user2,
|
||||
assignees: [user2],
|
||||
|
@ -44,8 +44,8 @@ RSpec.shared_context 'IssuesFinder context' do
|
|||
updated_at: 2.weeks.from_now)
|
||||
end
|
||||
|
||||
let_it_be(:issue4, reload: true) { create(:issue, project: project3) }
|
||||
let_it_be(:issue5, reload: true) do
|
||||
let_it_be(:item4, reload: true) { create(:issue, project: project3) }
|
||||
let_it_be(:item5, reload: true) do
|
||||
create(:issue,
|
||||
author: user,
|
||||
assignees: [user],
|
||||
|
@ -55,18 +55,20 @@ RSpec.shared_context 'IssuesFinder context' do
|
|||
updated_at: 3.days.ago)
|
||||
end
|
||||
|
||||
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) }
|
||||
let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) }
|
||||
let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) }
|
||||
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: item1) }
|
||||
let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: item2) }
|
||||
let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: item3) }
|
||||
|
||||
let(:items_model) { Issue }
|
||||
end
|
||||
|
||||
RSpec.shared_context 'IssuesFinder#execute context' do
|
||||
let!(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') }
|
||||
let!(:label_link) { create(:label_link, label: label, target: issue2) }
|
||||
let!(:label_link2) { create(:label_link, label: label2, target: issue3) }
|
||||
let!(:closed_item) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') }
|
||||
let!(:label_link) { create(:label_link, label: label, target: item2) }
|
||||
let!(:label_link2) { create(:label_link, label: label2, target: item3) }
|
||||
let(:search_user) { user }
|
||||
let(:params) { {} }
|
||||
let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
|
||||
let(:items) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
|
||||
|
||||
before_all do
|
||||
project1.add_maintainer(user)
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_context 'WorkItemsFinder context' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:user2) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||
let_it_be(:project1, reload: true) { create(:project, group: group) }
|
||||
let_it_be(:project2, reload: true) { create(:project) }
|
||||
let_it_be(:project3, reload: true) { create(:project, group: subgroup) }
|
||||
let_it_be(:release) { create(:release, project: project1, tag: 'v1.0.0') }
|
||||
let_it_be(:milestone) { create(:milestone, project: project1, releases: [release]) }
|
||||
let_it_be(:label) { create(:label, project: project2) }
|
||||
let_it_be(:label2) { create(:label, project: project2) }
|
||||
let_it_be(:item1, reload: true) do
|
||||
create(:work_item,
|
||||
author: user,
|
||||
assignees: [user],
|
||||
project: project1,
|
||||
milestone: milestone,
|
||||
title: 'gitlab',
|
||||
created_at: 1.week.ago,
|
||||
updated_at: 1.week.ago)
|
||||
end
|
||||
|
||||
let_it_be(:item2, reload: true) do
|
||||
create(:work_item,
|
||||
author: user,
|
||||
assignees: [user],
|
||||
project: project2,
|
||||
description: 'gitlab',
|
||||
created_at: 1.week.from_now,
|
||||
updated_at: 1.week.from_now)
|
||||
end
|
||||
|
||||
let_it_be(:item3, reload: true) do
|
||||
create(:work_item,
|
||||
author: user2,
|
||||
assignees: [user2],
|
||||
project: project2,
|
||||
title: 'tanuki',
|
||||
description: 'tanuki',
|
||||
created_at: 2.weeks.from_now,
|
||||
updated_at: 2.weeks.from_now)
|
||||
end
|
||||
|
||||
let_it_be(:item4, reload: true) { create(:work_item, project: project3) }
|
||||
let_it_be(:item5, reload: true) do
|
||||
create(:work_item,
|
||||
author: user,
|
||||
assignees: [user],
|
||||
project: project1,
|
||||
title: 'wotnot',
|
||||
created_at: 3.days.ago,
|
||||
updated_at: 3.days.ago)
|
||||
end
|
||||
|
||||
let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: item1) }
|
||||
let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: item2) }
|
||||
let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: item3) }
|
||||
|
||||
let(:items_model) { WorkItem }
|
||||
end
|
||||
|
||||
RSpec.shared_context 'WorkItemsFinder#execute context' do
|
||||
let!(:closed_item) { create(:work_item, author: user2, assignees: [user2], project: project2, state: 'closed') }
|
||||
let!(:label_link) { create(:label_link, label: label, target: item2) }
|
||||
let!(:label_link2) { create(:label_link, label: label2, target: item3) }
|
||||
let(:search_user) { user }
|
||||
let(:params) { {} }
|
||||
let(:items) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
|
||||
|
||||
before_all do
|
||||
project1.add_maintainer(user)
|
||||
project2.add_developer(user)
|
||||
project2.add_developer(user2)
|
||||
project3.add_developer(user)
|
||||
end
|
||||
end
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'includes Limitable concern' do
|
||||
describe '#exceeds_limits?' do
|
||||
let(:plan_limits) { create(:plan_limits, :default_plan) }
|
||||
|
||||
context 'without plan limits configured' do
|
||||
it { expect(subject.exceeds_limits?).to eq false }
|
||||
end
|
||||
|
||||
context 'without plan limits configured' do
|
||||
before do
|
||||
plan_limits.update!(subject.class.limit_name => 1)
|
||||
end
|
||||
|
||||
it { expect(subject.exceeds_limits?).to eq false }
|
||||
|
||||
context 'with an existing model' do
|
||||
before do
|
||||
subject.clone.save!
|
||||
end
|
||||
|
||||
it { expect(subject.exceeds_limits?).to eq true }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
let(:plan_limits) { create(:plan_limits, :default_plan) }
|
||||
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
require 'rake_helper'
|
||||
|
||||
RSpec.describe 'gitlab:db:validate_config', :silence_stdout do
|
||||
# We don't need to delete this data since it only modifies `ar_internal_metadata`
|
||||
# which would not be cleaned either by `DbCleaner`
|
||||
self.use_transactional_tests = false
|
||||
|
||||
before :all do
|
||||
Rake.application.rake_require 'active_record/railties/databases'
|
||||
Rake.application.rake_require 'tasks/seed_fu'
|
||||
|
@ -111,6 +115,26 @@ RSpec.describe 'gitlab:db:validate_config', :silence_stdout do
|
|||
end
|
||||
|
||||
it_behaves_like 'validates successfully'
|
||||
|
||||
context 'when config is pointing to incorrect server' do
|
||||
let(:test_config) do
|
||||
{
|
||||
main: main_database_config.merge(port: 11235)
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'validates successfully'
|
||||
end
|
||||
|
||||
context 'when config is pointing to non-existent database' do
|
||||
let(:test_config) do
|
||||
{
|
||||
main: main_database_config.merge(database: 'non_existent_database')
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'validates successfully'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when main: uses database_tasks=false' do
|
||||
|
|
|
@ -28,22 +28,10 @@ RSpec.describe BuildSuccessWorker do
|
|||
it 'does not stop the environment' do
|
||||
expect(environment).to be_available
|
||||
|
||||
stub_feature_flags(env_stopped_on_stop_success: true)
|
||||
|
||||
subject
|
||||
|
||||
expect(environment.reload).not_to be_stopped
|
||||
end
|
||||
|
||||
it 'does stop the environment when feature flag is disabled' do
|
||||
expect(environment).to be_available
|
||||
|
||||
stub_feature_flags(env_stopped_on_stop_success: false)
|
||||
|
||||
subject
|
||||
|
||||
expect(environment.reload).to be_stopped
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue