Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c5d67a0495
commit
1949d08c1a
|
|
@ -180,11 +180,9 @@ export default {
|
|||
/>
|
||||
</span>
|
||||
</div>
|
||||
<span class="gl-display-flex gl-justify-content-end">
|
||||
<gl-button v-gl-modal.authKeyModal class="gl-mt-2" :disabled="isDisabled">{{
|
||||
$options.RESET_KEY
|
||||
}}</gl-button>
|
||||
</span>
|
||||
<gl-button v-gl-modal.authKeyModal class="gl-mt-2" :disabled="isDisabled">{{
|
||||
$options.RESET_KEY
|
||||
}}</gl-button>
|
||||
<gl-modal
|
||||
modal-id="authKeyModal"
|
||||
:title="$options.RESET_KEY"
|
||||
|
|
|
|||
|
|
@ -511,16 +511,11 @@ export default {
|
|||
max-rows="10"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<gl-button :disabled="!canTestAlert" @click="validateTestAlert">{{
|
||||
$options.i18n.testAlertInfo
|
||||
}}</gl-button>
|
||||
</div>
|
||||
<gl-button :disabled="!canTestAlert" @click="validateTestAlert">{{
|
||||
$options.i18n.testAlertInfo
|
||||
}}</gl-button>
|
||||
</template>
|
||||
<div class="footer-block row-content-block gl-display-flex gl-justify-content-space-between">
|
||||
<gl-button category="primary" :disabled="!canSaveConfig" @click="onReset">
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
variant="success"
|
||||
category="primary"
|
||||
|
|
@ -529,6 +524,9 @@ export default {
|
|||
>
|
||||
{{ __('Save changes') }}
|
||||
</gl-button>
|
||||
<gl-button category="primary" :disabled="!canSaveConfig" @click="onReset">
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</gl-form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ export default {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="isEditing" class="row-content-block gl-display-flex gl-justify-content-end">
|
||||
<div v-if="isEditing" class="row-content-block">
|
||||
<gl-button class="btn-cancel gl-mr-4" data-testid="cancelEditing" @click="onCancel">
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
|
|
@ -232,7 +232,7 @@ export default {
|
|||
{{ s__('Badges|Save changes') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<div v-else class="gl-display-flex gl-justify-content-end form-group">
|
||||
<div v-else class="form-group">
|
||||
<gl-button :loading="isSaving" type="submit" variant="success" category="primary">
|
||||
{{ s__('Badges|Add badge') }}
|
||||
</gl-button>
|
||||
|
|
|
|||
|
|
@ -163,10 +163,7 @@ export default {
|
|||
</p>
|
||||
</template>
|
||||
</gl-table>
|
||||
<div
|
||||
class="ci-variable-actions d-flex justify-content-end"
|
||||
:class="{ 'justify-content-center': !tableIsNotEmpty }"
|
||||
>
|
||||
<div class="ci-variable-actions" :class="{ 'justify-content-center': !tableIsNotEmpty }">
|
||||
<gl-button
|
||||
v-if="tableIsNotEmpty"
|
||||
ref="secret-value-reveal-button"
|
||||
|
|
|
|||
|
|
@ -69,15 +69,13 @@ export default {
|
|||
</p>
|
||||
</template>
|
||||
</gl-table>
|
||||
<div class="gl-display-flex gl-justify-content-center">
|
||||
<gl-button
|
||||
v-gl-modal.deploy-freeze-modal
|
||||
data-testid="add-deploy-freeze"
|
||||
category="primary"
|
||||
variant="success"
|
||||
>
|
||||
{{ $options.translations.addDeployFreeze }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<gl-button
|
||||
v-gl-modal.deploy-freeze-modal
|
||||
data-testid="add-deploy-freeze"
|
||||
category="primary"
|
||||
variant="success"
|
||||
>
|
||||
{{ $options.translations.addDeployFreeze }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -92,15 +92,13 @@ export default {
|
|||
@select-project="updateSelectedProject"
|
||||
/>
|
||||
</div>
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<gl-button
|
||||
:disabled="settingsLoading"
|
||||
class="js-error-tracking-button"
|
||||
variant="success"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ __('Save changes') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<gl-button
|
||||
:disabled="settingsLoading"
|
||||
class="js-error-tracking-button"
|
||||
variant="success"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ __('Save changes') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export function membersBeforeSave(members) {
|
|||
: '';
|
||||
|
||||
return {
|
||||
type: member.type,
|
||||
username: member.username,
|
||||
avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
|
||||
title: sanitize(title),
|
||||
|
|
@ -275,9 +276,11 @@ class GfmAutoComplete {
|
|||
return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
|
||||
}
|
||||
|
||||
if (command === MEMBER_COMMAND.ASSIGN) {
|
||||
if (command === MEMBER_COMMAND.ASSIGN || command === MEMBER_COMMAND.REASSIGN) {
|
||||
// Only include members which are not assigned to Issuable currently
|
||||
return data.filter(member => !assignees.includes(member.search));
|
||||
return data.filter(
|
||||
member => member.type === 'User' && !assignees.includes(member.search),
|
||||
);
|
||||
} else if (command === MEMBER_COMMAND.UNASSIGN) {
|
||||
// Only include members which are assigned to Issuable currently
|
||||
return data.filter(member => assignees.includes(member.search));
|
||||
|
|
|
|||
|
|
@ -92,11 +92,9 @@ export default {
|
|||
</a>
|
||||
</p>
|
||||
</gl-form-group>
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<gl-button variant="success" category="primary" @click="updateGrafanaIntegration">
|
||||
{{ __('Save Changes') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<gl-button variant="success" category="primary" @click="updateGrafanaIntegration">
|
||||
{{ __('Save Changes') }}
|
||||
</gl-button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -130,18 +130,16 @@ export default {
|
|||
<span>{{ $options.i18n.autoCloseIncidents.label }}</span>
|
||||
</gl-form-checkbox>
|
||||
</gl-form-group>
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<gl-button
|
||||
ref="submitBtn"
|
||||
data-qa-selector="save_changes_button"
|
||||
:disabled="loading"
|
||||
variant="success"
|
||||
type="submit"
|
||||
class="js-no-auto-disable"
|
||||
>
|
||||
{{ $options.i18n.saveBtnLabel }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<gl-button
|
||||
ref="submitBtn"
|
||||
data-qa-selector="save_changes_button"
|
||||
:disabled="loading"
|
||||
variant="success"
|
||||
type="submit"
|
||||
class="js-no-auto-disable"
|
||||
>
|
||||
{{ $options.i18n.saveBtnLabel }}
|
||||
</gl-button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -149,17 +149,15 @@ export default {
|
|||
</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<gl-button
|
||||
v-gl-modal.resetWebhookModal
|
||||
class="gl-mt-3"
|
||||
:disabled="loading"
|
||||
:loading="resettingWebhook"
|
||||
data-testid="webhook-reset-btn"
|
||||
>
|
||||
{{ $options.i18n.webhookUrl.resetWebhookUrl }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<gl-button
|
||||
v-gl-modal.resetWebhookModal
|
||||
class="gl-mt-3"
|
||||
:disabled="loading"
|
||||
:loading="resettingWebhook"
|
||||
data-testid="webhook-reset-btn"
|
||||
>
|
||||
{{ $options.i18n.webhookUrl.resetWebhookUrl }}
|
||||
</gl-button>
|
||||
<gl-modal
|
||||
modal-id="resetWebhookModal"
|
||||
:title="$options.i18n.webhookUrl.resetWebhookUrl"
|
||||
|
|
@ -170,17 +168,15 @@ export default {
|
|||
{{ $options.i18n.webhookUrl.restKeyInfo }}
|
||||
</gl-modal>
|
||||
</gl-form-group>
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<gl-button
|
||||
ref="submitBtn"
|
||||
:disabled="isSaveDisabled"
|
||||
variant="success"
|
||||
type="submit"
|
||||
class="js-no-auto-disable"
|
||||
>
|
||||
{{ $options.i18n.saveBtnLabel }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<gl-button
|
||||
ref="submitBtn"
|
||||
:disabled="isSaveDisabled"
|
||||
variant="success"
|
||||
type="submit"
|
||||
class="js-no-auto-disable"
|
||||
>
|
||||
{{ $options.i18n.saveBtnLabel }}
|
||||
</gl-button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -44,11 +44,9 @@ export default {
|
|||
<form>
|
||||
<dashboard-timezone />
|
||||
<external-dashboard />
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<gl-button variant="success" category="primary" @click="saveChanges">
|
||||
{{ __('Save Changes') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<gl-button variant="success" category="primary" @click="saveChanges">
|
||||
{{ __('Save Changes') }}
|
||||
</gl-button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export default {
|
|||
variant="info"
|
||||
category="primary"
|
||||
class="js-get-started-pipelines"
|
||||
data-testid="get-started-pipelines"
|
||||
>
|
||||
{{ s__('Pipelines|Get started with Pipelines') }}
|
||||
</gl-button>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export default {
|
|||
variant="success"
|
||||
category="primary"
|
||||
class="js-run-pipeline"
|
||||
data-testid="run-pipeline-button"
|
||||
>
|
||||
{{ s__('Pipelines|Run Pipeline') }}
|
||||
</gl-button>
|
||||
|
|
@ -54,12 +55,13 @@ export default {
|
|||
v-if="resetCachePath"
|
||||
:loading="isResetCacheButtonLoading"
|
||||
class="js-clear-cache"
|
||||
data-testid="clear-cache-button"
|
||||
@click="onClickResetCache"
|
||||
>
|
||||
{{ s__('Pipelines|Clear Runner Caches') }}
|
||||
</gl-button>
|
||||
|
||||
<gl-button v-if="ciLintPath" :href="ciLintPath" class="js-ci-lint">
|
||||
<gl-button v-if="ciLintPath" :href="ciLintPath" class="js-ci-lint" data-testid="ci-lint-button">
|
||||
{{ s__('Pipelines|CI Lint') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -157,16 +157,14 @@ export default {
|
|||
}}
|
||||
</span>
|
||||
</template>
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<gl-button
|
||||
variant="success"
|
||||
class="gl-mt-5"
|
||||
:disabled="isTemplateSaving"
|
||||
@click="onSaveTemplate"
|
||||
>
|
||||
{{ __('Save changes') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<gl-button
|
||||
variant="success"
|
||||
class="gl-mt-5"
|
||||
:disabled="isTemplateSaving"
|
||||
@click="onSaveTemplate"
|
||||
>
|
||||
{{ __('Save changes') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -103,27 +103,25 @@ export default {
|
|||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<gl-button
|
||||
ref="cancel-button"
|
||||
type="reset"
|
||||
class="gl-mr-3 gl-display-block"
|
||||
:disabled="isCancelButtonDisabled"
|
||||
>
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
ref="save-button"
|
||||
type="submit"
|
||||
:disabled="isSubmitButtonDisabled"
|
||||
variant="success"
|
||||
category="primary"
|
||||
class="gl-display-flex gl-justify-content-center gl-align-items-center js-no-auto-disable"
|
||||
>
|
||||
{{ $options.i18n.SET_CLEANUP_POLICY_BUTTON }}
|
||||
<gl-loading-icon v-if="isLoading" class="gl-ml-3" />
|
||||
</gl-button>
|
||||
</div>
|
||||
<gl-button
|
||||
ref="cancel-button"
|
||||
type="reset"
|
||||
class="gl-mr-3 gl-display-block float-right"
|
||||
:disabled="isCancelButtonDisabled"
|
||||
>
|
||||
{{ __('Cancel') }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
ref="save-button"
|
||||
type="submit"
|
||||
:disabled="isSubmitButtonDisabled"
|
||||
variant="success"
|
||||
category="primary"
|
||||
class="js-no-auto-disable"
|
||||
>
|
||||
{{ $options.i18n.SET_CLEANUP_POLICY_BUTTON }}
|
||||
<gl-loading-icon v-if="isLoading" class="gl-ml-3" />
|
||||
</gl-button>
|
||||
</template>
|
||||
</gl-card>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { mapActions } from 'vuex';
|
||||
import { __, sprintf } from '../../../locale';
|
||||
import { deprecatedCreateFlash as Flash } from '~/flash';
|
||||
|
|
@ -8,7 +8,7 @@ import eventHub from '../../event_hub';
|
|||
|
||||
export default {
|
||||
components: {
|
||||
GlLoadingIcon,
|
||||
GlButton,
|
||||
},
|
||||
inject: ['fullPath'],
|
||||
props: {
|
||||
|
|
@ -65,19 +65,19 @@ export default {
|
|||
|
||||
<template>
|
||||
<div class="sidebar-item-warning-message-actions">
|
||||
<button type="button" class="btn btn-default gl-mr-3" @click="closeForm">
|
||||
<gl-button class="gl-mr-3" @click="closeForm">
|
||||
{{ __('Cancel') }}
|
||||
</button>
|
||||
</gl-button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
<gl-button
|
||||
data-testid="lock-toggle"
|
||||
class="btn btn-close"
|
||||
category="secondary"
|
||||
variant="warning"
|
||||
:disabled="isLoading"
|
||||
:loading="isLoading"
|
||||
@click.prevent="submitForm"
|
||||
>
|
||||
<gl-loading-icon v-if="isLoading" inline />
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -63,8 +63,13 @@ const autoCompleteMap = {
|
|||
SidebarMediator.singleton?.store?.assignees?.map(assignee => assignee.username) || [];
|
||||
}
|
||||
|
||||
if (doesCurrentLineStartWith('/assign', fullText, selectionStart)) {
|
||||
return this.members.filter(member => !this.assignees.includes(member.username));
|
||||
if (
|
||||
doesCurrentLineStartWith('/assign', fullText, selectionStart) ||
|
||||
doesCurrentLineStartWith('/reassign', fullText, selectionStart)
|
||||
) {
|
||||
return this.members.filter(
|
||||
member => member.type === 'User' && !this.assignees.includes(member.username),
|
||||
);
|
||||
}
|
||||
|
||||
if (doesCurrentLineStartWith('/unassign', fullText, selectionStart)) {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,12 @@ export default {
|
|||
active: tab.isActive,
|
||||
}"
|
||||
>
|
||||
<a :class="`js-${scope}-tab-${tab.scope}`" role="button" @click="onTabClick(tab)">
|
||||
<a
|
||||
:class="`js-${scope}-tab-${tab.scope}`"
|
||||
:data-testid="`${scope}-tab-${tab.scope}`"
|
||||
role="button"
|
||||
@click="onTabClick(tab)"
|
||||
>
|
||||
{{ tab.name }}
|
||||
|
||||
<span v-if="shouldRenderBadge(tab.count)" class="badge badge-pill"> {{ tab.count }} </span>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ class Projects::Ci::DailyBuildGroupReportResultsController < Projects::Applicati
|
|||
MAX_ITEMS = 1000
|
||||
REPORT_WINDOW = 90.days
|
||||
|
||||
before_action :validate_feature_flag!
|
||||
before_action :authorize_read_build_report_results!
|
||||
before_action :validate_param_type!
|
||||
|
||||
|
|
@ -19,10 +18,6 @@ class Projects::Ci::DailyBuildGroupReportResultsController < Projects::Applicati
|
|||
|
||||
private
|
||||
|
||||
def validate_feature_flag!
|
||||
render_404 unless Feature.enabled?(:ci_download_daily_code_coverage, project, default_enabled: true)
|
||||
end
|
||||
|
||||
def validate_param_type!
|
||||
respond_422 unless allowed_param_types.include?(param_type)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ class Projects::GraphsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def get_daily_coverage_options
|
||||
return unless Feature.enabled?(:ci_download_daily_code_coverage, @project, default_enabled: true)
|
||||
return unless can?(current_user, :read_build_report_results, project)
|
||||
|
||||
date_today = Date.current
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
module Ci
|
||||
class DailyBuildGroupReportResultService
|
||||
def execute(pipeline)
|
||||
return unless Feature.enabled?(:ci_daily_code_coverage, pipeline.project, default_enabled: true)
|
||||
|
||||
DailyBuildGroupReportResult.upsert_reports(coverage_reports(pipeline))
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ module QuickActions
|
|||
def extract_users(params)
|
||||
return [] if params.nil?
|
||||
|
||||
users = extract_references(params, :user)
|
||||
users = extract_references(params, :mentioned_user)
|
||||
|
||||
if users.empty?
|
||||
users =
|
||||
|
|
|
|||
|
|
@ -60,5 +60,4 @@
|
|||
|
||||
= render_if_exists 'admin/application_settings/updating_name_disabled_for_users', form: f
|
||||
= render_if_exists 'admin/application_settings/availability_on_namespace_setting', form: f
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Save changes'), class: 'btn btn-success qa-save-changes-button'
|
||||
= f.submit _('Save changes'), class: 'btn btn-success qa-save-changes-button'
|
||||
|
|
|
|||
|
|
@ -12,5 +12,4 @@
|
|||
= link_to sprite_icon('question-o'),
|
||||
help_page_path('user/admin_area/diff_limits',
|
||||
anchor: 'maximum-diff-patch-size')
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Save changes'), class: 'btn btn-success'
|
||||
= f.submit _('Save changes'), class: 'btn btn-success'
|
||||
|
|
|
|||
|
|
@ -47,5 +47,4 @@
|
|||
.form-group
|
||||
= f.label :external_authorization_service_default_label, _('Default classification label'), class: 'label-bold'
|
||||
= f.text_field :external_authorization_service_default_label, class: 'form-control'
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit 'Save changes', class: "btn btn-success"
|
||||
= f.submit 'Save changes', class: "btn btn-success"
|
||||
|
|
|
|||
|
|
@ -10,5 +10,4 @@
|
|||
%span.form-text.text-muted
|
||||
= (_("Changes affect new repositories only. If not specified, Git's default name %{branch_name_default} will be used.") % { branch_name_default: fallback_branch_name } ).html_safe
|
||||
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Save changes'), class: 'gl-button btn-success'
|
||||
= f.submit _('Save changes'), class: 'gl-button btn-success'
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@
|
|||
If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
|
||||
- clear_repository_checks_link = _('Clear all repository checks')
|
||||
- clear_repository_checks_message = _('This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?')
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= link_to clear_repository_checks_link, clear_repository_check_states_admin_application_settings_path, data: { confirm: clear_repository_checks_message }, method: :put, class: "btn btn-sm btn-remove"
|
||||
= link_to clear_repository_checks_link, clear_repository_check_states_admin_application_settings_path, data: { confirm: clear_repository_checks_message }, method: :put, class: "btn btn-sm btn-remove"
|
||||
|
||||
.sub-section
|
||||
%h4 Housekeeping
|
||||
|
|
@ -56,5 +55,4 @@
|
|||
.form-text.text-muted
|
||||
Number of Git pushes after which 'git gc' is run.
|
||||
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Save changes'), class: "btn btn-success"
|
||||
= f.submit _('Save changes'), class: "btn btn-success"
|
||||
|
|
|
|||
|
|
@ -14,5 +14,4 @@
|
|||
|
||||
= render_if_exists 'admin/application_settings/mirror_settings', form: f
|
||||
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Save changes'), class: "btn btn-success"
|
||||
= f.submit _('Save changes'), class: "btn btn-success"
|
||||
|
|
|
|||
|
|
@ -15,5 +15,4 @@
|
|||
%span.form-text.text-muted#static_objects_external_storage_auth_token_help_block
|
||||
= _('A secure token that identifies an external storage request.')
|
||||
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Save changes'), class: "btn btn-success"
|
||||
= f.submit _('Save changes'), class: "btn btn-success"
|
||||
|
|
|
|||
|
|
@ -22,5 +22,4 @@
|
|||
= f.text_field attribute[:name], class: 'form-text-input', value: attribute[:value]
|
||||
= f.label attribute[:label], attribute[:label], class: 'label-bold form-check-label'
|
||||
%br
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Save changes'), class: "btn btn-success qa-save-changes-button"
|
||||
= f.submit _('Save changes'), class: "btn btn-success qa-save-changes-button"
|
||||
|
|
|
|||
|
|
@ -57,5 +57,4 @@
|
|||
= f.label :sign_in_text, class: 'label-bold'
|
||||
= f.text_area :sign_in_text, class: 'form-control', rows: 4
|
||||
.form-text.text-muted Markdown enabled
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit 'Save changes', class: "btn btn-success"
|
||||
= f.submit 'Save changes', class: "btn btn-success"
|
||||
|
|
|
|||
|
|
@ -67,5 +67,4 @@
|
|||
= f.label :after_sign_up_text, class: 'label-bold'
|
||||
= f.text_area :after_sign_up_text, class: 'form-control', rows: 4
|
||||
.form-text.text-muted Markdown enabled
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit 'Save changes', class: "btn btn-success"
|
||||
= f.submit 'Save changes', class: "btn btn-success"
|
||||
|
|
|
|||
|
|
@ -8,5 +8,4 @@
|
|||
.form-text.text-muted
|
||||
Maximum time for web terminal websocket connection (in seconds).
|
||||
0 for unlimited.
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit 'Save changes', class: "btn btn-success"
|
||||
= f.submit 'Save changes', class: "btn btn-success"
|
||||
|
|
|
|||
|
|
@ -15,5 +15,4 @@
|
|||
= f.text_area :terms, class: 'form-control', rows: 8
|
||||
.form-text.text-muted
|
||||
= _("Markdown enabled")
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _("Save changes"), class: "btn btn-success"
|
||||
= f.submit _("Save changes"), class: "btn btn-success"
|
||||
|
|
|
|||
|
|
@ -66,5 +66,4 @@
|
|||
.form-group
|
||||
= f.label field_name, "#{type.upcase} SSH keys", class: 'label-bold'
|
||||
= f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control'
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Save changes'), class: "btn btn-success"
|
||||
= f.submit _('Save changes'), class: "btn btn-success"
|
||||
|
|
|
|||
|
|
@ -101,8 +101,7 @@
|
|||
= s_('IDE|Live Preview')
|
||||
%span.form-text.text-muted
|
||||
= s_('IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview.')
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Save changes'), class: "btn btn-success"
|
||||
= f.submit _('Save changes'), class: "btn btn-success"
|
||||
|
||||
- if Feature.enabled?(:maintenance_mode)
|
||||
%section.settings.no-animate#js-maintenance-mode-toggle{ class: ('expanded' if expanded_by_default?) }
|
||||
|
|
|
|||
|
|
@ -24,8 +24,7 @@
|
|||
.text-muted
|
||||
= html_escape(s_('ClusterIntegration|A cluster management project can be used to run deployment jobs with Kubernetes %{code_open}cluster-admin%{code_close} privileges.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
|
||||
= link_to _('More information'), help_page_path('user/clusters/management_project.md'), target: '_blank'
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= field.submit _('Save changes'), class: 'btn btn-success'
|
||||
= field.submit _('Save changes'), class: 'btn btn-success'
|
||||
|
||||
- if @cluster.managed?
|
||||
.sub-section.form-group
|
||||
|
|
@ -33,8 +32,7 @@
|
|||
= s_('ClusterIntegration|Clear cluster cache')
|
||||
%p
|
||||
= s_("ClusterIntegration|Clear the local cache of namespace and service accounts. This is necessary if your integration has become out of sync. The cache is repopulated during the next CI job that requires namespace and service accounts.")
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= link_to(s_('ClusterIntegration|Clear cluster cache'), clusterable.clear_cluster_cache_path(@cluster), method: :delete, class: 'btn btn-primary')
|
||||
= link_to(s_('ClusterIntegration|Clear cluster cache'), clusterable.clear_cluster_cache_path(@cluster), method: :delete, class: 'btn btn-primary')
|
||||
|
||||
.sub-section.form-group
|
||||
%h4.text-danger
|
||||
|
|
|
|||
|
|
@ -48,5 +48,5 @@
|
|||
- if cluster.allow_user_defined_namespace?
|
||||
= render('clusters/clusters/namespace', platform_field: platform_field)
|
||||
|
||||
.form-group.gl-display-flex.gl-justify-content-end
|
||||
.form-group
|
||||
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
|
||||
|
|
|
|||
|
|
@ -22,8 +22,7 @@
|
|||
pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
|
||||
title: s_('GroupSettings|Please choose a group URL with no special characters.'),
|
||||
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit s_('GroupSettings|Change group URL'), class: 'btn btn-warning'
|
||||
= f.submit s_('GroupSettings|Change group URL'), class: 'btn btn-warning'
|
||||
|
||||
.sub-section
|
||||
%h4.warning-title= s_('GroupSettings|Transfer group')
|
||||
|
|
@ -39,8 +38,7 @@
|
|||
%li= s_('GroupSettings|You can only transfer the group to a group you manage.')
|
||||
%li= s_('GroupSettings|You will need to update your local repositories to point to the new location.')
|
||||
%li= s_("GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.")
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit s_('GroupSettings|Transfer group'), class: 'btn btn-warning'
|
||||
= f.submit s_('GroupSettings|Transfer group'), class: 'btn btn-warning'
|
||||
|
||||
= render 'groups/settings/remove', group: @group
|
||||
= render_if_exists 'groups/settings/restore', group: @group
|
||||
|
|
|
|||
|
|
@ -24,6 +24,5 @@
|
|||
= link_to _('Download export'), download_export_group_path(group),
|
||||
rel: 'nofollow', method: :get, class: 'btn btn-default', data: { qa_selector: 'download_export_link' }
|
||||
- else
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= link_to _('Export group'), export_group_path(group),
|
||||
method: :post, class: 'btn btn-default', data: { qa_selector: 'export_group_link' }
|
||||
= link_to _('Export group'), export_group_path(group),
|
||||
method: :post, class: 'btn btn-default', data: { qa_selector: 'export_group_link' }
|
||||
|
|
|
|||
|
|
@ -29,5 +29,4 @@
|
|||
= link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link'
|
||||
|
||||
= render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Save changes'), class: 'btn btn-success mt-4 js-dirty-submit', data: { qa_selector: 'save_name_visibility_settings_button' }
|
||||
= f.submit _('Save changes'), class: 'btn btn-success mt-4 js-dirty-submit', data: { qa_selector: 'save_name_visibility_settings_button' }
|
||||
|
|
|
|||
|
|
@ -5,5 +5,4 @@
|
|||
= _('Removing this group also removes all child projects, including archived projects, and their resources.')
|
||||
%br
|
||||
%strong= _('Removed group can not be restored!')
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= button_to _('Remove group'), '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(group) }
|
||||
= button_to _('Remove group'), '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(group) }
|
||||
|
|
|
|||
|
|
@ -41,5 +41,4 @@
|
|||
= render 'groups/settings/two_factor_auth', f: f
|
||||
= render_if_exists 'groups/personal_access_token_expiration_policy', f: f, group: @group
|
||||
= render_if_exists 'groups/member_lock_setting', f: f, group: @group
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Save changes'), class: 'btn btn-success gl-mt-3 js-dirty-submit', data: { qa_selector: 'save_permissions_changes_button' }
|
||||
= f.submit _('Save changes'), class: 'btn btn-success gl-mt-3 js-dirty-submit', data: { qa_selector: 'save_permissions_changes_button' }
|
||||
|
|
|
|||
|
|
@ -26,6 +26,5 @@
|
|||
= link_to _('Generate new export'), generate_new_export_project_path(project),
|
||||
method: :post, class: "btn btn-default"
|
||||
- else
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= link_to _('Export project'), export_project_path(project),
|
||||
method: :post, class: "btn btn-default", data: { qa_selector: 'export_project_link' }
|
||||
= link_to _('Export project'), export_project_path(project),
|
||||
method: :post, class: "btn btn-default", data: { qa_selector: 'export_project_link' }
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
= ("To confirm, type %{phrase_code}").html_safe % { phrase_code: '<code class="js-confirm-danger-match">%{phrase_name}</code>'.html_safe % { phrase_name: @project.full_path } }
|
||||
.form-group
|
||||
= text_field_tag 'confirm_path_input', '', class: 'form-control js-confirm-danger-input qa-confirm-input'
|
||||
.form-actions.gl-display-flex.gl-justify-content-end
|
||||
.form-actions
|
||||
%button.btn.btn-default.gl-mr-4{ type: "button", "data-dismiss": "modal" }
|
||||
= _('Cancel')
|
||||
= submit_tag _('Reduce project visibility'), class: "btn btn-danger js-confirm-danger-submit qa-confirm-button", disabled: true
|
||||
|
|
|
|||
|
|
@ -26,5 +26,4 @@
|
|||
.form-text.text-muted
|
||||
= _("The maximum file size allowed is %{size}.") % { size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes) }
|
||||
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Start cleanup'), class: 'btn btn-success'
|
||||
= f.submit _('Start cleanup'), class: 'btn btn-success'
|
||||
|
|
|
|||
|
|
@ -28,5 +28,4 @@
|
|||
= _("Issues referenced by merge requests and commits within the default branch will be closed automatically")
|
||||
= link_to sprite_icon('question-o'), help_page_path('user/project/issues/managing_issues.md', anchor: 'disabling-automatic-issue-closing'), target: '_blank'
|
||||
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Save changes'), class: "btn btn-success"
|
||||
= f.submit _('Save changes'), class: "btn btn-success"
|
||||
|
|
|
|||
|
|
@ -21,10 +21,9 @@
|
|||
%input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
|
||||
%template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data_json(@project)
|
||||
.js-project-permissions-form
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
- if show_visibility_confirm_modal?(@project)
|
||||
= render "visibility_modal"
|
||||
= f.submit _('Save changes'), class: "btn btn-success #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: { qa_selector: 'visibility_features_permissions_save_button', check_field_name: ("project[visibility_level]" if show_visibility_confirm_modal?(@project)), check_compare_value: @project.visibility_level }
|
||||
- if show_visibility_confirm_modal?(@project)
|
||||
= render "visibility_modal"
|
||||
= f.submit _('Save changes'), class: "btn btn-success #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: { qa_selector: 'visibility_features_permissions_save_button', check_field_name: ("project[visibility_level]" if show_visibility_confirm_modal?(@project)), check_compare_value: @project.visibility_level }
|
||||
|
||||
%section.qa-merge-request-settings.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
|
||||
.settings-header
|
||||
|
|
@ -38,8 +37,7 @@
|
|||
= form_for @project, remote: true, html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f|
|
||||
%input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' }
|
||||
= render 'projects/merge_request_settings', form: f
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Save changes'), class: "btn btn-success qa-save-merge-request-changes rspec-save-merge-request-changes"
|
||||
= f.submit _('Save changes'), class: "btn btn-succes qa-save-merge-request-changes rspec-save-merge-request-changes"
|
||||
|
||||
= render_if_exists 'projects/merge_request_approvals_settings', expanded: expanded
|
||||
|
||||
|
|
@ -70,9 +68,8 @@
|
|||
.sub-section
|
||||
%h4= _('Housekeeping')
|
||||
%p= _('Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects.')
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= link_to _('Run housekeeping'), housekeeping_project_path(@project),
|
||||
method: :post, class: "btn btn-default"
|
||||
= link_to _('Run housekeeping'), housekeeping_project_path(@project),
|
||||
method: :post, class: "btn btn-default"
|
||||
|
||||
= render 'export', project: @project
|
||||
|
||||
|
|
@ -94,8 +91,7 @@
|
|||
%li= _('You will need to update your local repositories to point to the new location.')
|
||||
- if @project.deployment_platform.present?
|
||||
%li= _('Your deployment services will be broken, you will need to manually fix the services after renaming.')
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Change path'), class: "btn btn-warning qa-change-path-button"
|
||||
= f.submit _('Change path'), class: "btn btn-warning qa-change-path-button"
|
||||
|
||||
- if can?(current_user, :change_namespace, @project)
|
||||
.sub-section
|
||||
|
|
@ -111,8 +107,7 @@
|
|||
%li= _('You can only transfer the project to namespaces you manage.')
|
||||
%li= _('You will need to update your local repositories to point to the new location.')
|
||||
%li= _('Project visibility level will be changed to match namespace rules when transferring to a group.')
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger qa-transfer-button", data: { "confirm-danger-message" => transfer_project_message(@project) }
|
||||
= f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger qa-transfer-button", data: { "confirm-danger-message" => transfer_project_message(@project) }
|
||||
|
||||
- if @project.forked? && can?(current_user, :remove_fork_project, @project)
|
||||
.sub-section
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
.col-lg-8.gl-mb-3
|
||||
= form_for @hook, as: :hook, url: polymorphic_path([@project, :hooks]) do |f|
|
||||
= render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit 'Add webhook', class: 'btn btn-success'
|
||||
= f.submit 'Add webhook', class: 'btn btn-success'
|
||||
|
||||
= render 'shared/web_hooks/index', hooks: @hooks, hook_class: @hook.class
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
= label_tag :only_protected_branches, _('Only mirror protected branches'), class: 'form-check-label'
|
||||
= link_to sprite_icon('question-o'), help_page_path('user/project/protected_branches'), target: '_blank'
|
||||
|
||||
.panel-footer.gl-display-flex.gl-justify-content-end
|
||||
.panel-footer
|
||||
= f.submit _('Mirror repository'), class: 'btn btn-success js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror
|
||||
- else
|
||||
.gl-alert.gl-alert-info{ role: 'alert' }
|
||||
|
|
|
|||
|
|
@ -24,5 +24,5 @@
|
|||
.create_access_levels-container
|
||||
= yield :create_access_levels
|
||||
|
||||
.card-footer.gl-display-flex.gl-justify-content-end
|
||||
.card-footer
|
||||
= f.submit _('Protect'), class: 'btn-success btn', disabled: true, data: { qa_selector: 'protect_tag_button' }
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
method: :post, class: "btn btn-success"
|
||||
- else
|
||||
%p= _("Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= link_to _('Archive project'), archive_project_path(@project),
|
||||
data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' },
|
||||
method: :post, class: "btn btn-warning"
|
||||
= link_to _('Archive project'), archive_project_path(@project),
|
||||
data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' },
|
||||
method: :post, class: "btn btn-warning"
|
||||
|
|
|
|||
|
|
@ -40,5 +40,4 @@
|
|||
%hr
|
||||
= link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link'
|
||||
|
||||
.gl-display-flex.gl-justify-content-end
|
||||
= f.submit _('Save changes'), class: "btn btn-success mt-4 qa-save-naming-topics-avatar-button"
|
||||
= f.submit _('Save changes'), class: "btn btn-success mt-4 qa-save-naming-topics-avatar-button"
|
||||
|
|
|
|||
|
|
@ -17,5 +17,5 @@
|
|||
|
||||
.form-group
|
||||
= text_field_tag 'confirm_name_input', '', class: 'form-control js-confirm-danger-input qa-confirm-input'
|
||||
.form-actions.gl-display-flex.gl-justify-content-end
|
||||
.form-actions
|
||||
= submit_tag _('Confirm'), class: "btn btn-danger js-confirm-danger-submit qa-confirm-button"
|
||||
|
|
|
|||
|
|
@ -20,5 +20,5 @@
|
|||
%p.light.gl-mb-0
|
||||
= _('Allow this key to push to repository as well? (Default only allows pull access.)')
|
||||
|
||||
.form-group.row.gl-display-flex.gl-justify-content-end
|
||||
.form-group.row
|
||||
= f.submit _("Add key"), class: "btn-success btn"
|
||||
|
|
|
|||
|
|
@ -46,5 +46,5 @@
|
|||
= label_tag ("deploy_token_write_package_registry"), 'write_package_registry', class: 'label-bold form-check-label'
|
||||
.text-secondary= s_('DeployTokens|Allows write access to the package registry')
|
||||
|
||||
.gl-mt-3.gl-display-flex.gl-justify-content-end
|
||||
.gl-mt-3
|
||||
= f.submit s_('DeployTokens|Create deploy token'), class: 'btn btn-success qa-create-deploy-token'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update lock form buttons to gl-button
|
||||
merge_request: 41454
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Prevent assignment of groups using quick actions
|
||||
merge_request: 42810
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Revert justified-content-end settings buttons
|
||||
merge_request: 42273
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow Unleash clients to request feature flags when repository is private
|
||||
merge_request: 43059
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: ci_daily_code_coverage
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: ci_download_daily_code_coverage
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: not_issuable_queries
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27639
|
||||
rollout_issue_url:
|
||||
group: group::project management
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: pipelines_security_report_summary
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31136
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235943
|
||||
group: group::dynamic analysis
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: reactive_caching_limit_environment
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34202
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/202633
|
||||
group: group::configure
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ Kubernetes-specific environment variables are detailed in the
|
|||
| `CI_JOB_MANUAL` | 8.12 | all | The flag to indicate that job was manually started |
|
||||
| `CI_JOB_NAME` | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
|
||||
| `CI_JOB_STAGE` | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
|
||||
| `CI_JOB_STATUS` | all | 13.5 | The state of the job as each runner stage is executed. Use with [`after_script`](../yaml/README.md#before_script-and-after_script) where `CI_JOB_STATUS` can be either: `success`, `failed` or `canceled`. |
|
||||
| `CI_JOB_TOKEN` | 9.0 | 1.2 | Token used for authenticating with the [GitLab Container Registry](../../user/packages/container_registry/index.md), downloading [dependent repositories](../../user/project/new_ci_build_permissions_model.md#dependent-repositories), and accessing [GitLab-managed Terraform state](../../user/infrastructure/index.md#gitlab-managed-terraform-state). |
|
||||
| `CI_JOB_JWT` | 12.10 | all | RS256 JSON web token that can be used for authenticating with third party systems that support JWT authentication, for example [HashiCorp's Vault](../secrets/index.md). |
|
||||
| `CI_JOB_URL` | 11.1 | 0.5 | Job details URL |
|
||||
|
|
|
|||
|
|
@ -369,7 +369,7 @@ create an issue or an MR to propose a change to the user interface text.
|
|||
- milestones
|
||||
- reorder issues
|
||||
- runner, runners, shared runners
|
||||
- a to-do, to-dos
|
||||
- a to-do item, to dos
|
||||
- *Some features are capitalized*, typically nouns naming GitLab-specific
|
||||
capabilities or tools. For example:
|
||||
- GitLab CI/CD
|
||||
|
|
|
|||
|
|
@ -159,11 +159,21 @@ found in those projects' default branches.
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213014) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
|
||||
|
||||
You can export all your vulnerabilities in CSV format by clicking the **{upload}** **Export**
|
||||
button located at top right of the **Security Dashboard**. After the report
|
||||
is built, the CSV report downloads to your local machine. The report contains all
|
||||
vulnerabilities for the projects defined in the **Security Dashboard**,
|
||||
as filters don't apply to the export function.
|
||||
You can export all your vulnerabilities in CSV (comma separated values) format by clicking the
|
||||
**{upload}** **Export** button located at top right of the Security Dashboard. When the report is
|
||||
ready, the CSV report downloads to your local machine. The report contains all vulnerabilities for
|
||||
the projects defined in the Security Dashboard, as filters don't apply to the export function.
|
||||
|
||||
The fields in the export include:
|
||||
|
||||
- Scanner Type
|
||||
- Scanner Name
|
||||
- Status
|
||||
- Name
|
||||
- Details
|
||||
- Severity
|
||||
- [CVE](https://cve.mitre.org/)
|
||||
- Additional Info
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ conjunction with [GitLab CI/CD](../../ci/README.md), the open-source continuous
|
|||
integration service included with GitLab that coordinates the jobs.
|
||||
|
||||
If the project is on GitLab.com, [shared runners](../gitlab_com/index.md#shared-runners)
|
||||
are available, and you do not have to deploy one if they are enough for your
|
||||
are available. You don't have to deploy one if they are enough for your
|
||||
needs. If a project-specific runner is desired, or there are no shared runners,
|
||||
you can deploy one.
|
||||
|
||||
|
|
@ -134,11 +134,13 @@ file. Customizing the installation by modifying this file is not supported.
|
|||
> - Introduced in GitLab 10.2 for project-level clusters.
|
||||
> - Introduced in GitLab 11.6 for group-level clusters.
|
||||
|
||||
[Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) provides load balancing, SSL termination, and name-based virtual hosting
|
||||
[Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/)
|
||||
provides load balancing, SSL termination, and name-based virtual hosting
|
||||
out of the box. It acts as a web proxy for your applications and is useful
|
||||
if you want to use [Auto DevOps](../../topics/autodevops/index.md) or deploy your own web apps.
|
||||
|
||||
The Ingress Controller installed is [Ingress-NGINX](https://kubernetes.io/docs/concepts/services-networking/ingress/),
|
||||
The Ingress Controller installed is
|
||||
[Ingress-NGINX](https://kubernetes.io/docs/concepts/services-networking/ingress/),
|
||||
which is supported by the Kubernetes community.
|
||||
|
||||
NOTE: **Note:**
|
||||
|
|
@ -146,10 +148,10 @@ With the following procedure, a load balancer must be installed in your cluster
|
|||
to obtain the endpoint. You can use either
|
||||
Ingress, or Knative's own load balancer ([Istio](https://istio.io)) if using Knative.
|
||||
|
||||
In order to publish your web application, you first need to find the endpoint which will be either an IP
|
||||
To publish your web application, you first need to find the endpoint, which is either an IP
|
||||
address or a hostname associated with your load balancer.
|
||||
|
||||
To install it, click on the **Install** button for Ingress. GitLab will attempt
|
||||
To install it, click on the **Install** button for Ingress. GitLab attempts
|
||||
to determine the external endpoint and it should be available within a few minutes.
|
||||
|
||||
#### Determining the external endpoint automatically
|
||||
|
|
@ -165,11 +167,15 @@ using the `KUBE_INGRESS_BASE_DOMAIN` environment variable.
|
|||
|
||||
If the endpoint doesn't appear and your cluster runs on Google Kubernetes Engine:
|
||||
|
||||
1. Check your [Kubernetes cluster on Google Kubernetes Engine](https://console.cloud.google.com/kubernetes) to ensure there are no errors on its nodes.
|
||||
1. Ensure you have enough [Quotas](https://console.cloud.google.com/iam-admin/quotas) on Google Kubernetes Engine. For more information, see [Resource Quotas](https://cloud.google.com/compute/quotas).
|
||||
1. Check [Google Cloud's Status](https://status.cloud.google.com/) to ensure they are not having any disruptions.
|
||||
1. [Examine your Kubernetes cluster](https://console.cloud.google.com/kubernetes)
|
||||
on Google Kubernetes Engine to ensure there are no errors on its nodes.
|
||||
1. Ensure you have enough [Quotas](https://console.cloud.google.com/iam-admin/quotas)
|
||||
on Google Kubernetes Engine. For more information, see
|
||||
[Resource Quotas](https://cloud.google.com/compute/quotas).
|
||||
1. Review [Google Cloud's Status](https://status.cloud.google.com/) for service
|
||||
disruptions.
|
||||
|
||||
Once installed, you may see a `?` for "Ingress IP Address" depending on the
|
||||
After installing, you may see a `?` for **Ingress IP Address** depending on the
|
||||
cloud provider. For EKS specifically, this is because the ELB is created
|
||||
with a DNS name, not an IP address. If GitLab is still unable to
|
||||
determine the endpoint of your Ingress or Knative application, you can
|
||||
|
|
@ -195,58 +201,58 @@ The output of the following examples will show the external endpoint of your
|
|||
cluster. This information can then be used to set up DNS entries and forwarding
|
||||
rules that allow external access to your deployed applications.
|
||||
|
||||
If you installed Ingress via the **Applications**, run the following command:
|
||||
- If you installed Ingress using the **Applications**, run the following
|
||||
command:
|
||||
|
||||
```shell
|
||||
kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
|
||||
```
|
||||
```shell
|
||||
kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
|
||||
```
|
||||
|
||||
Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run:
|
||||
- Some Kubernetes clusters return a hostname instead, like
|
||||
[Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run:
|
||||
|
||||
```shell
|
||||
kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
|
||||
```
|
||||
```shell
|
||||
kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
|
||||
```
|
||||
|
||||
For Istio/Knative, the command will be different:
|
||||
If EKS is used, an [Elastic Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/)
|
||||
is also created, which will incur additional AWS costs.
|
||||
|
||||
```shell
|
||||
kubectl get svc --namespace=istio-system istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
|
||||
```
|
||||
- For Istio/Knative, the command will be different:
|
||||
|
||||
Otherwise, you can list the IP addresses of all load balancers:
|
||||
```shell
|
||||
kubectl get svc --namespace=istio-system istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
|
||||
```
|
||||
|
||||
```shell
|
||||
kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalancer.ingress)]}{.status.loadBalancer.ingress[*].ip} '
|
||||
```
|
||||
- Otherwise, you can list the IP addresses of all load balancers:
|
||||
|
||||
NOTE: **Note:**
|
||||
If EKS is used, an [Elastic Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/)
|
||||
will also be created, which will incur additional AWS costs.
|
||||
```shell
|
||||
kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalancer.ingress)]}{.status.loadBalancer.ingress[*].ip} '
|
||||
```
|
||||
|
||||
NOTE: **Note:**
|
||||
You may see a trailing `%` on some Kubernetes versions, **do not include it**.
|
||||
You may see a trailing `%` on some Kubernetes versions. Do not include it.
|
||||
|
||||
The Ingress is now available at this address and will route incoming requests to
|
||||
the proper service based on the DNS name in the request. To support this, a
|
||||
wildcard DNS CNAME record should be created for the desired domain name. For example,
|
||||
The Ingress is now available at this address, and routes incoming requests to
|
||||
the proper service based on the DNS name in the request. To support this, create
|
||||
a wildcard DNS CNAME record for the desired domain name. For example,
|
||||
`*.myekscluster.com` would point to the Ingress hostname obtained earlier.
|
||||
|
||||
#### Using a static IP
|
||||
|
||||
By default, an ephemeral external IP address is associated to the cluster's load
|
||||
balancer. If you associate the ephemeral IP with your DNS and the IP changes,
|
||||
your apps will not be able to be reached, and you'd have to change the DNS
|
||||
record again. In order to avoid that, you should change it into a static
|
||||
reserved IP.
|
||||
your apps won't be reachable, and you'd have to change the DNS record again.
|
||||
To avoid that, change it into a static reserved IP.
|
||||
|
||||
Read how to [promote an ephemeral external IP address in GKE](https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#promote_ephemeral_ip).
|
||||
|
||||
#### Pointing your DNS at the external endpoint
|
||||
|
||||
Once you've set up the external endpoint, you should associate it with a [wildcard DNS
|
||||
record](https://en.wikipedia.org/wiki/Wildcard_DNS_record) such as `*.example.com.`
|
||||
in order to be able to reach your apps. If your external endpoint is an IP address,
|
||||
use an A record. If your external endpoint is a hostname, use a CNAME record.
|
||||
After you have set up the external endpoint, associate it with a
|
||||
[wildcard DNS record](https://en.wikipedia.org/wiki/Wildcard_DNS_record) (such
|
||||
as `*.example.com.`) to reach your apps. If your external endpoint is an IP
|
||||
address, use an A record. If your external endpoint is a hostname, use a CNAME
|
||||
record.
|
||||
|
||||
#### Web Application Firewall (ModSecurity)
|
||||
|
||||
|
|
@ -256,16 +262,16 @@ A Web Application Firewall (WAF) examines traffic being sent or received,
|
|||
and can block malicious traffic before it reaches your application. The benefits
|
||||
of a WAF are:
|
||||
|
||||
- Real-time security monitoring for your application
|
||||
- Logging of all your HTTP traffic to the application
|
||||
- Access control for your application
|
||||
- Highly configurable logging and blocking rules
|
||||
- Real-time security monitoring for your application.
|
||||
- Logging of all your HTTP traffic to the application.
|
||||
- Access control for your application.
|
||||
- Highly configurable logging and blocking rules.
|
||||
|
||||
Out of the box, GitLab provides you with a WAF known as [`ModSecurity`](https://www.modsecurity.org/).
|
||||
|
||||
ModSecurity is a toolkit for real-time web application monitoring, logging,
|
||||
and access control. With GitLab's offering, the [OWASP's Core Rule Set](https://www.modsecurity.org/CRS/Documentation/),
|
||||
which provides generic attack detection capabilities, is automatically applied.
|
||||
12345678901234567890123456789012345678901234567890123456789012345678901234567890
|
||||
By default, GitLab provides you with a WAF known as [`ModSecurity`](https://www.modsecurity.org/),
|
||||
which is a toolkit for real-time web application monitoring, logging, and access
|
||||
control. GitLab's offering applies the [OWASP's Core Rule Set](https://www.modsecurity.org/CRS/Documentation/),
|
||||
which provides generic attack detection capabilities.
|
||||
|
||||
This feature:
|
||||
|
||||
|
|
@ -286,57 +292,61 @@ There is a small performance overhead by enabling ModSecurity. If this is
|
|||
considered significant for your application, you can disable ModSecurity's
|
||||
rule engine for your deployed application in any of the following ways:
|
||||
|
||||
1. Setting [the deployment variable](../../topics/autodevops/index.md)
|
||||
`AUTO_DEVOPS_MODSECURITY_SEC_RULE_ENGINE` to `Off`. This will prevent ModSecurity
|
||||
from processing any requests for the given application or environment.
|
||||
|
||||
1. Switching its respective toggle to the disabled position and applying changes through the **Save changes** button. This will reinstall
|
||||
Ingress with the recent changes.
|
||||
1. Set [the deployment variable](../../topics/autodevops/index.md)
|
||||
`AUTO_DEVOPS_MODSECURITY_SEC_RULE_ENGINE` to `Off` to prevent ModSecurity
|
||||
from processing any requests for the given application or environment.
|
||||
1. Switch its respective toggle to the disabled position, and then apply changes
|
||||
by selecting **Save changes** to reinstall Ingress with the recent changes.
|
||||
|
||||

|
||||
|
||||
##### Logging and blocking modes
|
||||
|
||||
To help you tune your WAF rules, you can globally set your WAF to either
|
||||
**Logging** or **Blocking** mode:
|
||||
*Logging* or *Blocking* mode:
|
||||
|
||||
- **Logging mode** - Allows traffic matching the rule to pass, and logs the event.
|
||||
- **Blocking mode** - Prevents traffic matching the rule from passing, and logs the event.
|
||||
- *Logging mode*: Allows traffic matching the rule to pass, and logs the event.
|
||||
- *Blocking mode*: Prevents traffic matching the rule from passing, and logs the event.
|
||||
|
||||
To change your WAF's mode:
|
||||
|
||||
1. [Install ModSecurity](../../topics/web_application_firewall/quick_start_guide.md) if you have not already done so.
|
||||
1. If you haven't already done so, [install ModSecurity](../../topics/web_application_firewall/quick_start_guide.md).
|
||||
1. Navigate to **Operations > Kubernetes**.
|
||||
1. In **Applications**, scroll to **Ingress**.
|
||||
1. Under **Global default**, select your desired mode.
|
||||
1. Click **Save changes**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
##### WAF version updates
|
||||
|
||||
Enabling, disabling, or changing the logging mode for **ModSecurity** is only allowed within same version of [Ingress](#ingress) due to limitations in [Helm](https://helm.sh/) which might be overcome in future releases.
|
||||
Enabling, disabling, or changing the logging mode for **ModSecurity** is only
|
||||
allowed within same version of [Ingress](#ingress) due to limitations in
|
||||
[Helm](https://helm.sh/) which might be overcome in future releases.
|
||||
|
||||
**ModSecurity** UI controls are disabled if the version deployed differs from the one available in GitLab, while actions at the [Ingress](#ingress) level, such as uninstalling, can still be performed:
|
||||
**ModSecurity** user interface controls are disabled if the version deployed
|
||||
differs from the one available in GitLab, while actions at the [Ingress](#ingress)
|
||||
level, such as uninstalling, can still be performed:
|
||||
|
||||

|
||||
|
||||
Updating [Ingress](#ingress) to the most recent version enables you to take advantage of bug fixes, security fixes, and performance improvements. To update [Ingress application](#ingress), you must first uninstall it, and then re-install it as described in [Install ModSecurity](../../topics/web_application_firewall/quick_start_guide.md).
|
||||
Update [Ingress](#ingress) to the most recent version to take advantage of bug
|
||||
fixes, security fixes, and performance improvements. To update the
|
||||
[Ingress application](#ingress), you must first uninstall it, and then re-install
|
||||
it as described in [Install ModSecurity](../../topics/web_application_firewall/quick_start_guide.md).
|
||||
|
||||
##### Viewing Web Application Firewall traffic
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14707) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.9.
|
||||
|
||||
You can view Web Application Firewall traffic by navigating to your project's
|
||||
**Security & Compliance > Threat Monitoring** page.
|
||||
|
||||
From there, you can see tracked over time:
|
||||
**Security & Compliance > Threat Monitoring** page. From there, you can see
|
||||
tracked over time:
|
||||
|
||||
- The total amount of traffic to your application.
|
||||
- The proportion of traffic that is considered anomalous by the Web Application
|
||||
Firewall's default [OWASP ruleset](https://www.modsecurity.org/CRS/Documentation/).
|
||||
|
||||
If a significant percentage of traffic is anomalous, it should be investigated
|
||||
for potential threats, which can be done by
|
||||
[examining the Web Application Firewall logs](#web-application-firewall-modsecurity).
|
||||
If a significant percentage of traffic is anomalous, investigate it for potential threats
|
||||
by [examining the Web Application Firewall logs](#web-application-firewall-modsecurity).
|
||||
|
||||

|
||||
|
||||
|
|
@ -345,55 +355,51 @@ for potential threats, which can be done by
|
|||
> - Introduced in GitLab 11.0 for project-level clusters.
|
||||
> - Introduced in GitLab 12.3 for group and instance-level clusters.
|
||||
|
||||
[JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a
|
||||
multi-user service for managing notebooks across a team. [Jupyter
|
||||
Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a
|
||||
web-based interactive programming environment used for data analysis,
|
||||
[JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service
|
||||
for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/)
|
||||
provide a web-based interactive programming environment used for data analysis,
|
||||
visualization, and machine learning.
|
||||
|
||||
Authentication will be enabled only for [project
|
||||
members](../project/members/index.md) for project-level clusters and group
|
||||
members for group-level clusters with [Developer or
|
||||
higher](../permissions.md) access to the associated project or group.
|
||||
|
||||
We use a [custom Jupyter
|
||||
image](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile)
|
||||
that installs additional useful packages on top of the base Jupyter. You
|
||||
will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](https://github.com/Nurtch/rubix).
|
||||
|
||||
More information on
|
||||
creating executable runbooks can be found in [our Runbooks
|
||||
documentation](../project/clusters/runbooks/index.md#configure-an-executable-runbook-with-gitlab). Note that
|
||||
Ingress must be installed and have an IP address assigned before
|
||||
JupyterHub can be installed.
|
||||
|
||||
NOTE: **Note:**
|
||||
The [`jupyter/jupyterhub`](https://jupyterhub.github.io/helm-chart/)
|
||||
chart is used to install this application with a
|
||||
[`values.yaml`](https://gitlab.com/gitlab-org/gitlab/blob/master/vendor/jupyter/values.yaml)
|
||||
file.
|
||||
|
||||
Authentication is enabled only for [project members](../project/members/index.md)
|
||||
for project-level clusters and group members for group-level clusters with
|
||||
[Developer or higher](../permissions.md) access to the associated project or group.
|
||||
|
||||
GitLab uses a [custom Jupyter image](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile)
|
||||
that installs additional useful packages on top of the base Jupyter. You
|
||||
will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](https://github.com/Nurtch/rubix).
|
||||
|
||||
More information on creating executable runbooks can be found in
|
||||
[our Runbooks documentation](../project/clusters/runbooks/index.md#configure-an-executable-runbook-with-gitlab).
|
||||
Ingress must be installed and have an IP address assigned before
|
||||
JupyterHub can be installed.
|
||||
|
||||
#### Jupyter Git Integration
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/28783) in GitLab 12.0 for project-level clusters.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/32512) in GitLab 12.3 for group and instance-level clusters.
|
||||
|
||||
When installing JupyterHub onto your Kubernetes cluster, [JupyterLab's Git extension](https://github.com/jupyterlab/jupyterlab-git)
|
||||
is automatically provisioned and configured using the authenticated user's:
|
||||
When installing JupyterHub onto your Kubernetes cluster,
|
||||
[JupyterLab's Git extension](https://github.com/jupyterlab/jupyterlab-git)
|
||||
is provisioned and configured using the authenticated user's:
|
||||
|
||||
- Name.
|
||||
- Email.
|
||||
- Newly created access token.
|
||||
|
||||
JupyterLab's Git extension enables full version control of your notebooks as well as issuance of Git commands within Jupyter.
|
||||
Git commands can be issued via the **Git** tab on the left panel or via Jupyter's command line prompt.
|
||||
JupyterLab's Git extension enables full version control of your notebooks, and
|
||||
issuance of Git commands within Jupyter. You can issue Git commands through the
|
||||
**Git** tab on the left panel, or through Jupyter's command-line prompt.
|
||||
|
||||
NOTE: **Note:**
|
||||
JupyterLab's Git extension stores the user token in the JupyterHub DB in encrypted format
|
||||
and in the single user Jupyter instance as plain text. This is because [Git requires storing
|
||||
credentials as plain text](https://git-scm.com/docs/git-credential-store). Potentially, if
|
||||
a nefarious user finds a way to read from the file system in the single user Jupyter instance
|
||||
they could retrieve the token.
|
||||
JupyterLab's Git extension stores the user token in the JupyterHub DB in encrypted
|
||||
format, and in the single user Jupyter instance as plain text, because
|
||||
[Git requires storing credentials as plain text](https://git-scm.com/docs/git-credential-store)
|
||||
Potentially, if a nefarious user finds a way to read from the file system in the
|
||||
single-user Jupyter instance, they could retrieve the token.
|
||||
|
||||

|
||||
|
||||
|
|
@ -412,18 +418,16 @@ cluster. It is used in conjunction with, and includes
|
|||
[Istio](https://istio.io) to provide an external IP address for all
|
||||
programs hosted by Knative.
|
||||
|
||||
You will be prompted to enter a wildcard
|
||||
domain where your applications will be exposed. Configure your DNS
|
||||
server to use the external IP address for that domain. For any
|
||||
application created and installed, they will be accessible as
|
||||
`<program_name>.<kubernetes_namespace>.<domain_name>`. This will require
|
||||
your Kubernetes cluster to have [RBAC
|
||||
enabled](../project/clusters/add_remove_clusters.md#rbac-cluster-resources).
|
||||
|
||||
NOTE: **Note:**
|
||||
The [`knative/knative`](https://storage.googleapis.com/triggermesh-charts)
|
||||
chart is used to install this application.
|
||||
|
||||
During installation, you must enter a wildcard domain where your applications
|
||||
will be exposed. Configure your DNS server to use the external IP address for that
|
||||
domain. Applications created and installed are accessible as
|
||||
`<program_name>.<kubernetes_namespace>.<domain_name>`, which requires
|
||||
your Kubernetes cluster to have
|
||||
[RBAC enabled](../project/clusters/add_remove_clusters.md#rbac-cluster-resources).
|
||||
|
||||
### Prometheus
|
||||
|
||||
> - Introduced in GitLab 10.4 for project-level clusters.
|
||||
|
|
@ -438,15 +442,14 @@ GitLab is able to monitor applications automatically, using the
|
|||
memory metrics are automatically collected, and response metrics are retrieved
|
||||
from NGINX Ingress as well.
|
||||
|
||||
To enable monitoring, simply install Prometheus into the cluster with the
|
||||
**Install** button.
|
||||
|
||||
NOTE: **Note:**
|
||||
The [`stable/prometheus`](https://github.com/helm/charts/tree/master/stable/prometheus)
|
||||
chart is used to install this application with a
|
||||
[`values.yaml`](https://gitlab.com/gitlab-org/gitlab/blob/master/vendor/prometheus/values.yaml)
|
||||
file.
|
||||
|
||||
To enable monitoring, install Prometheus into the cluster with the **Install**
|
||||
button.
|
||||
|
||||
### Crossplane
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34702) in GitLab 12.5 for project-level clusters.
|
||||
|
|
@ -470,15 +473,14 @@ The Crossplane GitLab-managed application:
|
|||
PostgreSQL (for example, CloudSQL from GCP or RDS from AWS) and other services
|
||||
required by the application via the Auto DevOps pipeline.
|
||||
|
||||
For information on configuring Crossplane installed on the cluster, see
|
||||
[Crossplane configuration](crossplane.md).
|
||||
|
||||
NOTE: **Note:**
|
||||
[`alpha/crossplane`](https://github.com/crossplane/crossplane/tree/v0.4.1/cluster/charts/crossplane) chart v0.4.1 is used to
|
||||
install Crossplane using the
|
||||
[`values.yaml`](https://github.com/crossplane/crossplane/blob/master/cluster/charts/crossplane/values.yaml.tmpl)
|
||||
file.
|
||||
|
||||
For information about configuring Crossplane installed on the cluster, see
|
||||
[Crossplane configuration](crossplane.md).
|
||||
|
||||
### Elastic Stack
|
||||
|
||||
> Introduced in GitLab 12.7 for project- and group-level clusters.
|
||||
|
|
@ -487,37 +489,33 @@ file.
|
|||
log analysis solution which helps in deep searching, analyzing and visualizing the logs
|
||||
generated from different machines.
|
||||
|
||||
GitLab is able to gather logs from pods in your cluster automatically.
|
||||
Filebeat will run as a DaemonSet on each node in your cluster, and it will ship container logs to Elasticsearch for querying.
|
||||
GitLab will then connect to Elasticsearch for logs instead of the Kubernetes API,
|
||||
and you will have access to more advanced querying capabilities.
|
||||
|
||||
Log data is automatically deleted after 30 days using [Curator](https://www.elastic.co/guide/en/elasticsearch/client/curator/5.5/about.html).
|
||||
GitLab can gather logs from pods in your cluster. Filebeat runs as a DaemonSet
|
||||
on each node in your cluster, and ships container logs to Elasticsearch for
|
||||
querying. GitLab then connects to Elasticsearch for logs, instead of the
|
||||
Kubernetes API, giving you access to more advanced querying capabilities. Log
|
||||
data is deleted after 30 days, using [Curator](https://www.elastic.co/guide/en/elasticsearch/client/curator/5.5/about.html).
|
||||
|
||||
To enable log shipping:
|
||||
|
||||
1. Ensure your cluster contains at least 3 nodes of instance types larger than
|
||||
`f1-micro`, `g1-small`, or `n1-standard-1`.
|
||||
1. Ensure your cluster contains at least three nodes of instance types larger
|
||||
than `f1-micro`, `g1-small`, or `n1-standard-1`.
|
||||
1. Navigate to **Operations > Kubernetes**.
|
||||
1. In **Kubernetes Cluster**, select a cluster.
|
||||
1. In the **Applications** section, find **Elastic Stack** and click **Install**.
|
||||
1. In the **Applications** section, find **Elastic Stack**, and then select
|
||||
**Install**.
|
||||
|
||||
NOTE: **Note:**
|
||||
The [`gitlab/elastic-stack`](https://gitlab.com/gitlab-org/charts/elastic-stack)
|
||||
chart is used to install this application with a
|
||||
[`values.yaml`](https://gitlab.com/gitlab-org/gitlab/blob/master/vendor/elastic_stack/values.yaml)
|
||||
file.
|
||||
file. The chart deploys three identical Elasticsearch pods which can't be
|
||||
colocated, and each requires one CPU and 2 GB of RAM, making them
|
||||
incompatible with clusters containing fewer than three nodes, or consisting of
|
||||
`f1-micro`, `g1-small`, `n1-standard-1`, or `*-highcpu-2` instance types.
|
||||
|
||||
NOTE: **Note:**
|
||||
The chart deploys 3 identical Elasticsearch pods which can't be colocated, and each
|
||||
requires 1 CPU and 2 GB of RAM, making them incompatible with clusters containing
|
||||
fewer than 3 nodes or consisting of `f1-micro`, `g1-small`, `n1-standard-1`, or
|
||||
`*-highcpu-2` instance types.
|
||||
|
||||
NOTE: **Note:**
|
||||
The Elastic Stack cluster application is intended as a log aggregation solution and is not related to our
|
||||
[Advanced Search](../search/advanced_global_search.md) functionality, which uses a separate
|
||||
Elasticsearch cluster.
|
||||
The Elastic Stack cluster application is intended as a log aggregation solution
|
||||
and is not related to our [Advanced Search](../search/advanced_global_search.md)
|
||||
functionality, which uses a separate Elasticsearch cluster.
|
||||
|
||||
#### Optional: deploy Kibana to perform advanced queries
|
||||
|
||||
|
|
@ -1267,7 +1265,7 @@ You can check the default [`values.yaml`](https://gitlab.com/gitlab-org/gitlab/-
|
|||
You can customize the installation of Elastic Stack by defining
|
||||
`.gitlab/managed-apps/elastic-stack/values.yaml` file in your cluster
|
||||
management project. Refer to the
|
||||
[chart](https://gitlab.com/gitlab-org/charts/elastic-stack) for the
|
||||
[chart](https://gitlab.com/gitlab-org/charts/elastic-stack) for all
|
||||
available configuration options.
|
||||
|
||||
NOTE: **Note:**
|
||||
|
|
@ -1324,7 +1322,7 @@ You can customize the installation of Fluentd by defining
|
|||
`.gitlab/managed-apps/fluentd/values.yaml` file in your cluster management
|
||||
project. Refer to the
|
||||
[configuration chart for the current development release of Fluentd](https://github.com/helm/charts/tree/master/stable/fluentd#configuration)
|
||||
for the available configuration options.
|
||||
for all available configuration options.
|
||||
|
||||
NOTE: **Note:**
|
||||
The configuration chart link points to the current development release, which
|
||||
|
|
@ -1347,7 +1345,7 @@ knative:
|
|||
|
||||
You can customize the installation of Knative by defining `.gitlab/managed-apps/knative/values.yaml`
|
||||
file in your cluster management project. Refer to the [chart](https://gitlab.com/gitlab-org/charts/knative)
|
||||
for the available configuration options.
|
||||
for all available configuration options.
|
||||
|
||||
Here is an example configuration for Knative:
|
||||
|
||||
|
|
|
|||
|
|
@ -286,6 +286,22 @@ By default, when an NPM package is not found in the GitLab NPM Registry, the req
|
|||
|
||||
Administrators can disable this behavior in the [Continuous Integration settings](../../admin_area/settings/continuous_integration.md).
|
||||
|
||||
### Installing packages from other organizations
|
||||
|
||||
You can route package requests to organizations and users outside of GitLab.
|
||||
|
||||
To do this, add lines to your `.npmrc` file, replacing `my-org` with the namespace or group that owns your project's repository. The name is case-sensitive and must match the name of your group or namespace exactly.
|
||||
|
||||
```shell
|
||||
@foo:registry=https://gitlab.example.com/api/v4/packages/npm/
|
||||
//gitlab.com/api/v4/packages/npm/:_authToken=
|
||||
//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken= "<your_token>"
|
||||
|
||||
@my-other-org:registry=https://gitlab.example.com/api/v4/packages/npm/
|
||||
//gitlab.com/api/v4/packages/npm/:_authToken=
|
||||
//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken= "<your_token>"
|
||||
```
|
||||
|
||||
## Removing a package
|
||||
|
||||
In the packages view of your project page, you can delete packages by clicking
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ which visibility level you select on project settings.
|
|||
|
||||
- Disabled: disabled for everyone
|
||||
- Only team members: only team members can see even if your project is public or internal
|
||||
- Everyone with access: everyone can see depending on your project visibility level
|
||||
- Everyone with access: everyone can see depending on your project's visibility level
|
||||
- Everyone: enabled for everyone (only available for GitLab Pages)
|
||||
|
||||
### Protected branches
|
||||
|
|
|
|||
|
|
@ -10966,6 +10966,9 @@ msgstr ""
|
|||
msgid "FeatureFlags|Loading feature flags"
|
||||
msgstr ""
|
||||
|
||||
msgid "FeatureFlags|Loading user lists"
|
||||
msgstr ""
|
||||
|
||||
msgid "FeatureFlags|More information"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -10984,7 +10987,7 @@ msgstr ""
|
|||
msgid "FeatureFlags|New feature flag"
|
||||
msgstr ""
|
||||
|
||||
msgid "FeatureFlags|New list"
|
||||
msgid "FeatureFlags|New user list"
|
||||
msgstr ""
|
||||
|
||||
msgid "FeatureFlags|Percent of users"
|
||||
|
|
@ -11023,6 +11026,9 @@ msgstr ""
|
|||
msgid "FeatureFlags|There was an error fetching the feature flags."
|
||||
msgstr ""
|
||||
|
||||
msgid "FeatureFlags|There was an error fetching the user lists."
|
||||
msgstr ""
|
||||
|
||||
msgid "FeatureFlags|There was an error retrieving user lists"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -11038,6 +11044,9 @@ msgstr ""
|
|||
msgid "FeatureFlags|User List"
|
||||
msgstr ""
|
||||
|
||||
msgid "FeatureFlags|User Lists"
|
||||
msgstr ""
|
||||
|
||||
msgid "FeatureFlag|List"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15114,9 +15123,6 @@ msgstr ""
|
|||
msgid "List your Bitbucket Server repositories"
|
||||
msgstr ""
|
||||
|
||||
msgid "Lists"
|
||||
msgstr ""
|
||||
|
||||
msgid "Live preview"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Plan' do
|
||||
RSpec.describe 'Plan', :reliable do
|
||||
describe 'Milestones' do
|
||||
include Support::Dates
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Plan' do
|
||||
RSpec.describe 'Plan', :reliable do
|
||||
describe 'Group milestone' do
|
||||
include Support::Dates
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Plan' do
|
||||
RSpec.describe 'Plan', :reliable do
|
||||
describe 'Project milestone' do
|
||||
include Support::Dates
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
it 'creates a merge request with a milestone and label', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/514' do
|
||||
it 'creates a merge request with a milestone and label', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/514', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/254988', type: :stale } do
|
||||
gitlab_account_username = "@#{Runtime::User.username}"
|
||||
|
||||
milestone = Resource::ProjectMilestone.fabricate_via_api! do |milestone|
|
||||
|
|
|
|||
|
|
@ -295,18 +295,21 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
end
|
||||
|
||||
context 'assignees' do
|
||||
let(:issue_assignee) { create(:issue, project: project) }
|
||||
let(:unassigned_user) { create(:user) }
|
||||
let_it_be(:issue_assignee) { create(:issue, project: project, assignees: [user]) }
|
||||
let_it_be(:unassigned_user) { create(:user) }
|
||||
|
||||
before do
|
||||
issue_assignee.update(assignees: [user])
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
before_all do
|
||||
project.add_maintainer(unassigned_user)
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
it 'lists users who are currently not assigned to the issue when using /assign' do
|
||||
visit project_issue_path(project, issue_assignee)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
note = find('#note-body')
|
||||
page.within '.timeline-content-form' do
|
||||
note.native.send_keys('/as')
|
||||
|
|
@ -318,6 +321,7 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
wait_for_requests
|
||||
|
||||
expect(find('#at-view-users .atwho-view-ul')).not_to have_content(user.username)
|
||||
expect(find('#at-view-users .atwho-view-ul')).not_to have_content(group.name)
|
||||
expect(find('#at-view-users .atwho-view-ul')).to have_content(unassigned_user.username)
|
||||
end
|
||||
|
||||
|
|
@ -330,6 +334,7 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
textarea.native.send_keys(:tab)
|
||||
|
||||
expect(find('#at-view-users .atwho-view-ul')).to have_content(unassigned_user.username)
|
||||
expect(find('#at-view-users .atwho-view-ul')).not_to have_content(group.name)
|
||||
expect(find('#at-view-users .atwho-view-ul')).to have_content(user.username)
|
||||
end
|
||||
end
|
||||
|
|
@ -644,18 +649,21 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
end
|
||||
|
||||
context 'assignees' do
|
||||
let(:issue_assignee) { create(:issue, project: project) }
|
||||
let(:unassigned_user) { create(:user) }
|
||||
let_it_be(:issue_assignee) { create(:issue, project: project, assignees: [user]) }
|
||||
let_it_be(:unassigned_user) { create(:user) }
|
||||
|
||||
before do
|
||||
issue_assignee.update(assignees: [user])
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
before_all do
|
||||
project.add_maintainer(unassigned_user)
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
it 'lists users who are currently not assigned to the issue when using /assign' do
|
||||
visit project_issue_path(project, issue_assignee)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
note = find('#note-body')
|
||||
page.within '.timeline-content-form' do
|
||||
note.native.send_keys('/as')
|
||||
|
|
@ -668,12 +676,15 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
wait_for_requests
|
||||
|
||||
expect(find('.tribute-container ul', visible: true)).not_to have_content(user.username)
|
||||
expect(find('.tribute-container ul', visible: true)).not_to have_content(group.name)
|
||||
expect(find('.tribute-container ul', visible: true)).to have_content(unassigned_user.username)
|
||||
end
|
||||
|
||||
it 'lists users who are currently not assigned to the issue when using /assign on the second line' do
|
||||
visit project_issue_path(project, issue_assignee)
|
||||
|
||||
wait_for_requests
|
||||
|
||||
note = find('#note-body')
|
||||
page.within '.timeline-content-form' do
|
||||
note.native.send_keys('/assign @user2')
|
||||
|
|
|
|||
|
|
@ -34,16 +34,14 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`]
|
|||
<gl-form-group-stub label=\\"Alert test payload\\" label-for=\\"alert-json\\" label-class=\\"label-bold\\">
|
||||
<gl-form-textarea-stub noresize=\\"true\\" id=\\"alert-json\\" disabled=\\"true\\" state=\\"true\\" placeholder=\\"Enter test alert JSON....\\" rows=\\"6\\" max-rows=\\"10\\"></gl-form-textarea-stub>
|
||||
</gl-form-group-stub>
|
||||
<div class=\\"gl-display-flex gl-justify-content-end\\">
|
||||
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\">Test alert payload</gl-button-stub>
|
||||
</div>
|
||||
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\">Test alert payload</gl-button-stub>
|
||||
<div class=\\"footer-block row-content-block gl-display-flex gl-justify-content-space-between\\">
|
||||
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\">
|
||||
Cancel
|
||||
</gl-button-stub>
|
||||
<gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\">
|
||||
Save changes
|
||||
</gl-button-stub>
|
||||
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\">
|
||||
Cancel
|
||||
</gl-button-stub>
|
||||
</div>
|
||||
</gl-form-stub>
|
||||
</div>"
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@ describe('GfmAutoComplete', () => {
|
|||
it('should set the text avatar if avatar_url is null', () => {
|
||||
expect(membersBeforeSave([{ ...mockGroup, avatar_url: null }])).toEqual([
|
||||
{
|
||||
type: 'Group',
|
||||
username: 'my-group',
|
||||
avatarTag: '<div class="avatar rect-avatar center avatar-inline s26">M</div>',
|
||||
title: 'My Group (2)',
|
||||
|
|
@ -290,6 +291,7 @@ describe('GfmAutoComplete', () => {
|
|||
it('should set the image avatar if avatar_url is given', () => {
|
||||
expect(membersBeforeSave([mockGroup])).toEqual([
|
||||
{
|
||||
type: 'Group',
|
||||
username: 'my-group',
|
||||
avatarTag:
|
||||
'<img src="./group.jpg" alt="my-group" class="avatar rect-avatar avatar-inline center s26"/>',
|
||||
|
|
@ -303,6 +305,7 @@ describe('GfmAutoComplete', () => {
|
|||
it('should set mentions disabled icon if mentionsDisabled is set', () => {
|
||||
expect(membersBeforeSave([{ ...mockGroup, mentionsDisabled: true }])).toEqual([
|
||||
{
|
||||
type: 'Group',
|
||||
username: 'my-group',
|
||||
avatarTag:
|
||||
'<img src="./group.jpg" alt="my-group" class="avatar rect-avatar avatar-inline center s26"/>',
|
||||
|
|
@ -321,6 +324,7 @@ describe('GfmAutoComplete', () => {
|
|||
]),
|
||||
).toEqual([
|
||||
{
|
||||
type: 'User',
|
||||
username: 'my-user',
|
||||
avatarTag:
|
||||
'<img src="./users.jpg" alt="my-user" class="avatar avatar-inline center s26"/>',
|
||||
|
|
|
|||
|
|
@ -93,21 +93,17 @@ exports[`grafana integration component default state to match the default snapsh
|
|||
</p>
|
||||
</gl-form-group-stub>
|
||||
|
||||
<div
|
||||
class="gl-display-flex gl-justify-content-end"
|
||||
<gl-button-stub
|
||||
buttontextclasses=""
|
||||
category="primary"
|
||||
icon=""
|
||||
size="medium"
|
||||
variant="success"
|
||||
>
|
||||
<gl-button-stub
|
||||
buttontextclasses=""
|
||||
category="primary"
|
||||
icon=""
|
||||
size="medium"
|
||||
variant="success"
|
||||
>
|
||||
|
||||
Save Changes
|
||||
|
||||
</gl-button-stub>
|
||||
</div>
|
||||
Save Changes
|
||||
|
||||
</gl-button-stub>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -93,24 +93,20 @@ exports[`Alert integration settings form default state should match the default
|
|||
</gl-form-checkbox-stub>
|
||||
</gl-form-group-stub>
|
||||
|
||||
<div
|
||||
class="gl-display-flex gl-justify-content-end"
|
||||
<gl-button-stub
|
||||
buttontextclasses=""
|
||||
category="primary"
|
||||
class="js-no-auto-disable"
|
||||
data-qa-selector="save_changes_button"
|
||||
icon=""
|
||||
size="medium"
|
||||
type="submit"
|
||||
variant="success"
|
||||
>
|
||||
<gl-button-stub
|
||||
buttontextclasses=""
|
||||
category="primary"
|
||||
class="js-no-auto-disable"
|
||||
data-qa-selector="save_changes_button"
|
||||
icon=""
|
||||
size="medium"
|
||||
type="submit"
|
||||
variant="success"
|
||||
>
|
||||
|
||||
Save changes
|
||||
|
||||
</gl-button-stub>
|
||||
</div>
|
||||
Save changes
|
||||
|
||||
</gl-button-stub>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -42,25 +42,21 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="gl-display-flex gl-justify-content-end"
|
||||
<gl-button-stub
|
||||
buttontextclasses=""
|
||||
category="primary"
|
||||
class="gl-mt-3"
|
||||
data-testid="webhook-reset-btn"
|
||||
icon=""
|
||||
role="button"
|
||||
size="medium"
|
||||
tabindex="0"
|
||||
variant="default"
|
||||
>
|
||||
<gl-button-stub
|
||||
buttontextclasses=""
|
||||
category="primary"
|
||||
class="gl-mt-3"
|
||||
data-testid="webhook-reset-btn"
|
||||
icon=""
|
||||
role="button"
|
||||
size="medium"
|
||||
tabindex="0"
|
||||
variant="default"
|
||||
>
|
||||
|
||||
Reset webhook URL
|
||||
|
||||
</gl-button-stub>
|
||||
</div>
|
||||
Reset webhook URL
|
||||
|
||||
</gl-button-stub>
|
||||
|
||||
<gl-modal-stub
|
||||
modalclass=""
|
||||
|
|
@ -77,23 +73,19 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
|
|||
</gl-modal-stub>
|
||||
</gl-form-group-stub>
|
||||
|
||||
<div
|
||||
class="gl-display-flex gl-justify-content-end"
|
||||
<gl-button-stub
|
||||
buttontextclasses=""
|
||||
category="primary"
|
||||
class="js-no-auto-disable"
|
||||
icon=""
|
||||
size="medium"
|
||||
type="submit"
|
||||
variant="success"
|
||||
>
|
||||
<gl-button-stub
|
||||
buttontextclasses=""
|
||||
category="primary"
|
||||
class="js-no-auto-disable"
|
||||
icon=""
|
||||
size="medium"
|
||||
type="submit"
|
||||
variant="success"
|
||||
>
|
||||
|
||||
Save changes
|
||||
|
||||
</gl-button-stub>
|
||||
</div>
|
||||
Save changes
|
||||
|
||||
</gl-button-stub>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { GlFilteredSearch } from '@gitlab/ui';
|
||||
import { GlFilteredSearch, GlButton, GlLoadingIcon } from '@gitlab/ui';
|
||||
import Api from '~/api';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
|
||||
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
|
||||
|
||||
import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
|
||||
import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
|
||||
import BlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
|
||||
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
|
||||
|
||||
import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue';
|
||||
import Store from '~/pipelines/stores/pipelines_store';
|
||||
import { pipelineWithStages, stageReply, users, mockSearch, branches } from './mock_data';
|
||||
|
|
@ -49,6 +57,20 @@ describe('Pipelines', () => {
|
|||
};
|
||||
|
||||
const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
|
||||
const findByTestId = id => wrapper.find(`[data-testid="${id}"]`);
|
||||
const findNavigationTabs = () => wrapper.find(NavigationTabs);
|
||||
const findNavigationControls = () => wrapper.find(NavigationControls);
|
||||
const findTab = tab => findByTestId(`pipelines-tab-${tab}`);
|
||||
|
||||
const findRunPipelineButton = () => findByTestId('run-pipeline-button');
|
||||
const findCiLintButton = () => findByTestId('ci-lint-button');
|
||||
const findCleanCacheButton = () => findByTestId('clear-cache-button');
|
||||
|
||||
const findEmptyState = () => wrapper.find(EmptyState);
|
||||
const findBlankState = () => wrapper.find(BlankState);
|
||||
const findStagesDropdown = () => wrapper.find('.js-builds-dropdown-button');
|
||||
|
||||
const findTablePagination = () => wrapper.find(TablePagination);
|
||||
|
||||
const createComponent = (props = defaultProps, methods) => {
|
||||
wrapper = mount(PipelinesComponent, {
|
||||
|
|
@ -87,19 +109,19 @@ describe('Pipelines', () => {
|
|||
});
|
||||
|
||||
it('renders tabs', () => {
|
||||
expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
|
||||
expect(findTab('all').text()).toContain('All');
|
||||
});
|
||||
|
||||
it('renders Run Pipeline link', () => {
|
||||
expect(wrapper.find('.js-run-pipeline').attributes('href')).toBe(paths.newPipelinePath);
|
||||
expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
|
||||
});
|
||||
|
||||
it('renders CI Lint link', () => {
|
||||
expect(wrapper.find('.js-ci-lint').attributes('href')).toBe(paths.ciLintPath);
|
||||
expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
|
||||
});
|
||||
|
||||
it('renders Clear Runner Cache button', () => {
|
||||
expect(wrapper.find('.js-clear-cache').text()).toBe('Clear Runner Caches');
|
||||
expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
|
||||
});
|
||||
|
||||
it('renders pipelines table', () => {
|
||||
|
|
@ -127,23 +149,31 @@ describe('Pipelines', () => {
|
|||
});
|
||||
|
||||
it('renders tabs', () => {
|
||||
expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
|
||||
expect(findTab('all').text()).toContain('All');
|
||||
});
|
||||
|
||||
it('renders Run Pipeline link', () => {
|
||||
expect(wrapper.find('.js-run-pipeline').attributes('href')).toEqual(paths.newPipelinePath);
|
||||
expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
|
||||
});
|
||||
|
||||
it('renders CI Lint link', () => {
|
||||
expect(wrapper.find('.js-ci-lint').attributes('href')).toEqual(paths.ciLintPath);
|
||||
expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
|
||||
});
|
||||
|
||||
it('renders Clear Runner Cache button', () => {
|
||||
expect(wrapper.find('.js-clear-cache').text()).toEqual('Clear Runner Caches');
|
||||
expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
|
||||
});
|
||||
|
||||
it('renders tab empty state', () => {
|
||||
expect(wrapper.find('.empty-state h4').text()).toEqual('There are currently no pipelines.');
|
||||
expect(findBlankState().text()).toBe('There are currently no pipelines.');
|
||||
});
|
||||
|
||||
it('renders tab empty state finished scope', () => {
|
||||
wrapper.vm.scope = 'finished';
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findBlankState().text()).toBe('There are currently no finished pipelines.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -165,18 +195,23 @@ describe('Pipelines', () => {
|
|||
});
|
||||
|
||||
it('renders empty state', () => {
|
||||
expect(wrapper.find('.js-empty-state h4').text()).toEqual('Build with confidence');
|
||||
|
||||
expect(wrapper.find('.js-get-started-pipelines').attributes('href')).toEqual(
|
||||
paths.helpPagePath,
|
||||
);
|
||||
expect(
|
||||
findEmptyState()
|
||||
.find('h4')
|
||||
.text(),
|
||||
).toBe('Build with confidence');
|
||||
expect(
|
||||
findEmptyState()
|
||||
.find(GlButton)
|
||||
.attributes('href'),
|
||||
).toBe(paths.helpPagePath);
|
||||
});
|
||||
|
||||
it('does not render tabs nor buttons', () => {
|
||||
expect(wrapper.find('.js-pipelines-tab-all').exists()).toBeFalsy();
|
||||
expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy();
|
||||
expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy();
|
||||
expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy();
|
||||
expect(findTab('all').exists()).toBe(false);
|
||||
expect(findRunPipelineButton().exists()).toBeFalsy();
|
||||
expect(findCiLintButton().exists()).toBeFalsy();
|
||||
expect(findCleanCacheButton().exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -189,20 +224,18 @@ describe('Pipelines', () => {
|
|||
});
|
||||
|
||||
it('renders tabs', () => {
|
||||
expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
|
||||
expect(findTab('all').text()).toContain('All');
|
||||
});
|
||||
|
||||
it('renders buttons', () => {
|
||||
expect(wrapper.find('.js-run-pipeline').attributes('href')).toEqual(paths.newPipelinePath);
|
||||
expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
|
||||
|
||||
expect(wrapper.find('.js-ci-lint').attributes('href')).toEqual(paths.ciLintPath);
|
||||
expect(wrapper.find('.js-clear-cache').text()).toEqual('Clear Runner Caches');
|
||||
expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
|
||||
expect(findCleanCacheButton().text()).toBe('Clear Runner Caches');
|
||||
});
|
||||
|
||||
it('renders error state', () => {
|
||||
expect(wrapper.find('.empty-state').text()).toContain(
|
||||
'There was an error fetching the pipelines.',
|
||||
);
|
||||
expect(findBlankState().text()).toContain('There was an error fetching the pipelines.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -218,13 +251,13 @@ describe('Pipelines', () => {
|
|||
});
|
||||
|
||||
it('renders tabs', () => {
|
||||
expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
|
||||
expect(findTab('all').text()).toContain('All');
|
||||
});
|
||||
|
||||
it('does not render buttons', () => {
|
||||
expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy();
|
||||
expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy();
|
||||
expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy();
|
||||
expect(findRunPipelineButton().exists()).toBeFalsy();
|
||||
expect(findCiLintButton().exists()).toBeFalsy();
|
||||
expect(findCleanCacheButton().exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders pipelines table', () => {
|
||||
|
|
@ -252,17 +285,17 @@ describe('Pipelines', () => {
|
|||
});
|
||||
|
||||
it('renders tabs', () => {
|
||||
expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
|
||||
expect(findTab('all').text()).toContain('All');
|
||||
});
|
||||
|
||||
it('does not render buttons', () => {
|
||||
expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy();
|
||||
expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy();
|
||||
expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy();
|
||||
expect(findRunPipelineButton().exists()).toBeFalsy();
|
||||
expect(findCiLintButton().exists()).toBeFalsy();
|
||||
expect(findCleanCacheButton().exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders tab empty state', () => {
|
||||
expect(wrapper.find('.empty-state h4').text()).toEqual('There are currently no pipelines.');
|
||||
expect(wrapper.find('.empty-state h4').text()).toBe('There are currently no pipelines.');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -284,18 +317,22 @@ describe('Pipelines', () => {
|
|||
});
|
||||
|
||||
it('renders empty state without button to set CI', () => {
|
||||
expect(wrapper.find('.js-empty-state').text()).toEqual(
|
||||
expect(findEmptyState().text()).toBe(
|
||||
'This project is not currently set up to run pipelines.',
|
||||
);
|
||||
|
||||
expect(wrapper.find('.js-get-started-pipelines').exists()).toBeFalsy();
|
||||
expect(
|
||||
findEmptyState()
|
||||
.find(GlButton)
|
||||
.exists(),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('does not render tabs or buttons', () => {
|
||||
expect(wrapper.find('.js-pipelines-tab-all').exists()).toBeFalsy();
|
||||
expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy();
|
||||
expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy();
|
||||
expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy();
|
||||
expect(findTab('all').exists()).toBe(false);
|
||||
expect(findRunPipelineButton().exists()).toBeFalsy();
|
||||
expect(findCiLintButton().exists()).toBeFalsy();
|
||||
expect(findCleanCacheButton().exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -309,13 +346,13 @@ describe('Pipelines', () => {
|
|||
});
|
||||
|
||||
it('renders tabs', () => {
|
||||
expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
|
||||
expect(findTab('all').text()).toContain('All');
|
||||
});
|
||||
|
||||
it('does not renders buttons', () => {
|
||||
expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy();
|
||||
expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy();
|
||||
expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy();
|
||||
expect(findRunPipelineButton().exists()).toBeFalsy();
|
||||
expect(findCiLintButton().exists()).toBeFalsy();
|
||||
expect(findCleanCacheButton().exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders error state', () => {
|
||||
|
|
@ -342,14 +379,20 @@ describe('Pipelines', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should set up navigation tabs', () => {
|
||||
expect(findNavigationTabs().props('tabs')).toEqual([
|
||||
{ name: 'All', scope: 'all', count: '3', isActive: true },
|
||||
{ name: 'Finished', scope: 'finished', count: undefined, isActive: false },
|
||||
{ name: 'Branches', scope: 'branches', isActive: false },
|
||||
{ name: 'Tags', scope: 'tags', isActive: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should render navigation tabs', () => {
|
||||
expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
|
||||
|
||||
expect(wrapper.find('.js-pipelines-tab-finished').text()).toContain('Finished');
|
||||
|
||||
expect(wrapper.find('.js-pipelines-tab-branches').text()).toContain('Branches');
|
||||
|
||||
expect(wrapper.find('.js-pipelines-tab-tags').text()).toContain('Tags');
|
||||
expect(findTab('all').html()).toContain('All');
|
||||
expect(findTab('finished').text()).toContain('Finished');
|
||||
expect(findTab('branches').text()).toContain('Branches');
|
||||
expect(findTab('tags').text()).toContain('Tags');
|
||||
});
|
||||
|
||||
it('should make an API request when using tabs', () => {
|
||||
|
|
@ -362,7 +405,7 @@ describe('Pipelines', () => {
|
|||
);
|
||||
|
||||
return waitForPromises().then(() => {
|
||||
wrapper.find('.js-pipelines-tab-finished').trigger('click');
|
||||
findTab('finished').trigger('click');
|
||||
|
||||
expect(updateContentMock).toHaveBeenCalledWith({ scope: 'finished', page: '1' });
|
||||
});
|
||||
|
|
@ -401,133 +444,172 @@ describe('Pipelines', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('User Interaction', () => {
|
||||
let updateContentMock;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(window.history, 'pushState').mockImplementation(() => null);
|
||||
});
|
||||
|
||||
describe('onChangeTab', () => {
|
||||
it('should set page to 1', () => {
|
||||
const updateContentMock = jest.fn(() => {});
|
||||
createComponent(
|
||||
{ hasGitlabCi: true, canCreatePipeline: true, ...paths },
|
||||
{
|
||||
updateContent: updateContentMock,
|
||||
},
|
||||
);
|
||||
beforeEach(() => {
|
||||
mock.onGet(paths.endpoint).reply(200, pipelines);
|
||||
createComponent();
|
||||
|
||||
wrapper.vm.onChangeTab('running');
|
||||
updateContentMock = jest.spyOn(wrapper.vm, 'updateContent');
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
describe('when user changes tabs', () => {
|
||||
it('should set page to 1', () => {
|
||||
findNavigationTabs().vm.$emit('onChangeTab', 'running');
|
||||
|
||||
expect(updateContentMock).toHaveBeenCalledWith({ scope: 'running', page: '1' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('onChangePage', () => {
|
||||
describe('when user changes page', () => {
|
||||
it('should update page and keep scope', () => {
|
||||
const updateContentMock = jest.fn(() => {});
|
||||
createComponent(
|
||||
{ hasGitlabCi: true, canCreatePipeline: true, ...paths },
|
||||
{
|
||||
updateContent: updateContentMock,
|
||||
},
|
||||
);
|
||||
|
||||
wrapper.vm.onChangePage(4);
|
||||
findTablePagination().vm.change(4);
|
||||
|
||||
expect(updateContentMock).toHaveBeenCalledWith({ scope: wrapper.vm.scope, page: '4' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('updates results when a staged is clicked', () => {
|
||||
beforeEach(() => {
|
||||
const copyPipeline = { ...pipelineWithStages };
|
||||
copyPipeline.id += 1;
|
||||
mock
|
||||
.onGet('twitter/flight/pipelines.json')
|
||||
.reply(
|
||||
200,
|
||||
{
|
||||
pipelines: [pipelineWithStages],
|
||||
count: {
|
||||
all: 1,
|
||||
finished: 1,
|
||||
pending: 0,
|
||||
running: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
'POLL-INTERVAL': 100,
|
||||
},
|
||||
)
|
||||
.onGet(pipelineWithStages.details.stages[0].dropdown_path)
|
||||
.reply(200, stageReply);
|
||||
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe('when a request is being made', () => {
|
||||
it('stops polling, cancels the request, & restarts polling', () => {
|
||||
const stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
|
||||
const restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
|
||||
const cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel');
|
||||
mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
|
||||
|
||||
return waitForPromises()
|
||||
.then(() => {
|
||||
wrapper.vm.isMakingRequest = true;
|
||||
findStagesDropdown().trigger('click');
|
||||
})
|
||||
.then(() => {
|
||||
expect(cancelMock).toHaveBeenCalled();
|
||||
expect(stopMock).toHaveBeenCalled();
|
||||
expect(restartMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no request is being made', () => {
|
||||
it('stops polling & restarts polling', () => {
|
||||
const stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
|
||||
const restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
|
||||
mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
|
||||
|
||||
return waitForPromises()
|
||||
.then(() => {
|
||||
findStagesDropdown().trigger('click');
|
||||
expect(stopMock).toHaveBeenCalled();
|
||||
})
|
||||
.then(() => {
|
||||
expect(restartMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('computed properties', () => {
|
||||
describe('Rendered content', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe('tabs', () => {
|
||||
it('returns default tabs', () => {
|
||||
expect(wrapper.vm.tabs).toEqual([
|
||||
{ name: 'All', scope: 'all', count: undefined, isActive: true },
|
||||
{ name: 'Finished', scope: 'finished', count: undefined, isActive: false },
|
||||
{ name: 'Branches', scope: 'branches', isActive: false },
|
||||
{ name: 'Tags', scope: 'tags', isActive: false },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('emptyTabMessage', () => {
|
||||
it('returns message with finished scope', () => {
|
||||
wrapper.vm.scope = 'finished';
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no finished pipelines.');
|
||||
});
|
||||
describe('displays different content', () => {
|
||||
it('shows loading state when the app is loading', () => {
|
||||
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns message without scope when scope is `all`', () => {
|
||||
expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no pipelines.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('stateToRender', () => {
|
||||
it('returns loading state when the app is loading', () => {
|
||||
expect(wrapper.vm.stateToRender).toEqual('loading');
|
||||
});
|
||||
|
||||
it('returns error state when app has error', () => {
|
||||
it('shows error state when app has error', () => {
|
||||
wrapper.vm.hasError = true;
|
||||
wrapper.vm.isLoading = false;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.stateToRender).toEqual('error');
|
||||
expect(findBlankState().props('message')).toBe(
|
||||
'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns table list when app has pipelines', () => {
|
||||
it('shows table list when app has pipelines', () => {
|
||||
wrapper.vm.isLoading = false;
|
||||
wrapper.vm.hasError = false;
|
||||
wrapper.vm.state.pipelines = pipelines.pipelines;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.stateToRender).toEqual('tableList');
|
||||
expect(wrapper.find(PipelinesTableComponent).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty tab when app does not have pipelines but project has pipelines', () => {
|
||||
it('shows empty tab when app does not have pipelines but project has pipelines', () => {
|
||||
wrapper.vm.state.count.all = 10;
|
||||
wrapper.vm.isLoading = false;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.stateToRender).toEqual('emptyTab');
|
||||
expect(findBlankState().exists()).toBe(true);
|
||||
expect(findBlankState().props('message')).toBe('There are currently no pipelines.');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty tab when project has CI', () => {
|
||||
it('shows empty tab when project has CI', () => {
|
||||
wrapper.vm.isLoading = false;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.stateToRender).toEqual('emptyTab');
|
||||
expect(findBlankState().exists()).toBe(true);
|
||||
expect(findBlankState().props('message')).toBe('There are currently no pipelines.');
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty state when project does not have pipelines nor CI', () => {
|
||||
it('shows empty state when project does not have pipelines nor CI', () => {
|
||||
createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
|
||||
|
||||
wrapper.vm.isLoading = false;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.stateToRender).toEqual('emptyState');
|
||||
expect(wrapper.find(EmptyState).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldRenderTabs', () => {
|
||||
describe('displays tabs', () => {
|
||||
it('returns true when state is loading & has already made the first request', () => {
|
||||
wrapper.vm.isLoading = true;
|
||||
wrapper.vm.hasMadeRequest = true;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.shouldRenderTabs).toEqual(true);
|
||||
expect(findNavigationTabs().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -537,7 +619,7 @@ describe('Pipelines', () => {
|
|||
wrapper.vm.hasMadeRequest = true;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.shouldRenderTabs).toEqual(true);
|
||||
expect(findNavigationTabs().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -547,7 +629,7 @@ describe('Pipelines', () => {
|
|||
wrapper.vm.hasMadeRequest = true;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.shouldRenderTabs).toEqual(true);
|
||||
expect(findNavigationTabs().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -557,7 +639,7 @@ describe('Pipelines', () => {
|
|||
wrapper.vm.hasMadeRequest = true;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.shouldRenderTabs).toEqual(true);
|
||||
expect(findNavigationTabs().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -565,7 +647,7 @@ describe('Pipelines', () => {
|
|||
wrapper.vm.hasMadeRequest = false;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.shouldRenderTabs).toEqual(false);
|
||||
expect(findNavigationTabs().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -576,17 +658,17 @@ describe('Pipelines', () => {
|
|||
wrapper.vm.hasMadeRequest = true;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.shouldRenderTabs).toEqual(false);
|
||||
expect(findNavigationTabs().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldRenderButtons', () => {
|
||||
describe('displays buttons', () => {
|
||||
it('returns true when it has paths & has made the first request', () => {
|
||||
wrapper.vm.hasMadeRequest = true;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.shouldRenderButtons).toEqual(true);
|
||||
expect(findNavigationControls().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -594,77 +676,12 @@ describe('Pipelines', () => {
|
|||
wrapper.vm.hasMadeRequest = false;
|
||||
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(wrapper.vm.shouldRenderButtons).toEqual(false);
|
||||
expect(findNavigationControls().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updates results when a staged is clicked', () => {
|
||||
beforeEach(() => {
|
||||
const copyPipeline = { ...pipelineWithStages };
|
||||
copyPipeline.id += 1;
|
||||
mock
|
||||
.onGet('twitter/flight/pipelines.json')
|
||||
.reply(
|
||||
200,
|
||||
{
|
||||
pipelines: [pipelineWithStages],
|
||||
count: {
|
||||
all: 1,
|
||||
finished: 1,
|
||||
pending: 0,
|
||||
running: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
'POLL-INTERVAL': 100,
|
||||
},
|
||||
)
|
||||
.onGet(pipelineWithStages.details.stages[0].dropdown_path)
|
||||
.reply(200, stageReply);
|
||||
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe('when a request is being made', () => {
|
||||
it('stops polling, cancels the request, & restarts polling', () => {
|
||||
const stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
|
||||
const restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
|
||||
const cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel');
|
||||
mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
|
||||
|
||||
return waitForPromises()
|
||||
.then(() => {
|
||||
wrapper.vm.isMakingRequest = true;
|
||||
wrapper.find('.js-builds-dropdown-button').trigger('click');
|
||||
})
|
||||
.then(() => {
|
||||
expect(cancelMock).toHaveBeenCalled();
|
||||
expect(stopMock).toHaveBeenCalled();
|
||||
expect(restartMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no request is being made', () => {
|
||||
it('stops polling & restarts polling', () => {
|
||||
const stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
|
||||
const restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
|
||||
mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
|
||||
|
||||
return waitForPromises()
|
||||
.then(() => {
|
||||
wrapper.find('.js-builds-dropdown-button').trigger('click');
|
||||
expect(stopMock).toHaveBeenCalled();
|
||||
})
|
||||
.then(() => {
|
||||
expect(restartMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pipeline filters', () => {
|
||||
let updateContentMock;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
|
||||
import eventHub from '~/sidebar/event_hub';
|
||||
import { deprecatedCreateFlash as flash } from '~/flash';
|
||||
|
|
@ -22,7 +21,6 @@ describe('EditFormButtons', () => {
|
|||
};
|
||||
|
||||
const findLockToggle = () => wrapper.find('[data-testid="lock-toggle"]');
|
||||
const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
|
||||
const createComponent = ({ props = {}, data = {}, resolved = true }) => {
|
||||
store = issuableType === ISSUABLE_TYPE_ISSUE ? createStore() : createMrStore();
|
||||
|
|
@ -33,7 +31,7 @@ describe('EditFormButtons', () => {
|
|||
jest.spyOn(store, 'dispatch').mockRejectedValue();
|
||||
}
|
||||
|
||||
wrapper = shallowMount(EditFormButtons, {
|
||||
wrapper = mount(EditFormButtons, {
|
||||
store,
|
||||
provide: {
|
||||
fullPath: '',
|
||||
|
|
@ -78,8 +76,8 @@ describe('EditFormButtons', () => {
|
|||
expect(findLockToggle().attributes('disabled')).toBe('disabled');
|
||||
});
|
||||
|
||||
it('displays the GlLoadingIcon', () => {
|
||||
expect(findGlLoadingIcon().exists()).toBe(true);
|
||||
it('sets loading on the toggle button', () => {
|
||||
expect(findLockToggle().props('loading')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -121,7 +119,7 @@ describe('EditFormButtons', () => {
|
|||
|
||||
it('resets loading', async () => {
|
||||
await wrapper.vm.$nextTick().then(() => {
|
||||
expect(findGlLoadingIcon().exists()).toBe(false);
|
||||
expect(findLockToggle().props('loading')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -156,7 +154,7 @@ describe('EditFormButtons', () => {
|
|||
|
||||
it('resets loading', async () => {
|
||||
await wrapper.vm.$nextTick().then(() => {
|
||||
expect(findGlLoadingIcon().exists()).toBe(false);
|
||||
expect(findLockToggle().props('loading')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -834,6 +834,19 @@ RSpec.describe QuickActions::InterpretService do
|
|||
let(:issuable) { issue }
|
||||
end
|
||||
|
||||
context 'assigning to a group' do
|
||||
let_it_be(:group) { create(:group, :public) }
|
||||
|
||||
before_all do
|
||||
group.add_developer(create(:user))
|
||||
end
|
||||
|
||||
it_behaves_like 'empty command', "Failed to assign a user because no user was found." do
|
||||
let(:content) { "/assign #{group.to_reference}" }
|
||||
let(:issuable) { issue }
|
||||
end
|
||||
end
|
||||
|
||||
context 'unassign command' do
|
||||
let(:content) { '/unassign' }
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue