Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6e91fbf774
commit
16515bdfcb
174 changed files with 2308 additions and 349 deletions
2
Gemfile
2
Gemfile
|
@ -172,7 +172,7 @@ gem 'diffy', '~> 3.3'
|
|||
gem 'diff_match_patch', '~> 0.1.0'
|
||||
|
||||
# Application server
|
||||
gem 'rack', '~> 2.0.9'
|
||||
gem 'rack', '~> 2.1.4'
|
||||
# https://github.com/sharpstone/rack-timeout/blob/master/README.md#rails-apps-manually
|
||||
gem 'rack-timeout', '~> 0.5.1', require: 'rack/timeout/base'
|
||||
|
||||
|
|
|
@ -852,7 +852,7 @@ GEM
|
|||
public_suffix (4.0.3)
|
||||
pyu-ruby-sasl (0.0.3.3)
|
||||
raabro (1.1.6)
|
||||
rack (2.0.9)
|
||||
rack (2.1.4)
|
||||
rack-accept (0.4.5)
|
||||
rack (>= 0.4)
|
||||
rack-attack (6.3.0)
|
||||
|
@ -1423,7 +1423,7 @@ DEPENDENCIES
|
|||
prometheus-client-mmap (~> 0.12.0)
|
||||
pry-byebug (~> 3.9.0)
|
||||
pry-rails (~> 0.3.9)
|
||||
rack (~> 2.0.9)
|
||||
rack (~> 2.1.4)
|
||||
rack-attack (~> 6.3.0)
|
||||
rack-cors (~> 1.0.6)
|
||||
rack-oauth2 (~> 1.9.3)
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
<script>
|
||||
import { GlFormInput, GlFormSelect } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { PERCENT_ROLLOUT_GROUP_ID } from '../../constants';
|
||||
import ParameterFormGroup from './parameter_form_group.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlFormInput,
|
||||
GlFormSelect,
|
||||
ParameterFormGroup,
|
||||
},
|
||||
props: {
|
||||
strategy: {
|
||||
required: true,
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
percentageDescription: __('Enter a whole number between 0 and 100'),
|
||||
percentageInvalid: __('Percent rollout must be a whole number between 0 and 100'),
|
||||
percentageLabel: __('Percentage'),
|
||||
stickinessDescription: __('Consistency guarantee method'),
|
||||
stickinessLabel: __('Based on'),
|
||||
},
|
||||
stickinessOptions: [
|
||||
{
|
||||
value: 'DEFAULT',
|
||||
text: __('Available ID'),
|
||||
},
|
||||
{
|
||||
value: 'USERID',
|
||||
text: __('User ID'),
|
||||
},
|
||||
{
|
||||
value: 'SESSIONID',
|
||||
text: __('Session ID'),
|
||||
},
|
||||
{
|
||||
value: 'RANDOM',
|
||||
text: __('Random'),
|
||||
},
|
||||
],
|
||||
computed: {
|
||||
isValid() {
|
||||
const percentageNum = Number(this.percentage);
|
||||
return Number.isInteger(percentageNum) && percentageNum >= 0 && percentageNum <= 100;
|
||||
},
|
||||
percentage() {
|
||||
return this.strategy?.parameters?.rollout ?? '100';
|
||||
},
|
||||
stickiness() {
|
||||
return this.strategy?.parameters?.stickiness ?? this.$options.stickinessOptions[0].value;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onPercentageChange(value) {
|
||||
this.$emit('change', {
|
||||
parameters: {
|
||||
groupId: PERCENT_ROLLOUT_GROUP_ID,
|
||||
rollout: value,
|
||||
stickiness: this.stickiness,
|
||||
},
|
||||
});
|
||||
},
|
||||
onStickinessChange(value) {
|
||||
this.$emit('change', {
|
||||
parameters: {
|
||||
groupId: PERCENT_ROLLOUT_GROUP_ID,
|
||||
rollout: this.percentage,
|
||||
stickiness: value,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="gl-display-flex">
|
||||
<div class="gl-mr-7" data-testid="strategy-flexible-rollout-percentage">
|
||||
<parameter-form-group
|
||||
:label="$options.i18n.percentageLabel"
|
||||
:description="isValid ? $options.i18n.percentageDescription : ''"
|
||||
:invalid-feedback="$options.i18n.percentageInvalid"
|
||||
:state="isValid"
|
||||
>
|
||||
<template #default="{ inputId }">
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<gl-form-input
|
||||
:id="inputId"
|
||||
:value="percentage"
|
||||
:state="isValid"
|
||||
class="rollout-percentage gl-text-right gl-w-9"
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
@input="onPercentageChange"
|
||||
/>
|
||||
<span class="ml-1">%</span>
|
||||
</div>
|
||||
</template>
|
||||
</parameter-form-group>
|
||||
</div>
|
||||
|
||||
<div class="gl-mr-7" data-testid="strategy-flexible-rollout-stickiness">
|
||||
<parameter-form-group
|
||||
:label="$options.i18n.stickinessLabel"
|
||||
:description="$options.i18n.stickinessDescription"
|
||||
>
|
||||
<template #default="{ inputId }">
|
||||
<gl-form-select
|
||||
:id="inputId"
|
||||
:value="stickiness"
|
||||
:options="$options.stickinessOptions"
|
||||
@change="onStickinessChange"
|
||||
/>
|
||||
</template>
|
||||
</parameter-form-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -49,7 +49,7 @@ export default {
|
|||
:state="hasUserLists"
|
||||
:invalid-feedback="$options.translations.rolloutUserListNoListError"
|
||||
:label="$options.translations.rolloutUserListLabel"
|
||||
:description="$options.translations.rolloutUserListDescription"
|
||||
:description="hasUserLists ? $options.translations.rolloutUserListDescription : ''"
|
||||
>
|
||||
<template #default="{ inputId }">
|
||||
<gl-form-select
|
||||
|
|
|
@ -15,7 +15,7 @@ export default {
|
|||
type: Object,
|
||||
},
|
||||
},
|
||||
translations: {
|
||||
i18n: {
|
||||
rolloutPercentageDescription: __('Enter a whole number between 0 and 100'),
|
||||
rolloutPercentageInvalid: s__(
|
||||
'FeatureFlags|Percent rollout must be a whole number between 0 and 100',
|
||||
|
@ -24,10 +24,11 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
isValid() {
|
||||
return Number(this.percentage) >= 0 && Number(this.percentage) <= 100;
|
||||
const percentageNum = Number(this.percentage);
|
||||
return Number.isInteger(percentageNum) && percentageNum >= 0 && percentageNum <= 100;
|
||||
},
|
||||
percentage() {
|
||||
return this.strategy?.parameters?.percentage ?? '';
|
||||
return this.strategy?.parameters?.percentage ?? '100';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -44,9 +45,9 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<parameter-form-group
|
||||
:label="$options.translations.rolloutPercentageLabel"
|
||||
:description="$options.translations.rolloutPercentageDescription"
|
||||
:invalid-feedback="$options.translations.rolloutPercentageInvalid"
|
||||
:label="$options.i18n.rolloutPercentageLabel"
|
||||
:description="isValid ? $options.i18n.rolloutPercentageDescription : ''"
|
||||
:invalid-feedback="$options.i18n.rolloutPercentageInvalid"
|
||||
:state="isValid"
|
||||
>
|
||||
<template #default="{ inputId }">
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
<script>
|
||||
import Vue from 'vue';
|
||||
import { isNumber } from 'lodash';
|
||||
import { GlButton, GlFormSelect, GlFormGroup, GlIcon, GlLink, GlToken } from '@gitlab/ui';
|
||||
import { GlAlert, GlButton, GlFormSelect, GlFormGroup, GlIcon, GlLink, GlToken } from '@gitlab/ui';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { EMPTY_PARAMETERS, STRATEGY_SELECTIONS } from '../constants';
|
||||
import {
|
||||
EMPTY_PARAMETERS,
|
||||
STRATEGY_SELECTIONS,
|
||||
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
|
||||
} from '../constants';
|
||||
|
||||
import NewEnvironmentsDropdown from './new_environments_dropdown.vue';
|
||||
import StrategyParameters from './strategy_parameters.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlFormGroup,
|
||||
GlFormSelect,
|
||||
|
@ -51,13 +56,13 @@ export default {
|
|||
i18n: {
|
||||
allEnvironments: __('All environments'),
|
||||
environmentsLabel: __('Environments'),
|
||||
rolloutUserListLabel: s__('FeatureFlag|List'),
|
||||
rolloutUserListDescription: s__('FeatureFlag|Select a user list'),
|
||||
rolloutUserListNoListError: s__('FeatureFlag|There are no configured user lists'),
|
||||
strategyTypeDescription: __('Select strategy activation method.'),
|
||||
strategyTypeDescription: __('Select strategy activation method'),
|
||||
strategyTypeLabel: s__('FeatureFlag|Type'),
|
||||
environmentsSelectDescription: s__(
|
||||
'FeatureFlag|Select the environment scope for this feature flag.',
|
||||
'FeatureFlag|Select the environment scope for this feature flag',
|
||||
),
|
||||
considerFlexibleRollout: s__(
|
||||
'FeatureFlags|Consider using the more flexible "Percent rollout" strategy instead.',
|
||||
),
|
||||
},
|
||||
|
||||
|
@ -85,6 +90,9 @@ export default {
|
|||
filteredEnvironments() {
|
||||
return this.environments.filter(e => !e.shouldBeDestroyed);
|
||||
},
|
||||
isPercentUserRollout() {
|
||||
return this.formStrategy.name === ROLLOUT_STRATEGY_PERCENT_ROLLOUT;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addEnvironment(environment) {
|
||||
|
@ -121,73 +129,84 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-py-6">
|
||||
<div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row flex-md-wrap">
|
||||
<div class="mr-5">
|
||||
<gl-form-group :label="$options.i18n.strategyTypeLabel" :label-for="strategyTypeId">
|
||||
<p class="gl-display-inline-block ">{{ $options.i18n.strategyTypeDescription }}</p>
|
||||
<gl-link :href="strategyTypeDocsPagePath" target="_blank">
|
||||
<gl-icon name="question" />
|
||||
</gl-link>
|
||||
<gl-form-select
|
||||
:id="strategyTypeId"
|
||||
:value="formStrategy.name"
|
||||
:options="$options.strategies"
|
||||
@change="onStrategyTypeChange"
|
||||
<div>
|
||||
<gl-alert v-if="isPercentUserRollout" variant="tip" :dismissible="false">
|
||||
{{ $options.i18n.considerFlexibleRollout }}
|
||||
</gl-alert>
|
||||
|
||||
<div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-py-6">
|
||||
<div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row flex-md-wrap">
|
||||
<div class="mr-5">
|
||||
<gl-form-group :label="$options.i18n.strategyTypeLabel" :label-for="strategyTypeId">
|
||||
<template #description>
|
||||
{{ $options.i18n.strategyTypeDescription }}
|
||||
<gl-link :href="strategyTypeDocsPagePath" target="_blank">
|
||||
<gl-icon name="question" />
|
||||
</gl-link>
|
||||
</template>
|
||||
<gl-form-select
|
||||
:id="strategyTypeId"
|
||||
:value="formStrategy.name"
|
||||
:options="$options.strategies"
|
||||
@change="onStrategyTypeChange"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</div>
|
||||
|
||||
<div data-testid="strategy">
|
||||
<strategy-parameters
|
||||
:strategy="strategy"
|
||||
:user-lists="userLists"
|
||||
@change="onStrategyChange"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-testid="strategy">
|
||||
<strategy-parameters
|
||||
:strategy="strategy"
|
||||
:user-lists="userLists"
|
||||
@change="onStrategyChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="align-self-end align-self-md-stretch order-first offset-md-0 order-md-0 gl-ml-auto"
|
||||
>
|
||||
<gl-button
|
||||
data-testid="delete-strategy-button"
|
||||
variant="danger"
|
||||
icon="remove"
|
||||
@click="$emit('delete')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label class="gl-display-block" :for="environmentsDropdownId">{{
|
||||
$options.i18n.environmentsLabel
|
||||
}}</label>
|
||||
<p class="gl-display-inline-block">{{ $options.i18n.environmentsSelectDescription }}</p>
|
||||
<gl-link :href="environmentsScopeDocsPath" target="_blank">
|
||||
<gl-icon name="question" />
|
||||
</gl-link>
|
||||
<div class="gl-display-flex gl-flex-direction-column">
|
||||
<div
|
||||
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row align-items-start gl-md-align-items-center"
|
||||
>
|
||||
<new-environments-dropdown
|
||||
:id="environmentsDropdownId"
|
||||
:endpoint="endpoint"
|
||||
class="gl-mr-3"
|
||||
@add="addEnvironment"
|
||||
/>
|
||||
<span v-if="appliesToAllEnvironments" class="text-secondary gl-mt-3 mt-md-0 ml-md-3">
|
||||
{{ $options.i18n.allEnvironments }}
|
||||
</span>
|
||||
<div v-else class="gl-display-flex gl-align-items-center">
|
||||
<gl-token
|
||||
v-for="environment in filteredEnvironments"
|
||||
:key="environment.id"
|
||||
class="gl-mt-3 gl-mr-3 mt-md-0 mr-md-0 ml-md-2 rounded-pill"
|
||||
@close="removeScope(environment)"
|
||||
>
|
||||
{{ environment.environmentScope }}
|
||||
</gl-token>
|
||||
<div
|
||||
class="align-self-end align-self-md-stretch order-first offset-md-0 order-md-0 gl-ml-auto"
|
||||
>
|
||||
<gl-button
|
||||
data-testid="delete-strategy-button"
|
||||
variant="danger"
|
||||
icon="remove"
|
||||
@click="$emit('delete')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="gl-display-block" :for="environmentsDropdownId">{{
|
||||
$options.i18n.environmentsLabel
|
||||
}}</label>
|
||||
<div class="gl-display-flex gl-flex-direction-column">
|
||||
<div
|
||||
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row align-items-start gl-md-align-items-center"
|
||||
>
|
||||
<new-environments-dropdown
|
||||
:id="environmentsDropdownId"
|
||||
:endpoint="endpoint"
|
||||
class="gl-mr-3"
|
||||
@add="addEnvironment"
|
||||
/>
|
||||
<span v-if="appliesToAllEnvironments" class="text-secondary gl-mt-3 mt-md-0 ml-md-3">
|
||||
{{ $options.i18n.allEnvironments }}
|
||||
</span>
|
||||
<div v-else class="gl-display-flex gl-align-items-center">
|
||||
<gl-token
|
||||
v-for="environment in filteredEnvironments"
|
||||
:key="environment.id"
|
||||
class="gl-mt-3 gl-mr-3 mt-md-0 mr-md-0 ml-md-2 rounded-pill"
|
||||
@close="removeScope(environment)"
|
||||
>
|
||||
{{ environment.environmentScope }}
|
||||
</gl-token>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="gl-display-inline-block gl-py-3">
|
||||
{{ $options.i18n.environmentsSelectDescription }}
|
||||
</span>
|
||||
<gl-link :href="environmentsScopeDocsPath" target="_blank">
|
||||
<gl-icon name="question" />
|
||||
</gl-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
<script>
|
||||
import {
|
||||
ROLLOUT_STRATEGY_ALL_USERS,
|
||||
ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
|
||||
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
|
||||
ROLLOUT_STRATEGY_USER_ID,
|
||||
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
|
||||
} from '../constants';
|
||||
|
||||
import Default from './strategies/default.vue';
|
||||
import FlexibleRollout from './strategies/flexible_rollout.vue';
|
||||
import PercentRollout from './strategies/percent_rollout.vue';
|
||||
import UsersWithId from './strategies/users_with_id.vue';
|
||||
import GitlabUserList from './strategies/gitlab_user_list.vue';
|
||||
|
||||
const STRATEGIES = Object.freeze({
|
||||
[ROLLOUT_STRATEGY_ALL_USERS]: Default,
|
||||
[ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT]: FlexibleRollout,
|
||||
[ROLLOUT_STRATEGY_PERCENT_ROLLOUT]: PercentRollout,
|
||||
[ROLLOUT_STRATEGY_USER_ID]: UsersWithId,
|
||||
[ROLLOUT_STRATEGY_GITLAB_USER_LIST]: GitlabUserList,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { s__ } from '~/locale';
|
|||
|
||||
export const ROLLOUT_STRATEGY_ALL_USERS = 'default';
|
||||
export const ROLLOUT_STRATEGY_PERCENT_ROLLOUT = 'gradualRolloutUserId';
|
||||
export const ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT = 'flexibleRollout';
|
||||
export const ROLLOUT_STRATEGY_USER_ID = 'userWithId';
|
||||
export const ROLLOUT_STRATEGY_GITLAB_USER_LIST = 'gitlabUserList';
|
||||
|
||||
|
@ -34,6 +35,10 @@ export const STRATEGY_SELECTIONS = [
|
|||
value: ROLLOUT_STRATEGY_ALL_USERS,
|
||||
text: s__('FeatureFlags|All users'),
|
||||
},
|
||||
{
|
||||
value: ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
|
||||
text: s__('FeatureFlags|Percent rollout'),
|
||||
},
|
||||
{
|
||||
value: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
|
||||
text: s__('FeatureFlags|Percent of users'),
|
||||
|
|
|
@ -2,6 +2,7 @@ import { s__, n__, sprintf } from '~/locale';
|
|||
import {
|
||||
ALL_ENVIRONMENTS_NAME,
|
||||
ROLLOUT_STRATEGY_ALL_USERS,
|
||||
ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
|
||||
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
|
||||
ROLLOUT_STRATEGY_USER_ID,
|
||||
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
|
||||
|
@ -12,6 +13,23 @@ const badgeTextByType = {
|
|||
name: s__('FeatureFlags|All Users'),
|
||||
parameters: null,
|
||||
},
|
||||
[ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT]: {
|
||||
name: s__('FeatureFlags|Percent rollout'),
|
||||
parameters: ({ parameters: { rollout, stickiness } }) => {
|
||||
switch (stickiness) {
|
||||
case 'USERID':
|
||||
return sprintf(s__('FeatureFlags|%{percent} by user ID'), { percent: `${rollout}%` });
|
||||
case 'SESSIONID':
|
||||
return sprintf(s__('FeatureFlags|%{percent} by session ID'), { percent: `${rollout}%` });
|
||||
case 'RANDOM':
|
||||
return sprintf(s__('FeatureFlags|%{percent} randomly'), { percent: `${rollout}%` });
|
||||
default:
|
||||
return sprintf(s__('FeatureFlags|%{percent} by available ID'), {
|
||||
percent: `${rollout}%`,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
[ROLLOUT_STRATEGY_PERCENT_ROLLOUT]: {
|
||||
name: s__('FeatureFlags|Percent of users'),
|
||||
parameters: ({ parameters: { percentage } }) => `${percentage}%`,
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue';
|
||||
import { featureAccessLevelNone } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
projectFeatureToggle,
|
||||
},
|
||||
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change',
|
||||
},
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
|
@ -34,7 +34,6 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
featureEnabled() {
|
||||
return this.value !== 0;
|
||||
|
@ -51,7 +50,6 @@ export default {
|
|||
return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleFeature(featureEnabled) {
|
||||
if (featureEnabled === false || this.options.length < 1) {
|
||||
|
@ -96,7 +94,11 @@ export default {
|
|||
{{ optionName }}
|
||||
</option>
|
||||
</select>
|
||||
<i aria-hidden="true" class="fa fa-chevron-down"> </i>
|
||||
<gl-icon
|
||||
name="chevron-down"
|
||||
aria-hidden="true"
|
||||
class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlSprintf, GlLink, GlFormCheckbox } from '@gitlab/ui';
|
||||
import { GlIcon, GlSprintf, GlLink, GlFormCheckbox } from '@gitlab/ui';
|
||||
|
||||
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
|
||||
import { s__ } from '~/locale';
|
||||
|
@ -22,6 +22,7 @@ export default {
|
|||
projectFeatureSetting,
|
||||
projectFeatureToggle,
|
||||
projectSettingRow,
|
||||
GlIcon,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
GlFormCheckbox,
|
||||
|
@ -325,7 +326,12 @@ export default {
|
|||
>{{ s__('ProjectSettings|Public') }}</option
|
||||
>
|
||||
</select>
|
||||
<i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
|
||||
<gl-icon
|
||||
name="chevron-down"
|
||||
aria-hidden="true"
|
||||
data-hidden="true"
|
||||
class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span class="form-text text-muted">{{ visibilityLevelDescription }}</span>
|
||||
|
@ -540,7 +546,12 @@ export default {
|
|||
>{{ featureAccessLevelEveryone[1] }}</option
|
||||
>
|
||||
</select>
|
||||
<i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
|
||||
<gl-icon
|
||||
name="chevron-down"
|
||||
aria-hidden="true"
|
||||
data-hidden="true"
|
||||
class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</project-setting-row>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import { difference, union } from 'lodash';
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import flash from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { __ } from '~/locale';
|
||||
|
@ -26,47 +25,49 @@ export default {
|
|||
'projectIssuesPath',
|
||||
'projectPath',
|
||||
],
|
||||
data: () => ({
|
||||
labelsSelectInProgress: false,
|
||||
}),
|
||||
computed: {
|
||||
...mapState(['selectedLabels']),
|
||||
},
|
||||
mounted() {
|
||||
this.setInitialState({
|
||||
data() {
|
||||
return {
|
||||
isLabelsSelectInProgress: false,
|
||||
selectedLabels: this.initiallySelectedLabels,
|
||||
});
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setInitialState', 'replaceSelectedLabels']),
|
||||
handleDropdownClose() {
|
||||
$(this.$el).trigger('hidden.gl.dropdown');
|
||||
},
|
||||
handleUpdateSelectedLabels(labels) {
|
||||
handleUpdateSelectedLabels(dropdownLabels) {
|
||||
const currentLabelIds = this.selectedLabels.map(label => label.id);
|
||||
const userAddedLabelIds = labels.filter(label => label.set).map(label => label.id);
|
||||
const userRemovedLabelIds = labels.filter(label => !label.set).map(label => label.id);
|
||||
const userAddedLabelIds = dropdownLabels.filter(label => label.set).map(label => label.id);
|
||||
const userRemovedLabelIds = dropdownLabels.filter(label => !label.set).map(label => label.id);
|
||||
|
||||
const issuableLabels = difference(
|
||||
union(currentLabelIds, userAddedLabelIds),
|
||||
userRemovedLabelIds,
|
||||
);
|
||||
const labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds);
|
||||
|
||||
this.labelsSelectInProgress = true;
|
||||
this.updateSelectedLabels(labelIds);
|
||||
},
|
||||
handleLabelRemove(labelId) {
|
||||
const currentLabelIds = this.selectedLabels.map(label => label.id);
|
||||
const labelIds = difference(currentLabelIds, [labelId]);
|
||||
|
||||
this.updateSelectedLabels(labelIds);
|
||||
},
|
||||
updateSelectedLabels(labelIds) {
|
||||
this.isLabelsSelectInProgress = true;
|
||||
|
||||
axios({
|
||||
data: {
|
||||
[this.issuableType]: {
|
||||
label_ids: issuableLabels,
|
||||
label_ids: labelIds,
|
||||
},
|
||||
},
|
||||
method: 'put',
|
||||
url: this.labelsUpdatePath,
|
||||
})
|
||||
.then(({ data }) => this.replaceSelectedLabels(data.labels))
|
||||
.then(({ data }) => {
|
||||
this.selectedLabels = data.labels;
|
||||
})
|
||||
.catch(() => flash(__('An error occurred while updating labels.')))
|
||||
.finally(() => {
|
||||
this.labelsSelectInProgress = false;
|
||||
this.isLabelsSelectInProgress = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
|
@ -76,6 +77,7 @@ export default {
|
|||
<template>
|
||||
<labels-select
|
||||
class="block labels js-labels-block"
|
||||
:allow-label-remove="true"
|
||||
:allow-label-create="allowLabelCreate"
|
||||
:allow-label-edit="allowLabelEdit"
|
||||
:allow-multiselect="true"
|
||||
|
@ -86,11 +88,12 @@ export default {
|
|||
:labels-fetch-path="labelsFetchPath"
|
||||
:labels-filter-base-path="projectIssuesPath"
|
||||
:labels-manage-path="labelsManagePath"
|
||||
:labels-select-in-progress="labelsSelectInProgress"
|
||||
:labels-select-in-progress="isLabelsSelectInProgress"
|
||||
:selected-labels="selectedLabels"
|
||||
:variant="$options.sidebar"
|
||||
data-qa-selector="labels_block"
|
||||
@onDropdownClose="handleDropdownClose"
|
||||
@onLabelRemove="handleLabelRemove"
|
||||
@updateSelectedLabels="handleUpdateSelectedLabels"
|
||||
>
|
||||
{{ __('None') }}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import Vuex from 'vuex';
|
||||
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
|
||||
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
|
||||
import SidebarLabels from './components/labels/sidebar_labels.vue';
|
||||
|
@ -17,11 +16,9 @@ import createDefaultClient from '~/lib/graphql';
|
|||
import { isInIssuePage, isInIncidentPage, parseBoolean } from '~/lib/utils/common_utils';
|
||||
import createFlash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
|
||||
|
||||
Vue.use(Translate);
|
||||
Vue.use(VueApollo);
|
||||
Vue.use(Vuex);
|
||||
|
||||
function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-options')) {
|
||||
return JSON.parse(sidebarOptEl.innerHTML);
|
||||
|
@ -94,8 +91,6 @@ export function mountSidebarLabels() {
|
|||
return false;
|
||||
}
|
||||
|
||||
const labelsStore = new Vuex.Store(labelsSelectModule());
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
|
@ -105,7 +100,6 @@ export function mountSidebarLabels() {
|
|||
allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels),
|
||||
initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels),
|
||||
},
|
||||
store: labelsStore,
|
||||
render: createElement => createElement(SidebarLabels),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,11 +8,13 @@ import {
|
|||
import { generateBadges } from 'ee_else_ce/vue_shared/components/members/utils';
|
||||
import { __ } from '~/locale';
|
||||
import { AVATAR_SIZE } from '../constants';
|
||||
import { glEmojiTag } from '~/emoji';
|
||||
|
||||
export default {
|
||||
name: 'UserAvatar',
|
||||
avatarSize: AVATAR_SIZE,
|
||||
orphanedUserLabel: __('Orphaned member'),
|
||||
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
|
||||
components: {
|
||||
GlAvatarLink,
|
||||
GlAvatarLabeled,
|
||||
|
@ -38,6 +40,12 @@ export default {
|
|||
badges() {
|
||||
return generateBadges(this.member, this.isCurrentUser).filter(badge => badge.show);
|
||||
},
|
||||
statusEmoji() {
|
||||
return this.user?.status?.emoji;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
glEmojiTag,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -60,6 +68,9 @@ export default {
|
|||
:entity-id="user.id"
|
||||
>
|
||||
<template #meta>
|
||||
<div v-if="statusEmoji" class="gl-p-1">
|
||||
<span v-safe-html:[$options.safeHtmlConfig]="glEmojiTag(statusEmoji)"></span>
|
||||
</div>
|
||||
<div v-for="badge in badges" :key="badge.text" class="gl-p-1">
|
||||
<gl-badge size="sm" :variant="badge.variant">
|
||||
{{ badge.text }}
|
||||
|
|
|
@ -38,8 +38,8 @@ export const FIELDS = [
|
|||
{
|
||||
key: 'maxRole',
|
||||
label: __('Max role'),
|
||||
thClass: 'col-meta',
|
||||
tdClass: 'col-meta',
|
||||
thClass: 'col-max-role',
|
||||
tdClass: 'col-max-role',
|
||||
},
|
||||
{
|
||||
key: 'expiration',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { GlTable } from '@gitlab/ui';
|
||||
import { GlTable, GlBadge } from '@gitlab/ui';
|
||||
import { FIELDS } from '../constants';
|
||||
import initUserPopovers from '~/user_popovers';
|
||||
import MemberAvatar from './member_avatar.vue';
|
||||
|
@ -9,17 +9,20 @@ import CreatedAt from './created_at.vue';
|
|||
import ExpiresAt from './expires_at.vue';
|
||||
import MemberActionButtons from './member_action_buttons.vue';
|
||||
import MembersTableCell from './members_table_cell.vue';
|
||||
import RoleDropdown from './role_dropdown.vue';
|
||||
|
||||
export default {
|
||||
name: 'MembersTable',
|
||||
components: {
|
||||
GlTable,
|
||||
GlBadge,
|
||||
MemberAvatar,
|
||||
CreatedAt,
|
||||
ExpiresAt,
|
||||
MembersTableCell,
|
||||
MemberSource,
|
||||
MemberActionButtons,
|
||||
RoleDropdown,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['members', 'tableFields']),
|
||||
|
@ -77,6 +80,13 @@ export default {
|
|||
<expires-at :date="expiresAt" />
|
||||
</template>
|
||||
|
||||
<template #cell(maxRole)="{ item: member }">
|
||||
<members-table-cell #default="{ permissions }" :member="member">
|
||||
<role-dropdown v-if="permissions.canUpdate" :member="member" />
|
||||
<gl-badge v-else>{{ member.accessLevel.stringValue }}</gl-badge>
|
||||
</members-table-cell>
|
||||
</template>
|
||||
|
||||
<template #cell(actions)="{ item: member }">
|
||||
<members-table-cell #default="{ memberType, isCurrentUser, permissions }" :member="member">
|
||||
<member-action-buttons
|
||||
|
|
|
@ -33,7 +33,7 @@ export default {
|
|||
return MEMBER_TYPES.user;
|
||||
},
|
||||
isDirectMember() {
|
||||
return this.member.source?.id === this.sourceId;
|
||||
return this.isGroup || this.member.source?.id === this.sourceId;
|
||||
},
|
||||
isCurrentUser() {
|
||||
return this.member.user?.id === this.currentUserId;
|
||||
|
@ -44,6 +44,9 @@ export default {
|
|||
canResend() {
|
||||
return Boolean(this.member.invite?.canResend);
|
||||
},
|
||||
canUpdate() {
|
||||
return !this.isCurrentUser && this.isDirectMember && this.member.canUpdate;
|
||||
},
|
||||
},
|
||||
render() {
|
||||
return this.$scopedSlots.default({
|
||||
|
@ -53,6 +56,7 @@ export default {
|
|||
permissions: {
|
||||
canRemove: this.canRemove,
|
||||
canResend: this.canResend,
|
||||
canUpdate: this.canUpdate,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
<script>
|
||||
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
|
||||
|
||||
export default {
|
||||
name: 'RoleDropdown',
|
||||
components: {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
},
|
||||
props: {
|
||||
member: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDesktop: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.isDesktop = bp.isDesktop();
|
||||
},
|
||||
methods: {
|
||||
handleSelect() {
|
||||
// Vuex action will be called here to make API request and update `member.accessLevel`
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-dropdown
|
||||
:right="!isDesktop"
|
||||
:text="member.accessLevel.stringValue"
|
||||
:header-text="__('Change permissions')"
|
||||
>
|
||||
<gl-dropdown-item
|
||||
v-for="(value, name) in member.validRoles"
|
||||
:key="value"
|
||||
is-check-item
|
||||
:is-checked="value === member.accessLevel.integerValue"
|
||||
@click="handleSelect"
|
||||
>
|
||||
{{ name }}
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</template>
|
|
@ -8,8 +8,20 @@ export default {
|
|||
components: {
|
||||
GlLabel,
|
||||
},
|
||||
props: {
|
||||
disableLabels: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['selectedLabels', 'allowScopedLabels', 'labelsFilterBasePath']),
|
||||
...mapState([
|
||||
'selectedLabels',
|
||||
'allowLabelRemove',
|
||||
'allowScopedLabels',
|
||||
'labelsFilterBasePath',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
labelFilterUrl(label) {
|
||||
|
@ -42,7 +54,10 @@ export default {
|
|||
:background-color="label.color"
|
||||
:target="labelFilterUrl(label)"
|
||||
:scoped="scopedLabel(label)"
|
||||
:show-close-button="allowLabelRemove"
|
||||
:disabled="disableLabels"
|
||||
tooltip-placement="top"
|
||||
@close="$emit('onLabelRemove', label.id)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -28,6 +28,11 @@ export default {
|
|||
DropdownValueCollapsed,
|
||||
},
|
||||
props: {
|
||||
allowLabelRemove: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
allowLabelEdit: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
@ -130,6 +135,7 @@ export default {
|
|||
mounted() {
|
||||
this.setInitialState({
|
||||
variant: this.variant,
|
||||
allowLabelRemove: this.allowLabelRemove,
|
||||
allowLabelEdit: this.allowLabelEdit,
|
||||
allowLabelCreate: this.allowLabelCreate,
|
||||
allowMultiselect: this.allowMultiselect,
|
||||
|
@ -252,7 +258,10 @@ export default {
|
|||
:allow-label-edit="allowLabelEdit"
|
||||
:labels-select-in-progress="labelsSelectInProgress"
|
||||
/>
|
||||
<dropdown-value>
|
||||
<dropdown-value
|
||||
:disable-labels="labelsSelectInProgress"
|
||||
@onLabelRemove="$emit('onLabelRemove', $event)"
|
||||
>
|
||||
<slot></slot>
|
||||
</dropdown-value>
|
||||
<dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
|
||||
|
|
|
@ -54,8 +54,5 @@ export const createLabel = ({ state, dispatch }, label) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const replaceSelectedLabels = ({ commit }, selectedLabels) =>
|
||||
commit(types.REPLACE_SELECTED_LABELS, selectedLabels);
|
||||
|
||||
export const updateSelectedLabels = ({ commit }, labels) =>
|
||||
commit(types.UPDATE_SELECTED_LABELS, { labels });
|
||||
|
|
|
@ -15,7 +15,6 @@ export const RECEIVE_CREATE_LABEL_FAILURE = 'RECEIVE_CREATE_LABEL_FAILURE';
|
|||
export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY';
|
||||
export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS';
|
||||
|
||||
export const REPLACE_SELECTED_LABELS = 'REPLACE_SELECTED_LABELS';
|
||||
export const UPDATE_SELECTED_LABELS = 'UPDATE_SELECTED_LABELS';
|
||||
|
||||
export const TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW = 'TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW';
|
||||
|
|
|
@ -57,10 +57,6 @@ export default {
|
|||
state.labelCreateInProgress = false;
|
||||
},
|
||||
|
||||
[types.REPLACE_SELECTED_LABELS](state, selectedLabels = []) {
|
||||
state.selectedLabels = selectedLabels;
|
||||
},
|
||||
|
||||
[types.UPDATE_SELECTED_LABELS](state, { labels }) {
|
||||
// Find the label to update from all the labels
|
||||
// and change `set` prop value to represent their current state.
|
||||
|
|
|
@ -15,6 +15,7 @@ export default () => ({
|
|||
|
||||
// UI Flags
|
||||
variant: '',
|
||||
allowLabelRemove: false,
|
||||
allowLabelCreate: false,
|
||||
allowLabelEdit: false,
|
||||
allowScopedLabels: false,
|
||||
|
|
|
@ -216,6 +216,10 @@
|
|||
width: px-to-rem(150px);
|
||||
}
|
||||
|
||||
.col-max-role {
|
||||
width: px-to-rem(175px);
|
||||
}
|
||||
|
||||
.col-expiration {
|
||||
width: px-to-rem(200px);
|
||||
}
|
||||
|
|
|
@ -171,7 +171,7 @@ class Admin::UsersController < Admin::ApplicationController
|
|||
# restore username to keep form action url.
|
||||
user.username = params[:id]
|
||||
format.html { render "edit" }
|
||||
format.json { render json: [result[:message]], status: result[:status] }
|
||||
format.json { render json: [result[:message]], status: :internal_server_error }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -45,7 +45,7 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
|
|||
if result[:status] == :success
|
||||
head :ok
|
||||
else
|
||||
render json: { message: result[:message] }, status: result[:status]
|
||||
render json: { message: result[:message] }, status: :internal_server_error
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -7,22 +7,22 @@ module Types
|
|||
graphql_name 'DetailedStatus'
|
||||
|
||||
field :group, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Group of the pipeline status'
|
||||
description: 'Group of the status'
|
||||
field :icon, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Icon of the pipeline status'
|
||||
description: 'Icon of the status'
|
||||
field :favicon, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Favicon of the pipeline status'
|
||||
field :details_path, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Path of the details for the pipeline status'
|
||||
description: 'Favicon of the status'
|
||||
field :details_path, GraphQL::STRING_TYPE, null: true,
|
||||
description: 'Path of the details for the status'
|
||||
field :has_details, GraphQL::BOOLEAN_TYPE, null: false,
|
||||
description: 'Indicates if the pipeline status has further details',
|
||||
description: 'Indicates if the status has further details',
|
||||
method: :has_details?
|
||||
field :label, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Label of the pipeline status'
|
||||
description: 'Label of the status'
|
||||
field :text, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Text of the pipeline status'
|
||||
description: 'Text of the status'
|
||||
field :tooltip, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Tooltip associated with the pipeline status',
|
||||
description: 'Tooltip associated with the status',
|
||||
method: :status_tooltip
|
||||
field :action, Types::Ci::StatusActionType, null: true,
|
||||
description: 'Action information for the status. This includes method, button title, icon, path, and title',
|
||||
|
|
|
@ -12,6 +12,9 @@ module Types
|
|||
description: 'Size of the group'
|
||||
field :jobs, Ci::JobType.connection_type, null: true,
|
||||
description: 'Jobs in group'
|
||||
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
|
||||
description: 'Detailed status of the group',
|
||||
resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,9 @@ module Types
|
|||
description: 'Name of the job'
|
||||
field :needs, JobType.connection_type, null: true,
|
||||
description: 'Builds that must complete before the jobs run'
|
||||
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
|
||||
description: 'Detailed status of the job',
|
||||
resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,9 @@ module Types
|
|||
description: 'Name of the stage'
|
||||
field :groups, Ci::GroupType.connection_type, null: true,
|
||||
description: 'Group of jobs for the stage'
|
||||
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
|
||||
description: 'Detailed status of the stage',
|
||||
resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -471,7 +471,8 @@ module ProjectsHelper
|
|||
labels: :read_label,
|
||||
issues: :read_issue,
|
||||
project_members: :read_project_member,
|
||||
wiki: :read_wiki
|
||||
wiki: :read_wiki,
|
||||
feature_flags: :read_feature_flag
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -482,7 +483,8 @@ module ProjectsHelper
|
|||
:read_environment,
|
||||
:read_issue,
|
||||
:read_sentry_issue,
|
||||
:read_cluster
|
||||
:read_cluster,
|
||||
:read_feature_flag
|
||||
].any? do |ability|
|
||||
can?(current_user, ability, project)
|
||||
end
|
||||
|
@ -561,7 +563,11 @@ module ProjectsHelper
|
|||
end
|
||||
|
||||
def sidebar_operations_link_path(project = @project)
|
||||
metrics_project_environments_path(project) if can?(current_user, :read_environment, project)
|
||||
if can?(current_user, :read_environment, project)
|
||||
metrics_project_environments_path(project)
|
||||
else
|
||||
project_feature_flags_path(project)
|
||||
end
|
||||
end
|
||||
|
||||
def project_last_activity(project)
|
||||
|
@ -754,6 +760,7 @@ module ProjectsHelper
|
|||
logs
|
||||
product_analytics
|
||||
metrics_dashboard
|
||||
feature_flags
|
||||
tracings
|
||||
]
|
||||
end
|
||||
|
|
37
app/models/ci/deleted_object.rb
Normal file
37
app/models/ci/deleted_object.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
class DeletedObject < ApplicationRecord
|
||||
extend Gitlab::Ci::Model
|
||||
|
||||
mount_uploader :file, DeletedObjectUploader
|
||||
|
||||
scope :ready_for_destruction, ->(limit) do
|
||||
where('pick_up_at < ?', Time.current).limit(limit)
|
||||
end
|
||||
|
||||
scope :lock_for_destruction, ->(limit) do
|
||||
ready_for_destruction(limit)
|
||||
.select(:id)
|
||||
.order(:pick_up_at)
|
||||
.lock('FOR UPDATE SKIP LOCKED')
|
||||
end
|
||||
|
||||
def self.bulk_import(artifacts)
|
||||
attributes = artifacts.each.with_object([]) do |artifact, accumulator|
|
||||
record = artifact.to_deleted_object_attrs
|
||||
accumulator << record if record[:store_dir] && record[:file]
|
||||
end
|
||||
|
||||
self.insert_all(attributes) if attributes.any?
|
||||
end
|
||||
|
||||
def delete_file_from_storage
|
||||
file.remove!
|
||||
true
|
||||
rescue => exception
|
||||
Gitlab::ErrorTracking.track_exception(exception)
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -290,6 +290,15 @@ module Ci
|
|||
max_size&.megabytes.to_i
|
||||
end
|
||||
|
||||
def to_deleted_object_attrs
|
||||
{
|
||||
file_store: file_store,
|
||||
store_dir: file.store_dir.to_s,
|
||||
file: file_identifier,
|
||||
pick_up_at: expire_at || Time.current
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_size
|
||||
|
|
|
@ -841,6 +841,25 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
def build_with_artifacts_in_self_and_descendants(name)
|
||||
builds_in_self_and_descendants
|
||||
.ordered_by_pipeline # find job in hierarchical order
|
||||
.with_downloadable_artifacts
|
||||
.find_by_name(name)
|
||||
end
|
||||
|
||||
def builds_in_self_and_descendants
|
||||
Ci::Build.latest.where(pipeline: self_and_descendants)
|
||||
end
|
||||
|
||||
# Without using `unscoped`, caller scope is also included into the query.
|
||||
# Using `unscoped` here will be redundant after Rails 6.1
|
||||
def self_and_descendants
|
||||
::Gitlab::Ci::PipelineObjectHierarchy
|
||||
.new(self.class.unscoped.where(id: id), options: { same_project: true })
|
||||
.base_and_descendants
|
||||
end
|
||||
|
||||
def bridge_triggered?
|
||||
source_bridge.present?
|
||||
end
|
||||
|
|
|
@ -48,6 +48,7 @@ class CommitStatus < ApplicationRecord
|
|||
scope :ordered_by_stage, -> { order(stage_idx: :asc) }
|
||||
scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
|
||||
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
|
||||
scope :ordered_by_pipeline, -> { order(pipeline_id: :asc) }
|
||||
scope :before_stage, -> (index) { where('stage_idx < ?', index) }
|
||||
scope :for_stage, -> (index) { where(stage_idx: index) }
|
||||
scope :after_stage, -> (index) { where('stage_idx > ?', index) }
|
||||
|
|
|
@ -76,6 +76,7 @@ class Group < Namespace
|
|||
validate :visibility_level_allowed_by_projects
|
||||
validate :visibility_level_allowed_by_sub_groups
|
||||
validate :visibility_level_allowed_by_parent
|
||||
validate :two_factor_authentication_allowed
|
||||
validates :variables, variable_duplicates: true
|
||||
|
||||
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
||||
|
@ -589,6 +590,16 @@ class Group < Namespace
|
|||
errors.add(:visibility_level, "#{visibility} is not allowed since there are sub-groups with higher visibility.")
|
||||
end
|
||||
|
||||
def two_factor_authentication_allowed
|
||||
return unless has_parent?
|
||||
return unless require_two_factor_authentication
|
||||
|
||||
ancestor_settings = ancestors.find_by(parent_id: nil).namespace_settings
|
||||
return if ancestor_settings.allow_mfa_for_subgroups
|
||||
|
||||
errors.add(:require_two_factor_authentication, _('is forbidden by a top-level group'))
|
||||
end
|
||||
|
||||
def members_from_self_and_ancestor_group_shares
|
||||
group_group_link_table = GroupGroupLink.arel_table
|
||||
group_member_table = GroupMember.arel_table
|
||||
|
|
|
@ -4,6 +4,7 @@ class NamespaceSetting < ApplicationRecord
|
|||
belongs_to :namespace, inverse_of: :namespace_settings
|
||||
|
||||
validate :default_branch_name_content
|
||||
validate :allow_mfa_for_group
|
||||
|
||||
NAMESPACE_SETTINGS_PARAMS = [:default_branch_name].freeze
|
||||
|
||||
|
@ -16,6 +17,12 @@ class NamespaceSetting < ApplicationRecord
|
|||
errors.add(:default_branch_name, "can not be an empty string")
|
||||
end
|
||||
end
|
||||
|
||||
def allow_mfa_for_group
|
||||
if namespace&.subgroup? && allow_mfa_for_subgroups == false
|
||||
errors.add(:allow_mfa_for_subgroups, _('is not allowed since the group is not top-level group.'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
NamespaceSetting.prepend_if_ee('EE::NamespaceSetting')
|
||||
|
|
|
@ -951,7 +951,7 @@ class Project < ApplicationRecord
|
|||
latest_pipeline = ci_pipelines.latest_successful_for_ref(ref)
|
||||
return unless latest_pipeline
|
||||
|
||||
latest_pipeline.builds.latest.with_downloadable_artifacts.find_by(name: job_name)
|
||||
latest_pipeline.build_with_artifacts_in_self_and_descendants(job_name)
|
||||
end
|
||||
|
||||
def latest_successful_build_for_sha(job_name, sha)
|
||||
|
@ -960,7 +960,7 @@ class Project < ApplicationRecord
|
|||
latest_pipeline = ci_pipelines.latest_successful_for_sha(sha)
|
||||
return unless latest_pipeline
|
||||
|
||||
latest_pipeline.builds.latest.with_downloadable_artifacts.find_by(name: job_name)
|
||||
latest_pipeline.build_with_artifacts_in_self_and_descendants(job_name)
|
||||
end
|
||||
|
||||
def latest_successful_build_for_ref!(job_name, ref = default_branch)
|
||||
|
|
|
@ -27,7 +27,7 @@ class ConfluenceService < Service
|
|||
end
|
||||
|
||||
def description
|
||||
s_('ConfluenceService|Connect a Confluence Cloud Workspace to your GitLab project')
|
||||
s_('ConfluenceService|Connect a Confluence Cloud Workspace to GitLab')
|
||||
end
|
||||
|
||||
def detailed_description
|
||||
|
|
|
@ -16,7 +16,7 @@ class PackagistService < Service
|
|||
end
|
||||
|
||||
def description
|
||||
'Update your project on Packagist, the main Composer repository'
|
||||
s_('Integrations|Update your projects on Packagist, the main Composer repository')
|
||||
end
|
||||
|
||||
def self.to_param
|
||||
|
|
62
app/services/ci/delete_objects_service.rb
Normal file
62
app/services/ci/delete_objects_service.rb
Normal file
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
class DeleteObjectsService
|
||||
TransactionInProgressError = Class.new(StandardError)
|
||||
TRANSACTION_MESSAGE = "can't perform network calls inside a database transaction"
|
||||
BATCH_SIZE = 100
|
||||
RETRY_IN = 10.minutes
|
||||
|
||||
def execute
|
||||
objects = load_next_batch
|
||||
destroy_everything(objects)
|
||||
end
|
||||
|
||||
def remaining_batches_count(max_batch_count:)
|
||||
Ci::DeletedObject
|
||||
.ready_for_destruction(max_batch_count * BATCH_SIZE)
|
||||
.size
|
||||
.fdiv(BATCH_SIZE)
|
||||
.ceil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def load_next_batch
|
||||
# `find_by_sql` performs a write in this case and we need to wrap it in
|
||||
# a transaction to stick to the primary database.
|
||||
Ci::DeletedObject.transaction do
|
||||
Ci::DeletedObject.find_by_sql([
|
||||
next_batch_sql, new_pick_up_at: RETRY_IN.from_now
|
||||
])
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def next_batch_sql
|
||||
<<~SQL.squish
|
||||
UPDATE "ci_deleted_objects"
|
||||
SET "pick_up_at" = :new_pick_up_at
|
||||
WHERE "ci_deleted_objects"."id" IN (#{locked_object_ids_sql})
|
||||
RETURNING *
|
||||
SQL
|
||||
end
|
||||
|
||||
def locked_object_ids_sql
|
||||
Ci::DeletedObject.lock_for_destruction(BATCH_SIZE).to_sql
|
||||
end
|
||||
|
||||
def destroy_everything(objects)
|
||||
raise TransactionInProgressError, TRANSACTION_MESSAGE if transaction_open?
|
||||
return unless objects.any?
|
||||
|
||||
deleted = objects.select(&:delete_file_from_storage)
|
||||
Ci::DeletedObject.id_in(deleted.map(&:id)).delete_all
|
||||
end
|
||||
|
||||
def transaction_open?
|
||||
Ci::DeletedObject.connection.transaction_open?
|
||||
end
|
||||
end
|
||||
end
|
11
app/uploaders/deleted_object_uploader.rb
Normal file
11
app/uploaders/deleted_object_uploader.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DeletedObjectUploader < GitlabUploader
|
||||
include ObjectStorage::Concern
|
||||
|
||||
storage_options Gitlab.config.artifacts
|
||||
|
||||
def store_dir
|
||||
model.store_dir
|
||||
end
|
||||
end
|
|
@ -25,8 +25,8 @@
|
|||
= link_to _('Remove user & report'), admin_abuse_report_path(abuse_report, remove_user: true),
|
||||
data: { confirm: _("USER %{user} WILL BE REMOVED! Are you sure?") % { user: user.name } }, remote: true, method: :delete, class: "gl-button btn btn-sm btn-block btn-danger js-remove-tr"
|
||||
- if user && !user.blocked?
|
||||
= link_to _('Block user'), block_admin_user_path(user), data: {confirm: _('USER WILL BE BLOCKED! Are you sure?')}, method: :put, class: "btn btn-sm btn-block"
|
||||
= link_to _('Block user'), block_admin_user_path(user), data: {confirm: _('USER WILL BE BLOCKED! Are you sure?')}, method: :put, class: "gl-button btn btn-sm btn-block"
|
||||
- else
|
||||
.btn.btn-sm.disabled.btn-block
|
||||
= _('Already blocked')
|
||||
= link_to _('Remove report'), [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-sm btn-block btn-close js-remove-tr"
|
||||
= link_to _('Remove report'), [:admin, abuse_report], remote: true, method: :delete, class: "gl-button btn btn-sm btn-block btn-close js-remove-tr"
|
||||
|
|
|
@ -40,5 +40,5 @@
|
|||
= render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
|
||||
|
||||
.form-actions
|
||||
= f.submit 'Submit', class: "btn btn-success wide"
|
||||
= link_to "Cancel", admin_applications_path, class: "btn btn-cancel"
|
||||
= f.submit 'Submit', class: "gl-button btn btn-success wide"
|
||||
= link_to "Cancel", admin_applications_path, class: "gl-button btn btn-cancel"
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
%p.light
|
||||
System OAuth applications don't belong to any user and can only be managed by admins
|
||||
%hr
|
||||
%p= link_to 'New application', new_admin_application_path, class: 'btn btn-success'
|
||||
%p= link_to 'New application', new_admin_application_path, class: 'gl-button btn btn-success'
|
||||
%table.table
|
||||
%thead
|
||||
%tr
|
||||
|
@ -23,6 +23,6 @@
|
|||
%td= @application_counts[application.id].to_i
|
||||
%td= application.trusted? ? 'Y': 'N'
|
||||
%td= application.confidential? ? 'Y': 'N'
|
||||
%td= link_to 'Edit', edit_admin_application_path(application), class: 'btn btn-link'
|
||||
%td= link_to 'Edit', edit_admin_application_path(application), class: 'gl-button btn btn-link'
|
||||
%td= render 'delete_form', application: application
|
||||
= paginate @applications, theme: 'gitlab'
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
.input-group
|
||||
%input.label.label-monospace.monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true }
|
||||
.input-group-append
|
||||
= clipboard_button(target: '#application_id', title: _("Copy ID"), class: "btn btn btn-default")
|
||||
= clipboard_button(target: '#application_id', title: _("Copy ID"), class: "gl-button btn btn-default")
|
||||
%tr
|
||||
%td
|
||||
= _('Secret')
|
||||
|
@ -22,7 +22,7 @@
|
|||
.input-group
|
||||
%input.label.label-monospace.monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
|
||||
.input-group-append
|
||||
= clipboard_button(target: '#secret', title: _("Copy secret"), class: "btn btn btn-default")
|
||||
= clipboard_button(target: '#secret', title: _("Copy secret"), class: "gl-button btn btn-default")
|
||||
%tr
|
||||
%td
|
||||
= _('Callback URL')
|
||||
|
@ -45,5 +45,5 @@
|
|||
= render "shared/tokens/scopes_list", token: @application
|
||||
|
||||
.form-actions
|
||||
= link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide float-left'
|
||||
= link_to 'Edit', edit_admin_application_path(@application), class: 'gl-button btn btn-primary wide float-left'
|
||||
= render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger gl-ml-3'
|
||||
|
|
|
@ -29,10 +29,10 @@
|
|||
.gl-alert-body
|
||||
= render 'shared/group_tips'
|
||||
.form-actions
|
||||
= f.submit _('Create group'), class: "btn btn-success"
|
||||
= link_to _('Cancel'), admin_groups_path, class: "btn btn-cancel"
|
||||
= f.submit _('Create group'), class: "gl-button btn btn-success"
|
||||
= link_to _('Cancel'), admin_groups_path, class: "gl-button btn btn-cancel"
|
||||
|
||||
- else
|
||||
.form-actions
|
||||
= f.submit _('Save changes'), class: "btn btn-success", data: { qa_selector: 'save_changes_button' }
|
||||
= link_to _('Cancel'), admin_group_path(@group), class: "btn btn-cancel"
|
||||
= f.submit _('Save changes'), class: "gl-button btn btn-success", data: { qa_selector: 'save_changes_button' }
|
||||
= link_to _('Cancel'), admin_group_path(@group), class: "gl-button btn btn-cancel"
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
= search_field_tag :name, project_name, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name', data: { qa_selector: 'group_search_field' }
|
||||
= sprite_icon('search', css_class: 'search-icon')
|
||||
= render "shared/groups/dropdown", options_hash: admin_groups_sort_options_hash
|
||||
= link_to new_admin_group_path, class: "btn btn-success" do
|
||||
= link_to new_admin_group_path, class: "gl-button btn btn-success" do
|
||||
= _('New group')
|
||||
%ul.content-list
|
||||
= render @groups
|
||||
|
|
|
@ -115,7 +115,7 @@
|
|||
.gl-mt-3
|
||||
= select_tag :access_level, options_for_select(@group.access_level_roles), class: "project-access-select select2"
|
||||
%hr
|
||||
= button_tag _('Add users to group'), class: "btn btn-success"
|
||||
= button_tag _('Add users to group'), class: "gl-button btn btn-success"
|
||||
= render 'shared/members/requests', membership_source: @group, requesters: @requesters, force_mobile_view: true
|
||||
|
||||
.card
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
%code#health-check-token= Gitlab::CurrentSettings.health_check_access_token
|
||||
.gl-mt-3
|
||||
= button_to _("Reset health check access token"), reset_health_check_token_admin_application_settings_path,
|
||||
method: :put, class: 'btn btn-default',
|
||||
method: :put, class: 'gl-button btn btn-default',
|
||||
data: { confirm: _('Are you sure you want to reset the health check token?') }
|
||||
%p.light
|
||||
#{ _('Health information can be retrieved from the following endpoints. More information is available') }
|
||||
|
|
|
@ -14,5 +14,5 @@
|
|||
= f.text_field :extern_uid, class: 'form-control', required: true
|
||||
|
||||
.form-actions
|
||||
= f.submit _('Save changes'), class: "btn btn-success"
|
||||
= f.submit _('Save changes'), class: "gl-button btn btn-success"
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
%td
|
||||
= identity.extern_uid
|
||||
%td
|
||||
= link_to edit_admin_user_identity_path(@user, identity), class: 'btn btn-sm btn-grouped' do
|
||||
= link_to edit_admin_user_identity_path(@user, identity), class: 'gl-button btn btn-sm btn-grouped' do
|
||||
= _("Edit")
|
||||
= link_to [:admin, @user, identity], method: :delete,
|
||||
class: 'btn btn-sm btn-danger',
|
||||
class: 'gl-button btn btn-sm btn-danger',
|
||||
data: { confirm: _("Are you sure you want to remove this identity?") } do
|
||||
= _('Delete')
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
- page_title _("Identities"), @user.name, _("Users")
|
||||
= render 'admin/users/head'
|
||||
|
||||
= link_to _('New identity'), new_admin_user_identity_path, class: 'float-right btn btn-success'
|
||||
= link_to _('New identity'), new_admin_user_identity_path, class: 'float-right gl-button btn btn-success'
|
||||
- if @identities.present?
|
||||
.table-holder
|
||||
%table.table
|
||||
|
|
|
@ -30,8 +30,8 @@
|
|||
= dropdown_content
|
||||
= dropdown_loading
|
||||
= render 'shared/projects/dropdown'
|
||||
= link_to new_project_path, class: 'btn btn-success' do
|
||||
= link_to new_project_path, class: 'gl-button btn btn-success' do
|
||||
New Project
|
||||
= button_tag "Search", class: "btn btn-primary btn-search hide"
|
||||
= button_tag "Search", class: "gl-button btn btn-primary btn-search hide"
|
||||
|
||||
= render 'projects'
|
||||
|
|
|
@ -149,7 +149,7 @@
|
|||
|
||||
.form-group.row
|
||||
.offset-sm-3.col-sm-9
|
||||
= f.submit _('Transfer'), class: 'btn btn-primary'
|
||||
= f.submit _('Transfer'), class: 'gl-button btn btn-primary'
|
||||
|
||||
.card.repository-check
|
||||
.card-header
|
||||
|
@ -169,7 +169,7 @@
|
|||
= link_to sprite_icon('question-o'), help_page_path('administration/repository_checks')
|
||||
|
||||
.form-group
|
||||
= f.submit _('Trigger repository check'), class: 'btn btn-primary'
|
||||
= f.submit _('Trigger repository check'), class: 'gl-button btn btn-primary'
|
||||
|
||||
.col-md-6
|
||||
- if @group
|
||||
|
|
|
@ -65,15 +65,15 @@
|
|||
.table-section.table-button-footer.section-10
|
||||
.btn-group.table-action-buttons
|
||||
.btn-group
|
||||
= link_to admin_runner_path(runner), class: 'btn btn-default has-tooltip', title: _('Edit'), ref: 'tooltip', aria: { label: _('Edit') }, data: { placement: 'top', container: 'body'} do
|
||||
= link_to admin_runner_path(runner), class: 'gl-button btn btn-default has-tooltip', title: _('Edit'), ref: 'tooltip', aria: { label: _('Edit') }, data: { placement: 'top', container: 'body'} do
|
||||
= sprite_icon('pencil')
|
||||
.btn-group
|
||||
- if runner.active?
|
||||
= link_to [:pause, :admin, runner], method: :get, class: 'btn btn-default btn-svg has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
|
||||
= link_to [:pause, :admin, runner], method: :get, class: 'gl-button btn btn-default btn-svg has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
|
||||
= sprite_icon('pause')
|
||||
- else
|
||||
= link_to [:resume, :admin, runner], method: :get, class: 'btn btn-default btn-svg has-tooltip gl-px-3', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do
|
||||
= link_to [:resume, :admin, runner], method: :get, class: 'gl-button btn btn-default btn-svg has-tooltip gl-px-3', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do
|
||||
= sprite_icon('play')
|
||||
.btn-group
|
||||
= link_to [:admin, runner], method: :delete, class: 'btn btn-danger has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
|
||||
= link_to [:admin, runner], method: :delete, class: 'gl-button btn btn-danger has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
|
||||
= sprite_icon('close')
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
.filtered-search-box
|
||||
= dropdown_tag(_('Recent searches'),
|
||||
options: { wrapper_class: 'filtered-search-history-dropdown-wrapper',
|
||||
toggle_class: 'btn filtered-search-history-dropdown-toggle-button',
|
||||
toggle_class: 'gl-button btn filtered-search-history-dropdown-toggle-button',
|
||||
dropdown_class: 'filtered-search-history-dropdown',
|
||||
content_class: 'filtered-search-history-dropdown-content' }) do
|
||||
.js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
|
||||
|
@ -60,7 +60,7 @@
|
|||
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
|
||||
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
|
||||
%li.filter-dropdown-item{ data: {hint: "#{'{{hint}}'}", tag: "#{'{{tag}}'}", action: "#{'{{hint === \'search\' ? \'submit\' : \'\' }}'}" } }
|
||||
= button_tag class: %w[btn btn-link] do
|
||||
= button_tag class: %w[gl-button btn btn-link] do
|
||||
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
|
||||
-# haml lint's ClassAttributeWithStaticValue
|
||||
%svg
|
||||
|
@ -78,21 +78,21 @@
|
|||
%ul{ data: { dropdown: true } }
|
||||
- Ci::Runner::AVAILABLE_STATUSES.each do |status|
|
||||
%li.filter-dropdown-item{ data: { value: status } }
|
||||
= button_tag class: %w[btn btn-link] do
|
||||
= button_tag class: %w[gl-button btn btn-link] do
|
||||
= status.titleize
|
||||
|
||||
#js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
|
||||
%ul{ data: { dropdown: true } }
|
||||
- Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
|
||||
%li.filter-dropdown-item{ data: { value: runner_type } }
|
||||
= button_tag class: %w[btn btn-link] do
|
||||
= button_tag class: %w[gl-button btn btn-link] do
|
||||
= runner_type.titleize
|
||||
|
||||
#js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
|
||||
%ul{ data: { dropdown: true } }
|
||||
- Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
|
||||
%li.filter-dropdown-item{ data: { value: runner_type } }
|
||||
= button_tag class: %w[btn btn-link] do
|
||||
= button_tag class: %w[gl-button btn btn-link] do
|
||||
= runner_type.titleize
|
||||
|
||||
#js-dropdown-runner-tag.filtered-search-input-dropdown-menu.dropdown-menu
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
= project.full_name
|
||||
%td
|
||||
.float-right
|
||||
= link_to 'Disable', admin_namespace_project_runner_project_path(project.namespace, project, runner_project), method: :delete, class: 'btn btn-danger btn-sm'
|
||||
= link_to 'Disable', admin_namespace_project_runner_project_path(project.namespace, project, runner_project), method: :delete, class: 'gl-button btn btn-danger btn-sm'
|
||||
|
||||
%table.table.unassigned-projects
|
||||
%thead
|
||||
|
@ -73,7 +73,7 @@
|
|||
.float-right
|
||||
= form_for project.runner_projects.new, url: admin_namespace_project_runner_projects_path(project.namespace, project), method: :post do |f|
|
||||
= f.hidden_field :runner_id, value: @runner.id
|
||||
= f.submit 'Enable', class: 'btn btn-sm'
|
||||
= f.submit 'Enable', class: 'gl-button btn btn-sm'
|
||||
= paginate_without_count @projects
|
||||
|
||||
.col-md-6
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
- text, status = @domain.unverified? ? [_('Unverified'), 'badge-danger'] : [_('Verified'), 'badge-success']
|
||||
.badge{ class: status }
|
||||
= text
|
||||
= link_to sprite_icon("redo"), verify_admin_serverless_domain_path(@domain.id), method: :post, class: "btn has-tooltip", title: _("Retry verification")
|
||||
= link_to sprite_icon("redo"), verify_admin_serverless_domain_path(@domain.id), method: :post, class: "gl-button btn has-tooltip", title: _("Retry verification")
|
||||
|
||||
.col-sm-6
|
||||
= f.label :serverless_domain_dns, _('DNS'), class: 'label-bold'
|
||||
|
@ -65,7 +65,7 @@
|
|||
%span.form-text.text-muted
|
||||
= _("Upload a private key for your certificate")
|
||||
|
||||
= f.submit @domain.persisted? ? _('Save changes') : _('Add domain'), class: "btn btn-success js-serverless-domain-submit", disabled: @domain.persisted?
|
||||
= f.submit @domain.persisted? ? _('Save changes') : _('Add domain'), class: "gl-button btn btn-success js-serverless-domain-submit", disabled: @domain.persisted?
|
||||
- if @domain.persisted?
|
||||
%button.gl-button.btn.btn-danger{ type: 'button', data: { toggle: 'modal', target: "#modal-delete-domain" } }
|
||||
= _('Delete domain')
|
||||
|
@ -88,7 +88,7 @@
|
|||
= _("You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application.").html_safe % { domain: "<code>#{@domain.domain}</code>".html_safe }
|
||||
|
||||
.modal-footer
|
||||
%a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' }
|
||||
%a{ href: '#', data: { dismiss: 'modal' }, class: 'gl-button btn btn-default' }
|
||||
= _('Cancel')
|
||||
|
||||
= link_to _('Delete domain'),
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
= password_field_tag 'user[password]', nil, class: 'form-control', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
|
||||
|
||||
.submit-container.move-submit-down
|
||||
= submit_tag _('Enter Admin Mode'), class: 'btn btn-success', data: { qa_selector: 'enter_admin_mode_button' }
|
||||
= submit_tag _('Enter Admin Mode'), class: 'gl-button btn btn-success', data: { qa_selector: 'enter_admin_mode_button' }
|
||||
|
|
|
@ -6,4 +6,4 @@
|
|||
= _("Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.")
|
||||
|
||||
.submit-container.move-submit-down
|
||||
= submit_tag 'Verify code', class: 'btn btn-success'
|
||||
= submit_tag 'Verify code', class: 'gl-button btn btn-success'
|
||||
|
|
|
@ -30,10 +30,10 @@
|
|||
.btn.btn-sm.disabled
|
||||
Submitted as ham
|
||||
- else
|
||||
= link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'btn btn-sm btn-warning'
|
||||
= link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'gl-button btn btn-sm btn-warning'
|
||||
- if user && !user.blocked?
|
||||
= link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm"
|
||||
= link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "gl-button btn btn-sm"
|
||||
- else
|
||||
.btn.btn-sm.disabled
|
||||
Already blocked
|
||||
= link_to 'Remove log', [:admin, spam_log], remote: true, method: :delete, class: "btn btn-sm btn-close js-remove-tr"
|
||||
= link_to 'Remove log', [:admin, spam_log], remote: true, method: :delete, class: "gl-button btn btn-sm btn-close js-remove-tr"
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
.form-actions
|
||||
- if @user.new_record?
|
||||
= f.submit 'Create user', class: "btn gl-button btn-success"
|
||||
= link_to 'Cancel', admin_users_path, class: "btn btn-cancel"
|
||||
= link_to 'Cancel', admin_users_path, class: "gl-button btn btn-cancel"
|
||||
- else
|
||||
= f.submit 'Save changes', class: "btn gl-button btn-success"
|
||||
= link_to 'Cancel', admin_user_path(@user), class: "btn gl-button btn-cancel"
|
||||
|
|
|
@ -24,5 +24,14 @@
|
|||
%td= subscription.created_at
|
||||
%td= link_to 'Remove', jira_connect_subscription_path(subscription), class: 'remove-subscription'
|
||||
|
||||
%p
|
||||
%strong Browser limitations:
|
||||
Adding a namespace currently works only in browsers that allow cross site cookies. Please make sure to use
|
||||
%a{ href: 'https://www.mozilla.org/en-US/firefox/', target: '_blank', rel: 'noopener noreferrer' } Firefox
|
||||
or
|
||||
%a{ href: 'https://www.google.com/chrome/index.html', target: '_blank', rel: 'noopener noreferrer' } Google Chrome
|
||||
or enable cross-site cookies in your browser when adding a namespace.
|
||||
%a{ href: 'https://gitlab.com/gitlab-org/gitlab/-/issues/263509', target: '_blank', rel: 'noopener noreferrer' } Learn more
|
||||
|
||||
= page_specific_javascript_tag('jira_connect.js')
|
||||
- add_page_specific_style 'page_bundles/jira_connect'
|
||||
|
|
|
@ -297,7 +297,11 @@
|
|||
%span
|
||||
= _('Environments')
|
||||
|
||||
= render_if_exists 'layouts/nav/sidebar/project_feature_flags_link'
|
||||
- if project_nav_tab? :feature_flags
|
||||
= nav_link(controller: :feature_flags) do
|
||||
= link_to project_feature_flags_path(@project), title: _('Feature Flags'), class: 'shortcuts-feature-flags' do
|
||||
%span
|
||||
= _('Feature Flags')
|
||||
|
||||
- if project_nav_tab?(:product_analytics)
|
||||
= nav_link(controller: :product_analytics) do
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
.timeline-entry-inner
|
||||
.flash-container.timeline-content
|
||||
|
||||
.timeline-icon.d-none.d-sm-none.d-md-block
|
||||
.timeline-icon.d-none.d-md-block
|
||||
%a.author-link{ href: user_path(current_user) }
|
||||
= image_tag avatar_icon_for_user(current_user), alt: current_user.to_reference, class: 'avatar s40'
|
||||
.timeline-content.timeline-content-form
|
||||
|
|
|
@ -147,6 +147,14 @@
|
|||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: cronjob:ci_schedule_delete_objects_cron
|
||||
:feature_category: :continuous_integration
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: cronjob:container_expiration_policy
|
||||
:feature_category: :container_registry
|
||||
:has_external_dependencies:
|
||||
|
@ -1305,6 +1313,14 @@
|
|||
:idempotent:
|
||||
:tags:
|
||||
- :requires_disk_io
|
||||
- :name: ci_delete_objects
|
||||
:feature_category: :continuous_integration
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: create_commit_signature
|
||||
:feature_category: :source_code_management
|
||||
:has_external_dependencies:
|
||||
|
|
38
app/workers/ci/delete_objects_worker.rb
Normal file
38
app/workers/ci/delete_objects_worker.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
class DeleteObjectsWorker
|
||||
include ApplicationWorker
|
||||
include LimitedCapacity::Worker
|
||||
|
||||
feature_category :continuous_integration
|
||||
idempotent!
|
||||
|
||||
def perform_work(*args)
|
||||
service.execute
|
||||
end
|
||||
|
||||
def remaining_work_count(*args)
|
||||
@remaining_work_count ||= service
|
||||
.remaining_batches_count(max_batch_count: remaining_capacity)
|
||||
end
|
||||
|
||||
def max_running_jobs
|
||||
if ::Feature.enabled?(:ci_delete_objects_low_concurrency)
|
||||
2
|
||||
elsif ::Feature.enabled?(:ci_delete_objects_medium_concurrency)
|
||||
20
|
||||
elsif ::Feature.enabled?(:ci_delete_objects_high_concurrency)
|
||||
50
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def service
|
||||
@service ||= DeleteObjectsService.new
|
||||
end
|
||||
end
|
||||
end
|
18
app/workers/ci/schedule_delete_objects_cron_worker.rb
Normal file
18
app/workers/ci/schedule_delete_objects_cron_worker.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
class ScheduleDeleteObjectsCronWorker
|
||||
include ApplicationWorker
|
||||
# rubocop:disable Scalability/CronWorkerContext
|
||||
# This worker does not perform work scoped to a context
|
||||
include CronjobQueue
|
||||
# rubocop:enable Scalability/CronWorkerContext
|
||||
|
||||
feature_category :continuous_integration
|
||||
idempotent!
|
||||
|
||||
def perform(*args)
|
||||
Ci::DeleteObjectsWorker.perform_with_capacity(*args)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add unattended database migration option
|
||||
merge_request: 44392
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add close button to issue, MR, and epic sidebar labels
|
||||
merge_request: 42703
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update integration descriptions to not be project-specific
|
||||
merge_request: 44893
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add note about cross site cookies browser limitaion to Jira App page
|
||||
merge_request: 44898
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove duplicated BS display property from Commit/Snippet's HAML
|
||||
merge_request: 44917
|
||||
author: Takuya Noguchi
|
||||
type: other
|
5
changelogs/unreleased/add-ci-deleted-objects-table.yml
Normal file
5
changelogs/unreleased/add-ci-deleted-objects-table.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Parallelize removal of expired artifacts
|
||||
merge_request: 39464
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Adds flexible rollout strategy UX and documentation
|
||||
merge_request: 43611
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Include builds from child pipelines in latest sucessful build for ref/sha
|
||||
merge_request: 29710
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/lm-add-status-graphql.yml
Normal file
5
changelogs/unreleased/lm-add-status-graphql.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'GrahphQL: Adds status to jobs, stages, and groups'
|
||||
merge_request: 43069
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/move-ff-menu-doc-to-core.yml
Normal file
5
changelogs/unreleased/move-ff-menu-doc-to-core.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move feature flags to core
|
||||
merge_request: 44642
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace fa-chevron-down with GitLab SVG in project visibility settings
|
||||
merge_request: 45021
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve merge error when pre-receive hooks fail in fast-forward merge
|
||||
merge_request: 44843
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/sh-update-rack-2-1-4.yml
Normal file
5
changelogs/unreleased/sh-update-rack-2-1-4.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update to Rack v2.1.4
|
||||
merge_request: 44518
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: ci_delete_objects_high_concurrency
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39464
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247103
|
||||
group: group::continuous integration
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: ci_delete_objects_low_concurrency
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39464
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247103
|
||||
group: group::continuous integration
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: ci_delete_objects_medium_concurrency
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39464
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247103
|
||||
group: group::continuous integration
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -435,6 +435,9 @@ production: &base
|
|||
# Remove expired build artifacts
|
||||
expire_build_artifacts_worker:
|
||||
cron: "50 * * * *"
|
||||
# Remove files from object storage
|
||||
ci_schedule_delete_objects_worker:
|
||||
cron: "*/16 * * * *"
|
||||
# Stop expired environments
|
||||
environments_auto_stop_cron_worker:
|
||||
cron: "24 * * * *"
|
||||
|
|
|
@ -416,6 +416,9 @@ Settings.cron_jobs['pipeline_schedule_worker']['job_class'] = 'PipelineScheduleW
|
|||
Settings.cron_jobs['expire_build_artifacts_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['expire_build_artifacts_worker']['cron'] ||= '50 * * * *'
|
||||
Settings.cron_jobs['expire_build_artifacts_worker']['job_class'] = 'ExpireBuildArtifactsWorker'
|
||||
Settings.cron_jobs['ci_schedule_delete_objects_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['ci_schedule_delete_objects_worker']['cron'] ||= '*/16 * * * *'
|
||||
Settings.cron_jobs['ci_schedule_delete_objects_worker']['job_class'] = 'Ci::ScheduleDeleteObjectsCronWorker'
|
||||
Settings.cron_jobs['environments_auto_stop_cron_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['environments_auto_stop_cron_worker']['cron'] ||= '24 * * * *'
|
||||
Settings.cron_jobs['environments_auto_stop_cron_worker']['job_class'] = 'Environments::AutoStopCronWorker'
|
||||
|
|
|
@ -87,7 +87,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
|
|||
delete :leave, on: :collection
|
||||
end
|
||||
|
||||
resources :group_links, only: [:create, :update, :destroy], constraints: { id: /\d+/ }
|
||||
resources :group_links, only: [:create, :update, :destroy], constraints: { id: /\d+|:id/ }
|
||||
|
||||
resources :uploads, only: [:create] do
|
||||
collection do
|
||||
|
|
|
@ -50,6 +50,8 @@
|
|||
- 2
|
||||
- - ci_batch_reset_minutes
|
||||
- 1
|
||||
- - ci_delete_objects
|
||||
- 1
|
||||
- - container_repository
|
||||
- 1
|
||||
- - create_commit_signature
|
||||
|
|
29
db/migrate/20200813135558_create_ci_deleted_objects.rb
Normal file
29
db/migrate/20200813135558_create_ci_deleted_objects.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateCiDeletedObjects < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
create_table :ci_deleted_objects, if_not_exists: true do |t|
|
||||
t.integer :file_store, limit: 2, default: 1, null: false
|
||||
t.datetime_with_timezone :pick_up_at, null: false, default: -> { 'now()' }, index: true
|
||||
t.text :store_dir, null: false
|
||||
|
||||
# rubocop:disable Migration/AddLimitToTextColumns
|
||||
# This column depends on the `file` column from `ci_job_artifacts` table
|
||||
# which doesn't have a constraint limit on it.
|
||||
t.text :file, null: false
|
||||
# rubocop:enable Migration/AddLimitToTextColumns
|
||||
end
|
||||
|
||||
add_text_limit(:ci_deleted_objects, :store_dir, 1024)
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :ci_deleted_objects
|
||||
end
|
||||
end
|
1
db/schema_migrations/20200813135558
Normal file
1
db/schema_migrations/20200813135558
Normal file
|
@ -0,0 +1 @@
|
|||
5f7a5fa697d769f5ccc9f0a6f19a91c8935f2559e019d50895574819494baf7e
|
|
@ -10074,6 +10074,24 @@ CREATE SEQUENCE ci_daily_build_group_report_results_id_seq
|
|||
|
||||
ALTER SEQUENCE ci_daily_build_group_report_results_id_seq OWNED BY ci_daily_build_group_report_results.id;
|
||||
|
||||
CREATE TABLE ci_deleted_objects (
|
||||
id bigint NOT NULL,
|
||||
file_store smallint DEFAULT 1 NOT NULL,
|
||||
pick_up_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
store_dir text NOT NULL,
|
||||
file text NOT NULL,
|
||||
CONSTRAINT check_5e151d6912 CHECK ((char_length(store_dir) <= 1024))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE ci_deleted_objects_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE ci_deleted_objects_id_seq OWNED BY ci_deleted_objects.id;
|
||||
|
||||
CREATE TABLE ci_freeze_periods (
|
||||
id bigint NOT NULL,
|
||||
project_id bigint NOT NULL,
|
||||
|
@ -17293,6 +17311,8 @@ ALTER TABLE ONLY ci_builds_runner_session ALTER COLUMN id SET DEFAULT nextval('c
|
|||
|
||||
ALTER TABLE ONLY ci_daily_build_group_report_results ALTER COLUMN id SET DEFAULT nextval('ci_daily_build_group_report_results_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY ci_deleted_objects ALTER COLUMN id SET DEFAULT nextval('ci_deleted_objects_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY ci_freeze_periods ALTER COLUMN id SET DEFAULT nextval('ci_freeze_periods_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY ci_group_variables ALTER COLUMN id SET DEFAULT nextval('ci_group_variables_id_seq'::regclass);
|
||||
|
@ -18282,6 +18302,9 @@ ALTER TABLE ONLY ci_builds_runner_session
|
|||
ALTER TABLE ONLY ci_daily_build_group_report_results
|
||||
ADD CONSTRAINT ci_daily_build_group_report_results_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY ci_deleted_objects
|
||||
ADD CONSTRAINT ci_deleted_objects_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY ci_freeze_periods
|
||||
ADD CONSTRAINT ci_freeze_periods_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -19820,6 +19843,8 @@ CREATE UNIQUE INDEX index_ci_builds_runner_session_on_build_id ON ci_builds_runn
|
|||
|
||||
CREATE INDEX index_ci_daily_build_group_report_results_on_last_pipeline_id ON ci_daily_build_group_report_results USING btree (last_pipeline_id);
|
||||
|
||||
CREATE INDEX index_ci_deleted_objects_on_pick_up_at ON ci_deleted_objects USING btree (pick_up_at);
|
||||
|
||||
CREATE INDEX index_ci_freeze_periods_on_project_id ON ci_freeze_periods USING btree (project_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_ci_group_variables_on_group_id_and_key ON ci_group_variables USING btree (group_id, key);
|
||||
|
|
|
@ -4,9 +4,11 @@ group: Progressive Delivery
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Feature Flags API **(PREMIUM)**
|
||||
# Feature Flags API **(CORE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9566) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9566) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Starter](https://about.gitlab.com/pricing/) in 13.4.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.5.
|
||||
|
||||
NOTE: **Note:**
|
||||
This API is behind a [feature flag](../operations/feature_flags.md#enable-or-disable-feature-flag-strategies).
|
||||
|
|
|
@ -4,9 +4,11 @@ group: Progressive Delivery
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# Legacy Feature Flags API **(PREMIUM)**
|
||||
# Legacy Feature Flags API **(CORE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9566) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9566) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Starter](https://about.gitlab.com/pricing/) in 13.4.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212318) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.5.
|
||||
|
||||
CAUTION: **Deprecation:**
|
||||
This API is deprecated and [scheduled for removal in GitLab 14.0](https://gitlab.com/gitlab-org/gitlab/-/issues/213369). Use [this API](feature_flags.md) instead.
|
||||
|
|
|
@ -2010,6 +2010,11 @@ type BurnupChartDailyTotals {
|
|||
}
|
||||
|
||||
type CiGroup {
|
||||
"""
|
||||
Detailed status of the group
|
||||
"""
|
||||
detailedStatus: DetailedStatus
|
||||
|
||||
"""
|
||||
Jobs in group
|
||||
"""
|
||||
|
@ -2082,6 +2087,11 @@ type CiGroupEdge {
|
|||
}
|
||||
|
||||
type CiJob {
|
||||
"""
|
||||
Detailed status of the job
|
||||
"""
|
||||
detailedStatus: DetailedStatus
|
||||
|
||||
"""
|
||||
Name of the job
|
||||
"""
|
||||
|
@ -2154,6 +2164,11 @@ Identifier of Ci::Pipeline
|
|||
scalar CiPipelineID
|
||||
|
||||
type CiStage {
|
||||
"""
|
||||
Detailed status of the stage
|
||||
"""
|
||||
detailedStatus: DetailedStatus
|
||||
|
||||
"""
|
||||
Group of jobs for the stage
|
||||
"""
|
||||
|
@ -5295,42 +5310,42 @@ type DetailedStatus {
|
|||
action: StatusAction
|
||||
|
||||
"""
|
||||
Path of the details for the pipeline status
|
||||
Path of the details for the status
|
||||
"""
|
||||
detailsPath: String!
|
||||
detailsPath: String
|
||||
|
||||
"""
|
||||
Favicon of the pipeline status
|
||||
Favicon of the status
|
||||
"""
|
||||
favicon: String!
|
||||
|
||||
"""
|
||||
Group of the pipeline status
|
||||
Group of the status
|
||||
"""
|
||||
group: String!
|
||||
|
||||
"""
|
||||
Indicates if the pipeline status has further details
|
||||
Indicates if the status has further details
|
||||
"""
|
||||
hasDetails: Boolean!
|
||||
|
||||
"""
|
||||
Icon of the pipeline status
|
||||
Icon of the status
|
||||
"""
|
||||
icon: String!
|
||||
|
||||
"""
|
||||
Label of the pipeline status
|
||||
Label of the status
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
Text of the pipeline status
|
||||
Text of the status
|
||||
"""
|
||||
text: String!
|
||||
|
||||
"""
|
||||
Tooltip associated with the pipeline status
|
||||
Tooltip associated with the status
|
||||
"""
|
||||
tooltip: String!
|
||||
}
|
||||
|
|
|
@ -5367,6 +5367,20 @@
|
|||
"name": "CiGroup",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "detailedStatus",
|
||||
"description": "Detailed status of the group",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "DetailedStatus",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "jobs",
|
||||
"description": "Jobs in group",
|
||||
|
@ -5573,6 +5587,20 @@
|
|||
"name": "CiJob",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "detailedStatus",
|
||||
"description": "Detailed status of the job",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "DetailedStatus",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Name of the job",
|
||||
|
@ -5775,6 +5803,20 @@
|
|||
"name": "CiStage",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "detailedStatus",
|
||||
"description": "Detailed status of the stage",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "DetailedStatus",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "groups",
|
||||
"description": "Group of jobs for the stage",
|
||||
|
@ -14479,25 +14521,21 @@
|
|||
},
|
||||
{
|
||||
"name": "detailsPath",
|
||||
"description": "Path of the details for the pipeline status",
|
||||
"description": "Path of the details for the status",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "favicon",
|
||||
"description": "Favicon of the pipeline status",
|
||||
"description": "Favicon of the status",
|
||||
"args": [
|
||||
|
||||
],
|
||||
|
@ -14515,7 +14553,7 @@
|
|||
},
|
||||
{
|
||||
"name": "group",
|
||||
"description": "Group of the pipeline status",
|
||||
"description": "Group of the status",
|
||||
"args": [
|
||||
|
||||
],
|
||||
|
@ -14533,7 +14571,7 @@
|
|||
},
|
||||
{
|
||||
"name": "hasDetails",
|
||||
"description": "Indicates if the pipeline status has further details",
|
||||
"description": "Indicates if the status has further details",
|
||||
"args": [
|
||||
|
||||
],
|
||||
|
@ -14551,7 +14589,7 @@
|
|||
},
|
||||
{
|
||||
"name": "icon",
|
||||
"description": "Icon of the pipeline status",
|
||||
"description": "Icon of the status",
|
||||
"args": [
|
||||
|
||||
],
|
||||
|
@ -14569,7 +14607,7 @@
|
|||
},
|
||||
{
|
||||
"name": "label",
|
||||
"description": "Label of the pipeline status",
|
||||
"description": "Label of the status",
|
||||
"args": [
|
||||
|
||||
],
|
||||
|
@ -14587,7 +14625,7 @@
|
|||
},
|
||||
{
|
||||
"name": "text",
|
||||
"description": "Text of the pipeline status",
|
||||
"description": "Text of the status",
|
||||
"args": [
|
||||
|
||||
],
|
||||
|
@ -14605,7 +14643,7 @@
|
|||
},
|
||||
{
|
||||
"name": "tooltip",
|
||||
"description": "Tooltip associated with the pipeline status",
|
||||
"description": "Tooltip associated with the status",
|
||||
"args": [
|
||||
|
||||
],
|
||||
|
|
|
@ -324,6 +324,7 @@ Represents the total number of issues and their weights for a particular day.
|
|||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `detailedStatus` | DetailedStatus | Detailed status of the group |
|
||||
| `name` | String | Name of the job group |
|
||||
| `size` | Int | Size of the group |
|
||||
|
||||
|
@ -331,12 +332,14 @@ Represents the total number of issues and their weights for a particular day.
|
|||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `detailedStatus` | DetailedStatus | Detailed status of the job |
|
||||
| `name` | String | Name of the job |
|
||||
|
||||
### CiStage
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `detailedStatus` | DetailedStatus | Detailed status of the stage |
|
||||
| `name` | String | Name of the stage |
|
||||
|
||||
### ClusterAgent
|
||||
|
@ -851,14 +854,14 @@ Autogenerated return type of DestroySnippet.
|
|||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `action` | StatusAction | Action information for the status. This includes method, button title, icon, path, and title |
|
||||
| `detailsPath` | String! | Path of the details for the pipeline status |
|
||||
| `favicon` | String! | Favicon of the pipeline status |
|
||||
| `group` | String! | Group of the pipeline status |
|
||||
| `hasDetails` | Boolean! | Indicates if the pipeline status has further details |
|
||||
| `icon` | String! | Icon of the pipeline status |
|
||||
| `label` | String! | Label of the pipeline status |
|
||||
| `text` | String! | Text of the pipeline status |
|
||||
| `tooltip` | String! | Tooltip associated with the pipeline status |
|
||||
| `detailsPath` | String | Path of the details for the status |
|
||||
| `favicon` | String! | Favicon of the status |
|
||||
| `group` | String! | Group of the status |
|
||||
| `hasDetails` | Boolean! | Indicates if the status has further details |
|
||||
| `icon` | String! | Icon of the status |
|
||||
| `label` | String! | Label of the status |
|
||||
| `text` | String! | Text of the status |
|
||||
| `tooltip` | String! | Tooltip associated with the status |
|
||||
|
||||
### DiffPosition
|
||||
|
||||
|
|
|
@ -63,6 +63,11 @@ the given reference name and job, provided the job finished successfully. This
|
|||
is the same as [getting the job's artifacts](#get-job-artifacts), but by
|
||||
defining the job's name instead of its ID.
|
||||
|
||||
NOTE: **Note:**
|
||||
If a pipeline is [parent of other child pipelines](../ci/parent_child_pipelines.md), artifacts
|
||||
are searched in hierarchical order from parent to child. For example, if both parent and
|
||||
child pipelines have a job with the same name, the artifact from the parent pipeline will be returned.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/jobs/artifacts/:ref_name/download?job=name
|
||||
```
|
||||
|
@ -157,6 +162,11 @@ Download a single artifact file for a specific job of the latest successful
|
|||
pipeline for the given reference name from within the job's artifacts archive.
|
||||
The file is extracted from the archive and streamed to the client.
|
||||
|
||||
In [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/201784) and later, artifacts
|
||||
for [parent and child pipelines](../ci/parent_child_pipelines.md) are searched in hierarchical
|
||||
order from parent to child. For example, if both parent and child pipelines have a
|
||||
job with the same name, the artifact from the parent pipeline is returned.
|
||||
|
||||
```plaintext
|
||||
GET /projects/:id/jobs/artifacts/:ref_name/raw/*artifact_path?job=name
|
||||
```
|
||||
|
|
|
@ -343,6 +343,11 @@ The latest artifacts are created by jobs in the **most recent** successful pipel
|
|||
for the specific ref. If you run two types of pipelines for the same ref, timing determines the latest
|
||||
artifact. For example, if a merge request creates a branch pipeline at the same time as a scheduled pipeline, the pipeline that completed most recently creates the latest artifact.
|
||||
|
||||
In [GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/201784) and later, artifacts
|
||||
for [parent and child pipelines](../parent_child_pipelines.md) are searched in hierarchical
|
||||
order from parent to child. For example, if both parent and child pipelines have a
|
||||
job with the same name, the artifact from the parent pipeline is returned.
|
||||
|
||||
Artifacts for other pipelines can be accessed with direct access to them.
|
||||
|
||||
The structure of the URL to download the whole artifacts archive is the following:
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue