Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-30 21:09:29 +00:00
parent f06ebebade
commit 8924515456
71 changed files with 769 additions and 192 deletions

View File

@ -293,14 +293,14 @@ coverage-frontend:
- *yarn-install - *yarn-install
- run_timed_command "retry yarn run webpack-prod" - run_timed_command "retry yarn run webpack-prod"
qa-frontend-node:12:
extends: .qa-frontend-node
image: ${GITLAB_DEPENDENCY_PROXY}node:12
qa-frontend-node:14: qa-frontend-node:14:
extends: .qa-frontend-node extends: .qa-frontend-node
image: ${GITLAB_DEPENDENCY_PROXY}node:14 image: ${GITLAB_DEPENDENCY_PROXY}node:14
qa-frontend-node:16:
extends: .qa-frontend-node
image: ${GITLAB_DEPENDENCY_PROXY}node:16
qa-frontend-node:latest: qa-frontend-node:latest:
extends: extends:
- .qa-frontend-node - .qa-frontend-node

View File

@ -1252,7 +1252,7 @@ GEM
actionpack (>= 4.0) actionpack (>= 4.0)
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sqlite3 (1.3.13) sqlite3 (1.4.2)
ssh_data (1.2.0) ssh_data (1.2.0)
ssrf_filter (1.0.7) ssrf_filter (1.0.7)
stackprof (0.2.15) stackprof (0.2.15)

View File

@ -12,6 +12,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-security-newsletter-callout', '.js-security-newsletter-callout',
'.js-approaching-seats-count-threshold', '.js-approaching-seats-count-threshold',
'.js-storage-enforcement-banner', '.js-storage-enforcement-banner',
'.js-user-over-limit-free-plan-alert',
]; ];
const initCallouts = () => { const initCallouts = () => {

View File

@ -4,9 +4,11 @@ import { createAlert } from '~/flash';
import { updateHistory } from '~/lib/utils/url_utility'; import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber } from '~/locale'; import { formatNumber } from '~/locale';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue'; import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerBulkDelete from '../components/runner_bulk_delete.vue';
import RunnerList from '../components/runner_list.vue'; import RunnerList from '../components/runner_list.vue';
import RunnerName from '../components/runner_name.vue'; import RunnerName from '../components/runner_name.vue';
import RunnerStats from '../components/stat/runner_stats.vue'; import RunnerStats from '../components/stat/runner_stats.vue';
@ -53,6 +55,7 @@ export default {
GlLink, GlLink,
RegistrationDropdown, RegistrationDropdown,
RunnerFilteredSearchBar, RunnerFilteredSearchBar,
RunnerBulkDelete,
RunnerList, RunnerList,
RunnerName, RunnerName,
RunnerStats, RunnerStats,
@ -60,6 +63,8 @@ export default {
RunnerTypeTabs, RunnerTypeTabs,
RunnerActionsCell, RunnerActionsCell,
}, },
mixins: [glFeatureFlagMixin()],
inject: ['localMutations'],
props: { props: {
registrationToken: { registrationToken: {
type: String, type: String,
@ -180,6 +185,11 @@ export default {
}, },
]; ];
}, },
isBulkDeleteEnabled() {
// Feature flag: admin_runners_bulk_delete
// Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/353981
return this.glFeatures.adminRunnersBulkDelete;
},
}, },
watch: { watch: {
search: { search: {
@ -238,6 +248,12 @@ export default {
reportToSentry(error) { reportToSentry(error) {
captureException({ error, component: this.$options.name }); captureException({ error, component: this.$options.name });
}, },
onChecked({ runner, isChecked }) {
this.localMutations.setRunnerChecked({
runner,
isChecked,
});
},
}, },
filteredSearchNamespace: ADMIN_FILTERED_SEARCH_NAMESPACE, filteredSearchNamespace: ADMIN_FILTERED_SEARCH_NAMESPACE,
INSTANCE_TYPE, INSTANCE_TYPE,
@ -286,7 +302,13 @@ export default {
{{ __('No runners found') }} {{ __('No runners found') }}
</div> </div>
<template v-else> <template v-else>
<runner-list :runners="runners.items" :loading="runnersLoading"> <runner-bulk-delete v-if="isBulkDeleteEnabled" />
<runner-list
:runners="runners.items"
:loading="runnersLoading"
:checkable="isBulkDeleteEnabled"
@checked="onChecked"
>
<template #runner-name="{ runner }"> <template #runner-name="{ runner }">
<gl-link :href="runner.adminUrl"> <gl-link :href="runner.adminUrl">
<runner-name :runner="runner" /> <runner-name :runner="runner" />

View File

@ -1,9 +1,10 @@
import { GlToast } from '@gitlab/ui'; import { GlToast } from '@gitlab/ui';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import { updateOutdatedUrl } from '~/runner/runner_search_utils'; import { updateOutdatedUrl } from '~/runner/runner_search_utils';
import createDefaultClient from '~/lib/graphql';
import { createLocalState } from '../graphql/list/local_state';
import AdminRunnersApp from './admin_runners_app.vue'; import AdminRunnersApp from './admin_runners_app.vue';
Vue.use(GlToast); Vue.use(GlToast);
@ -27,8 +28,10 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
const { runnerInstallHelpPage, registrationToken } = el.dataset; const { runnerInstallHelpPage, registrationToken } = el.dataset;
const { cacheConfig, typeDefs, localMutations } = createLocalState();
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(), defaultClient: createDefaultClient({}, { cacheConfig, typeDefs }),
}); });
return new Vue({ return new Vue({
@ -36,6 +39,7 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
apolloProvider, apolloProvider,
provide: { provide: {
runnerInstallHelpPage, runnerInstallHelpPage,
localMutations,
}, },
render(h) { render(h) {
return h(AdminRunnersApp, { return h(AdminRunnersApp, {

View File

@ -0,0 +1,111 @@
<script>
import { GlButton, GlModalDirective, GlSprintf } from '@gitlab/ui';
import { n__, sprintf } from '~/locale';
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql';
export default {
components: {
GlButton,
GlSprintf,
},
directives: {
GlModal: GlModalDirective,
},
inject: ['localMutations'],
data() {
return {
checkedRunnerIds: [],
};
},
apollo: {
checkedRunnerIds: {
query: checkedRunnerIdsQuery,
},
},
computed: {
checkedCount() {
return this.checkedRunnerIds.length || 0;
},
bannerMessage() {
return sprintf(
n__(
'Runners|%{strongStart}%{count}%{strongEnd} runner selected',
'Runners|%{strongStart}%{count}%{strongEnd} runners selected',
this.checkedCount,
),
{
count: this.checkedCount,
},
);
},
modalTitle() {
return n__('Runners|Delete %d runner', 'Runners|Delete %d runners', this.checkedCount);
},
modalHtmlMessage() {
return sprintf(
n__(
'Runners|%{strongStart}%{count}%{strongEnd} runner will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?',
'Runners|%{strongStart}%{count}%{strongEnd} runners will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?',
this.checkedCount,
),
{
strongStart: '<strong>',
strongEnd: '</strong>',
count: this.checkedCount,
},
false,
);
},
primaryBtnText() {
return n__(
'Runners|Permanently delete %d runner',
'Runners|Permanently delete %d runners',
this.checkedCount,
);
},
},
methods: {
onClearChecked() {
this.localMutations.clearChecked();
},
onClickDelete: ignoreWhilePending(async function onClickDelete() {
const confirmed = await confirmAction(null, {
title: this.modalTitle,
modalHtmlMessage: this.modalHtmlMessage,
primaryBtnVariant: 'danger',
primaryBtnText: this.primaryBtnText,
});
if (confirmed) {
// TODO Call $apollo.mutate with list of runner
// ids in `this.checkedRunnerIds`.
// See https://gitlab.com/gitlab-org/gitlab/-/issues/339525/
}
}),
},
};
</script>
<template>
<div v-if="checkedCount" class="gl-my-4 gl-p-4 gl-border-1 gl-border-solid gl-border-gray-100">
<div class="gl-display-flex gl-align-items-center">
<div>
<gl-sprintf :message="bannerMessage">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
</div>
<div class="gl-ml-auto">
<gl-button data-testid="clear-btn" variant="default" @click="onClearChecked">{{
s__('Runners|Clear selection')
}}</gl-button>
<gl-button data-testid="delete-btn" variant="danger" @click="onClickDelete">{{
s__('Runners|Delete selected')
}}</gl-button>
</div>
</div>
</div>
</template>

View File

@ -4,11 +4,22 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/toolt
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql';
import { formatJobCount, tableField } from '../utils'; import { formatJobCount, tableField } from '../utils';
import RunnerSummaryCell from './cells/runner_summary_cell.vue'; import RunnerSummaryCell from './cells/runner_summary_cell.vue';
import RunnerStatusCell from './cells/runner_status_cell.vue'; import RunnerStatusCell from './cells/runner_status_cell.vue';
import RunnerTags from './runner_tags.vue'; import RunnerTags from './runner_tags.vue';
const defaultFields = [
tableField({ key: 'status', label: s__('Runners|Status') }),
tableField({ key: 'summary', label: s__('Runners|Runner'), thClasses: ['gl-lg-w-25p'] }),
tableField({ key: 'version', label: __('Version') }),
tableField({ key: 'jobCount', label: __('Jobs') }),
tableField({ key: 'tagList', label: __('Tags'), thClasses: ['gl-lg-w-25p'] }),
tableField({ key: 'contactedAt', label: __('Last contact') }),
tableField({ key: 'actions', label: '' }),
];
export default { export default {
components: { components: {
GlTableLite, GlTableLite,
@ -22,7 +33,20 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
apollo: {
checkedRunnerIds: {
query: checkedRunnerIdsQuery,
skip() {
return !this.checkable;
},
},
},
props: { props: {
checkable: {
type: Boolean,
required: false,
default: false,
},
loading: { loading: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -33,6 +57,10 @@ export default {
required: true, required: true,
}, },
}, },
emits: ['checked'],
data() {
return { checkedRunnerIds: [] };
},
computed: { computed: {
tableClass() { tableClass() {
// <gl-table-lite> does not provide a busy state, add // <gl-table-lite> does not provide a busy state, add
@ -42,6 +70,18 @@ export default {
'gl-opacity-6': this.loading, 'gl-opacity-6': this.loading,
}; };
}, },
fields() {
if (this.checkable) {
const checkboxField = tableField({
key: 'checkbox',
label: s__('Runners|Checkbox'),
thClasses: ['gl-w-9'],
tdClass: ['gl-text-center'],
});
return [checkboxField, ...defaultFields];
}
return defaultFields;
},
}, },
methods: { methods: {
formatJobCount(jobCount) { formatJobCount(jobCount) {
@ -55,16 +95,16 @@ export default {
} }
return {}; return {};
}, },
onCheckboxChange(runner, isChecked) {
this.$emit('checked', {
runner,
isChecked,
});
},
isChecked(runner) {
return this.checkedRunnerIds.includes(runner.id);
},
}, },
fields: [
tableField({ key: 'status', label: s__('Runners|Status') }),
tableField({ key: 'summary', label: s__('Runners|Runner'), thClasses: ['gl-lg-w-25p'] }),
tableField({ key: 'version', label: __('Version') }),
tableField({ key: 'jobCount', label: __('Jobs') }),
tableField({ key: 'tagList', label: __('Tags'), thClasses: ['gl-lg-w-25p'] }),
tableField({ key: 'contactedAt', label: __('Last contact') }),
tableField({ key: 'actions', label: '' }),
],
}; };
</script> </script>
<template> <template>
@ -73,13 +113,29 @@ export default {
:aria-busy="loading" :aria-busy="loading"
:class="tableClass" :class="tableClass"
:items="runners" :items="runners"
:fields="$options.fields" :fields="fields"
:tbody-tr-attr="runnerTrAttr" :tbody-tr-attr="runnerTrAttr"
data-testid="runner-list" data-testid="runner-list"
stacked="md" stacked="md"
primary-key="id" primary-key="id"
fixed fixed
> >
<template #head(checkbox)>
<!--
Checkbox to select all to be added here
See https://gitlab.com/gitlab-org/gitlab/-/issues/339525/
-->
<span></span>
</template>
<template #cell(checkbox)="{ item }">
<input
type="checkbox"
:checked="isChecked(item)"
@change="onCheckboxChange(item, $event.target.checked)"
/>
</template>
<template #cell(status)="{ item }"> <template #cell(status)="{ item }">
<runner-status-cell :runner="item" /> <runner-status-cell :runner="item" />
</template> </template>

View File

@ -0,0 +1,3 @@
query getCheckedRunnerIds {
checkedRunnerIds @client
}

View File

@ -0,0 +1,63 @@
import { makeVar } from '@apollo/client/core';
import typeDefs from './typedefs.graphql';
/**
* Local state for checkable runner items.
*
* Usage:
*
* ```
* import { createLocalState } from '~/runner/graphql/list/local_state';
*
* // initialize local state
* const { cacheConfig, typeDefs, localMutations } = createLocalState();
*
* // configure the client
* apolloClient = createApolloClient({}, { cacheConfig, typeDefs });
*
* // modify local state
* localMutations.setRunnerChecked( ... )
* ```
*
* Note: Currently only in use behind a feature flag:
* admin_runners_bulk_delete for the admin list, rollout issue:
* https://gitlab.com/gitlab-org/gitlab/-/issues/353981
*
* @returns {Object} An object to configure an Apollo client:
* contains cacheConfig, typeDefs, localMutations.
*/
export const createLocalState = () => {
const checkedRunnerIdsVar = makeVar({});
const cacheConfig = {
typePolicies: {
Query: {
fields: {
checkedRunnerIds() {
return Object.entries(checkedRunnerIdsVar())
.filter(([, isChecked]) => isChecked)
.map(([key]) => key);
},
},
},
},
};
const localMutations = {
setRunnerChecked({ runner, isChecked }) {
checkedRunnerIdsVar({
...checkedRunnerIdsVar(),
[runner.id]: isChecked,
});
},
clearChecked() {
checkedRunnerIdsVar({});
},
};
return {
cacheConfig,
typeDefs,
localMutations,
};
};

View File

@ -0,0 +1,3 @@
extend type Query {
checkedRunnerIds: [ID!]!
}

View File

@ -24,7 +24,7 @@ export const formatJobCount = (jobCount) => {
* @param {Object} options * @param {Object} options
* @returns Field object to add to GlTable fields * @returns Field object to add to GlTable fields
*/ */
export const tableField = ({ key, label = '', thClasses = [] }) => { export const tableField = ({ key, label = '', thClasses = [], ...options }) => {
return { return {
key, key,
label, label,
@ -32,6 +32,7 @@ export const tableField = ({ key, label = '', thClasses = [] }) => {
tdAttr: { tdAttr: {
'data-testid': `td-${key}`, 'data-testid': `td-${key}`,
}, },
...options,
}; };
}; };

View File

@ -4,7 +4,6 @@ import { __, s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue'; import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
import AutoDevOpsAlert from './auto_dev_ops_alert.vue'; import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue'; import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants'; import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
@ -52,7 +51,7 @@ export default {
TrainingProviderList, TrainingProviderList,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
inject: ['projectFullPath'], inject: ['projectFullPath', 'vulnerabilityTrainingDocsPath'],
props: { props: {
augmentedSecurityFeatures: { augmentedSecurityFeatures: {
type: Array, type: Array,
@ -127,9 +126,6 @@ export default {
}, },
}, },
autoDevopsEnabledAlertStorageKey: AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY, autoDevopsEnabledAlertStorageKey: AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
securityTraininDocLink: helpPagePath('user/application_security/vulnerabilities/index', {
anchor: 'enable-security-training-for-vulnerabilities',
}),
}; };
</script> </script>
@ -268,7 +264,7 @@ export default {
{{ $options.i18n.securityTrainingDescription }} {{ $options.i18n.securityTrainingDescription }}
</p> </p>
<p> <p>
<gl-link :href="$options.securityTraininDocLink">{{ <gl-link :href="vulnerabilityTrainingDocsPath">{{
$options.i18n.securityTrainingDoc $options.i18n.securityTrainingDoc
}}</gl-link> }}</gl-link>
</p> </p>

View File

@ -25,6 +25,7 @@ export const initSecurityConfiguration = (el) => {
gitlabCiHistoryPath, gitlabCiHistoryPath,
autoDevopsHelpPagePath, autoDevopsHelpPagePath,
autoDevopsPath, autoDevopsPath,
vulnerabilityTrainingDocsPath,
} = el.dataset; } = el.dataset;
const { augmentedSecurityFeatures, augmentedComplianceFeatures } = augmentFeatures( const { augmentedSecurityFeatures, augmentedComplianceFeatures } = augmentFeatures(
@ -41,6 +42,7 @@ export const initSecurityConfiguration = (el) => {
upgradePath, upgradePath,
autoDevopsHelpPagePath, autoDevopsHelpPagePath,
autoDevopsPath, autoDevopsPath,
vulnerabilityTrainingDocsPath,
}, },
render(createElement) { render(createElement) {
return createElement(SecurityConfigurationApp, { return createElement(SecurityConfigurationApp, {

View File

@ -25,7 +25,7 @@ export default {
</script> </script>
<template> <template>
<span> <span>
<gl-button ref="popoverTrigger" variant="link" icon="question" :aria-label="__('Help')" /> <gl-button ref="popoverTrigger" variant="link" icon="question-o" :aria-label="__('Help')" />
<gl-popover :target="() => $refs.popoverTrigger.$el" v-bind="options"> <gl-popover :target="() => $refs.popoverTrigger.$el" v-bind="options">
<template v-if="options.title" #title> <template v-if="options.title" #title>
<span v-safe-html="options.title"></span> <span v-safe-html="options.title"></span>

View File

@ -4,6 +4,9 @@ class Admin::RunnersController < Admin::ApplicationController
include RunnerSetupScripts include RunnerSetupScripts
before_action :runner, except: [:index, :tag_list, :runner_setup_scripts] before_action :runner, except: [:index, :tag_list, :runner_setup_scripts]
before_action only: [:index] do
push_frontend_feature_flag(:admin_runners_bulk_delete, default_enabled: :yaml)
end
feature_category :runner feature_category :runner

View File

@ -6,6 +6,10 @@ module Projects
def security_upgrade_path def security_upgrade_path
"https://#{ApplicationHelper.promo_host}/pricing/" "https://#{ApplicationHelper.promo_host}/pricing/"
end end
def vulnerability_training_docs_path
help_page_path('user/application_security/vulnerabilities/index', anchor: 'enable-security-training-for-vulnerabilities')
end
end end
end end
end end

View File

@ -31,3 +31,5 @@ module Users
end end
end end
end end
Users::GroupCalloutsHelper.prepend_mod

View File

@ -14,7 +14,8 @@ module Users
storage_enforcement_banner_first_enforcement_threshold: 3, storage_enforcement_banner_first_enforcement_threshold: 3,
storage_enforcement_banner_second_enforcement_threshold: 4, storage_enforcement_banner_second_enforcement_threshold: 4,
storage_enforcement_banner_third_enforcement_threshold: 5, storage_enforcement_banner_third_enforcement_threshold: 5,
storage_enforcement_banner_fourth_enforcement_threshold: 6 storage_enforcement_banner_fourth_enforcement_threshold: 6,
preview_user_over_limit_free_plan_alert: 7 # EE-only
} }
validates :group, presence: true validates :group, presence: true

View File

@ -4,7 +4,9 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
presents ::Issue, as: :issue presents ::Issue, as: :issue
def issue_path def issue_path
url_builder.build(issue, only_path: true) return url_builder.build(issue, only_path: true) unless use_work_items_path?
project_work_items_path(issue.project, work_items_path: issue.id)
end end
delegator_override :subscribed? delegator_override :subscribed?
@ -15,6 +17,18 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
def project_emails_disabled? def project_emails_disabled?
issue.project.emails_disabled? issue.project.emails_disabled?
end end
def web_url
return super unless use_work_items_path?
project_work_items_url(issue.project, work_items_path: issue.id)
end
private
def use_work_items_path?
issue.issue_type == 'task' && issue.project.work_items_feature_flag_enabled?
end
end end
IssuePresenter.prepend_mod_with('IssuePresenter') IssuePresenter.prepend_mod_with('IssuePresenter')

View File

@ -1,6 +1,8 @@
- add_page_specific_style 'page_bundles/members' - add_page_specific_style 'page_bundles/members'
- page_title _('Group members') - page_title _('Group members')
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @group
.row.gl-mt-3 .row.gl-mt-3
.col-lg-12 .col-lg-12
.gl-display-flex.gl-flex-wrap .gl-display-flex.gl-flex-wrap

View File

@ -7,6 +7,7 @@
= render_if_exists 'shared/thanks_for_purchase_banner', plan_title: plan_title, quantity: params[:purchased_quantity].to_i = render_if_exists 'shared/thanks_for_purchase_banner', plan_title: plan_title, quantity: params[:purchased_quantity].to_i
= render_if_exists 'shared/qrtly_reconciliation_alert', group: @group = render_if_exists 'shared/qrtly_reconciliation_alert', group: @group
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @group
- if show_invite_banner?(@group) - if show_invite_banner?(@group)
= content_for :group_invite_members_banner do = content_for :group_invite_members_banner do

View File

@ -20,6 +20,7 @@
= dispensable_render_if_exists "shared/namespace_user_cap_reached_alert" = dispensable_render_if_exists "shared/namespace_user_cap_reached_alert"
= dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert" = dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert"
= yield :page_level_alert = yield :page_level_alert
= yield :user_over_limit_free_plan_alert
= yield :group_invite_members_banner = yield :group_invite_members_banner
- unless @hide_breadcrumbs - unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs" = render "layouts/nav/breadcrumbs"

View File

@ -3,6 +3,7 @@
- escaped_default_branch_name = default_branch_name.shellescape - escaped_default_branch_name = default_branch_name.shellescape
- @skip_current_level_breadcrumb = true - @skip_current_level_breadcrumb = true
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
= render partial: 'flash_messages', locals: { project: @project } = render partial: 'flash_messages', locals: { project: @project }
= render "home_panel" = render "home_panel"

View File

@ -1,6 +1,8 @@
- page_title _('No repository') - page_title _('No repository')
- @skip_current_level_breadcrumb = true - @skip_current_level_breadcrumb = true
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
%h2.gl-display-flex %h2.gl-display-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center .gl-display-flex.gl-align-items-center.gl-justify-content-center
= sprite_icon('warning-solid', size: 24, css_class: 'gl-mr-2') = sprite_icon('warning-solid', size: 24, css_class: 'gl-mr-2')

View File

@ -1,6 +1,8 @@
- add_page_specific_style 'page_bundles/members' - add_page_specific_style 'page_bundles/members'
- page_title _("Members") - page_title _("Members")
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
.row.gl-mt-3 .row.gl-mt-3
.col-lg-12 .col-lg-12
- if can_invite_members_for_project?(@project) - if can_invite_members_for_project?(@project)

View File

@ -3,5 +3,6 @@
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
#js-security-configuration{ data: { **@configuration.to_html_data_attribute, #js-security-configuration{ data: { **@configuration.to_html_data_attribute,
vulnerability_training_docs_path: vulnerability_training_docs_path,
upgrade_path: security_upgrade_path, upgrade_path: security_upgrade_path,
project_full_path: @project.full_path } } project_full_path: @project.full_path } }

View File

@ -6,6 +6,7 @@
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity") = auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
= render partial: 'flash_messages', locals: { project: @project } = render partial: 'flash_messages', locals: { project: @project }
= render "projects/last_push" = render "projects/last_push"

View File

@ -0,0 +1,8 @@
---
name: admin_runners_bulk_delete
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81894
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353981
milestone: '14.9'
type: development
group: group::runner
default_enabled: false

View File

@ -0,0 +1,16 @@
- name: "Request a new review" # the name of the feature being removed. Avoid the words `deprecation`, `deprecate`, `removal`, and `remove` in this field because these are implied.
announcement_milestone: "15.0" # The milestone when this feature was deprecated.
announcement_date: "2022-05-22" # The date of the milestone release when this feature was deprecated. This should almost always be the 22nd of a month (YYYY-MM-DD), unless you did an out of band blog post.
removal_milestone: "15.0" # The milestone when this feature is being removed.
removal_date: "2022-05-22" # This should almost always be the 22nd of a month (YYYY-MM-DD), the date of the milestone release when this feature will be removed.
breaking_change: false # Change to true if this removal is a breaking change.
reporter: phikai # GitLab username of the person reporting the removal
body: | # Do not modify this line, instead modify the lines below.
The ability to [request a new review](https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#request-a-new-review) has been removed in GitLab 15.0. This feature is replaced with [requesting attention](https://docs.gitlab.com/ee/user/project/merge_requests/#request-attention-to-a-merge-request) to a merge request.
# The following items are not published on the docs page, but may be used in the future.
stage: Create # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth
tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
issue_url: # (optional) This is a link to the deprecation issue in GitLab
documentation_url: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#request-a-new-review # (optional) This is a link to the current documentation page
image_url: # (optional) This is a link to a thumbnail image depicting the feature
video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg

View File

@ -14,3 +14,4 @@ swap:
needs? to: "Rewrite the sentence, or use 'must', instead of" needs? to: "Rewrite the sentence, or use 'must', instead of"
note that: "Be concise: rewrite the sentence to not use" note that: "Be concise: rewrite the sentence to not use"
please: "Remove this word from the sentence: " please: "Remove this word from the sentence: "
respectively: "Rewrite the sentence to be more precise, instead of using "

View File

@ -51,7 +51,7 @@ If the highest number stable branch is unclear, check the [GitLab blog](https://
| [Ruby](#2-ruby) | `2.7` | From GitLab 13.6, Ruby 2.7 is required. Ruby 3.0 is not supported yet (see [the relevant epic](https://gitlab.com/groups/gitlab-org/-/epics/5149) for the current status). You must use the standard MRI implementation of Ruby. We love [JRuby](https://www.jruby.org/) and [Rubinius](https://github.com/rubinius/rubinius#the-rubinius-language-platform), but GitLab needs several Gems that have native extensions. | | [Ruby](#2-ruby) | `2.7` | From GitLab 13.6, Ruby 2.7 is required. Ruby 3.0 is not supported yet (see [the relevant epic](https://gitlab.com/groups/gitlab-org/-/epics/5149) for the current status). You must use the standard MRI implementation of Ruby. We love [JRuby](https://www.jruby.org/) and [Rubinius](https://github.com/rubinius/rubinius#the-rubinius-language-platform), but GitLab needs several Gems that have native extensions. |
| [Go](#3-go) | `1.16` | | | [Go](#3-go) | `1.16` | |
| [Git](#git) | `2.33.x` | From GitLab 14.4, Git 2.33.x and later is required. It's highly recommended that you use the [Git version provided by Gitaly](#git). | | [Git](#git) | `2.33.x` | From GitLab 14.4, Git 2.33.x and later is required. It's highly recommended that you use the [Git version provided by Gitaly](#git). |
| [Node.js](#4-node) | `12.22.1` | GitLab uses [webpack](https://webpack.js.org/) to compile frontend assets. Node.js 14.x is recommended, as it's faster. You can check which version you're running with `node -v`. You need to update it to a newer version if needed. | | [Node.js](#4-node) | `14.15.0` | GitLab uses [webpack](https://webpack.js.org/) to compile frontend assets. Node.js 16.x is recommended, as it's faster. You can check which version you're running with `node -v`. You need to update it to a newer version if needed. |
## GitLab directory structure ## GitLab directory structure
@ -263,7 +263,7 @@ GitLab requires the use of Node to compile JavaScript
assets, and Yarn to manage JavaScript dependencies. The current minimum assets, and Yarn to manage JavaScript dependencies. The current minimum
requirements for these are: requirements for these are:
- `node` >= v12.22.1. (We recommend node 14.x as it is faster) - `node` >= v14.15.0. (We recommend node 16.x as it is faster)
- `yarn` = v1.22.x (Yarn 2 is not supported yet) - `yarn` = v1.22.x (Yarn 2 is not supported yet)
In many distributions, In many distributions,
@ -271,8 +271,8 @@ the versions provided by the official package repositories are out of date, so
we need to install through the following commands: we need to install through the following commands:
```shell ```shell
# install node v14.x # install node v16.x
curl --location "https://deb.nodesource.com/setup_14.x" | sudo bash - curl --location "https://deb.nodesource.com/setup_16.x" | sudo bash -
sudo apt-get install -y nodejs sudo apt-get install -y nodejs
npm install --global yarn npm install --global yarn

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541) This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0. for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab provides some dashboards out-of-the-box for any project with GitLab provides some dashboards out-of-the-box for any project with
[Prometheus available](../../../user/project/integrations/prometheus.md). You can [Prometheus available](../../../user/project/integrations/prometheus.md). You can

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541) This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0. for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab provides a template to make it easier for you to create templates for GitLab provides a template to make it easier for you to create templates for
[custom dashboards](index.md). Templates provide helpful guidance and [custom dashboards](index.md). Templates provide helpful guidance and

View File

@ -11,7 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541) This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0. for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
By default, all projects include a [GitLab-defined Prometheus dashboard](default.md), which By default, all projects include a [GitLab-defined Prometheus dashboard](default.md), which
includes a few key metrics, but you can also define your own custom dashboards. includes a few key metrics, but you can also define your own custom dashboards.

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541) This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0. for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
The below panel types are supported in monitoring dashboards. The below panel types are supported in monitoring dashboards.

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541) This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0. for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
You can configure your [Monitoring dashboard](../index.md) to You can configure your [Monitoring dashboard](../index.md) to
display the time zone of your choice, and the links of your choice. display the time zone of your choice, and the links of your choice.

View File

@ -11,7 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541) This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0. for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
Templating variables can be used to make your metrics dashboard more versatile. Templating variables can be used to make your metrics dashboard more versatile.

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541) This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0. for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
## Query variables ## Query variables

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541) This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0. for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
Dashboards have several components: Dashboards have several components:

View File

@ -28,6 +28,12 @@ For removal reviewers (Technical Writers only):
https://about.gitlab.com/handbook/marketing/blog/release-posts/#update-the-removals-doc https://about.gitlab.com/handbook/marketing/blog/release-posts/#update-the-removals-doc
--> -->
## 15.0
### Request a new review
The ability to [request a new review](https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#request-a-new-review) has been removed in GitLab 15.0. This feature is replaced with [requesting attention](https://docs.gitlab.com/ee/user/project/merge_requests/#request-attention-to-a-merge-request) to a merge request.
## 14.9 ## 14.9
### Integrated error tracking disabled by default ### Integrated error tracking disabled by default

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -422,7 +422,7 @@ The following table lists group permissions available for each role:
| View 2FA status of members | | | | | ✓ | | View 2FA status of members | | | | | ✓ |
| View [Billing](../subscriptions/gitlab_com/index.md#view-your-gitlab-saas-subscription) | | | | | ✓ (4) | | View [Billing](../subscriptions/gitlab_com/index.md#view-your-gitlab-saas-subscription) | | | | | ✓ (4) |
| View [Usage Quotas](usage_quotas.md) Page | | | | ✓ | ✓ (4) | | View [Usage Quotas](usage_quotas.md) Page | | | | ✓ | ✓ (4) |
| Manage runners | | | | | ✓ | | Manage group runners | | | | | ✓ |
<!-- markdownlint-disable MD029 --> <!-- markdownlint-disable MD029 -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541) This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0. for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab supports automatically detecting and monitoring AWS resources, starting GitLab supports automatically detecting and monitoring AWS resources, starting
with the [Elastic Load Balancer](https://aws.amazon.com/elasticloadbalancing/) (ELB). with the [Elastic Load Balancer](https://aws.amazon.com/elasticloadbalancing/) (ELB).

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541) This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0. for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab has support for automatically detecting and monitoring HAProxy. This is provided by leveraging the [HAProxy Exporter](https://github.com/prometheus/haproxy_exporter), which translates HAProxy statistics into a Prometheus readable form. GitLab has support for automatically detecting and monitoring HAProxy. This is provided by leveraging the [HAProxy Exporter](https://github.com/prometheus/haproxy_exporter), which translates HAProxy statistics into a Prometheus readable form.

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541) This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0. for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab offers automatic detection of select [Prometheus exporters](https://prometheus.io/docs/instrumenting/exporters/). GitLab offers automatic detection of select [Prometheus exporters](https://prometheus.io/docs/instrumenting/exporters/).

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541) This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0. for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab has support for automatically detecting and monitoring Kubernetes metrics. GitLab has support for automatically detecting and monitoring Kubernetes metrics.

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541) This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0. for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab has support for automatically detecting and monitoring NGINX. This is provided by leveraging the [NGINX VTS exporter](https://github.com/hnlq715/nginx-vts-exporter), which translates [VTS statistics](https://github.com/vozlt/nginx-module-vts) into a Prometheus readable form. GitLab has support for automatically detecting and monitoring NGINX. This is provided by leveraging the [NGINX VTS exporter](https://github.com/hnlq715/nginx-vts-exporter), which translates [VTS statistics](https://github.com/vozlt/nginx-module-vts) into a Prometheus readable form.

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541) This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0. for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
GitLab has support for automatically detecting and monitoring the Kubernetes NGINX Ingress controller. This is provided by leveraging the built-in Prometheus metrics included with Kubernetes NGINX Ingress controller [version 0.16.0](https://github.com/kubernetes/ingress-nginx/blob/master/Changelog.md#0160) onward. GitLab has support for automatically detecting and monitoring the Kubernetes NGINX Ingress controller. This is provided by leveraging the built-in Prometheus metrics included with Kubernetes NGINX Ingress controller [version 0.16.0](https://github.com/kubernetes/ingress-nginx/blob/master/Changelog.md#0160) onward.

View File

@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
WARNING: WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541) This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346541)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0. for use in GitLab 14.7, and is planned for removal in GitLab 16.0.
NOTE: NOTE:
[NGINX Ingress version 0.16](nginx_ingress.md) and above have built-in Prometheus metrics, which are different than the VTS based metrics. [NGINX Ingress version 0.16](nginx_ingress.md) and above have built-in Prometheus metrics, which are different than the VTS based metrics.

View File

@ -112,7 +112,13 @@ This example shows reviewers and approval rules in a merge request sidebar:
### Request a new review ### Request a new review
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/293933) in GitLab 13.9. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/293933) in GitLab 13.9.
> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/357271) in GitLab 14.10.
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/357271)
for use in GitLab 14.10, and is planned for [removal](https://gitlab.com/gitlab-org/gitlab/-/issues/357271) in GitLab 15.0.
Use [attention requests](../index.md#request-attention-to-a-merge-request) instead.
After a reviewer completes their [merge request reviews](../../../discussions/index.md), After a reviewer completes their [merge request reviews](../../../discussions/index.md),
the author of the merge request can request a new review from the reviewer: the author of the merge request can request a new review from the reviewer:

View File

@ -15153,6 +15153,9 @@ msgstr ""
msgid "Explore groups" msgid "Explore groups"
msgstr "" msgstr ""
msgid "Explore paid plans"
msgstr ""
msgid "Explore projects" msgid "Explore projects"
msgstr "" msgstr ""
@ -16258,6 +16261,9 @@ msgstr ""
msgid "From %{providerTitle}" msgid "From %{providerTitle}"
msgstr "" msgstr ""
msgid "From June 22, 2022 (GitLab 15.1), free personal namespaces and top-level groups will be limited to %{free_limit} members"
msgstr ""
msgid "From issue creation until deploy to production" msgid "From issue creation until deploy to production"
msgstr "" msgstr ""
@ -22896,6 +22902,9 @@ msgstr ""
msgid "Manage labels" msgid "Manage labels"
msgstr "" msgstr ""
msgid "Manage members"
msgstr ""
msgid "Manage milestones" msgid "Manage milestones"
msgstr "" msgstr ""
@ -32184,6 +32193,16 @@ msgstr ""
msgid "Runners|%{percentage} spot." msgid "Runners|%{percentage} spot."
msgstr "" msgstr ""
msgid "Runners|%{strongStart}%{count}%{strongEnd} runner selected"
msgid_plural "Runners|%{strongStart}%{count}%{strongEnd} runners selected"
msgstr[0] ""
msgstr[1] ""
msgid "Runners|%{strongStart}%{count}%{strongEnd} runner will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?"
msgid_plural "Runners|%{strongStart}%{count}%{strongEnd} runners will be permanently deleted and no longer available for projects or groups in the instance. Are you sure you want to continue?"
msgstr[0] ""
msgstr[1] ""
msgid "Runners|A capacity of 1 enables warm HA through Auto Scaling group re-spawn. A capacity of 2 enables hot HA because the service is available even when a node is lost. A capacity of 3 or more enables hot HA and manual scaling of runner fleet." msgid "Runners|A capacity of 1 enables warm HA through Auto Scaling group re-spawn. A capacity of 2 enables hot HA because the service is available even when a node is lost. A capacity of 3 or more enables hot HA and manual scaling of runner fleet."
msgstr "" msgstr ""
@ -32229,9 +32248,15 @@ msgstr ""
msgid "Runners|Change to project runner" msgid "Runners|Change to project runner"
msgstr "" msgstr ""
msgid "Runners|Checkbox"
msgstr ""
msgid "Runners|Choose your preferred GitLab Runner" msgid "Runners|Choose your preferred GitLab Runner"
msgstr "" msgstr ""
msgid "Runners|Clear selection"
msgstr ""
msgid "Runners|Command to register runner" msgid "Runners|Command to register runner"
msgstr "" msgstr ""
@ -32244,12 +32269,20 @@ msgstr ""
msgid "Runners|Copy registration token" msgid "Runners|Copy registration token"
msgstr "" msgstr ""
msgid "Runners|Delete %d runner"
msgid_plural "Runners|Delete %d runners"
msgstr[0] ""
msgstr[1] ""
msgid "Runners|Delete runner" msgid "Runners|Delete runner"
msgstr "" msgstr ""
msgid "Runners|Delete runner %{name}?" msgid "Runners|Delete runner %{name}?"
msgstr "" msgstr ""
msgid "Runners|Delete selected"
msgstr ""
msgid "Runners|Deploy GitLab Runner in AWS" msgid "Runners|Deploy GitLab Runner in AWS"
msgstr "" msgstr ""
@ -32340,6 +32373,11 @@ msgstr ""
msgid "Runners|Paused" msgid "Runners|Paused"
msgstr "" msgstr ""
msgid "Runners|Permanently delete %d runner"
msgid_plural "Runners|Permanently delete %d runners"
msgstr[0] ""
msgstr[1] ""
msgid "Runners|Platform" msgid "Runners|Platform"
msgstr "" msgstr ""
@ -43243,6 +43281,9 @@ msgstr ""
msgid "YouTube URL or ID" msgid "YouTube URL or ID"
msgstr "" msgstr ""
msgid "Your %{doc_link_start}namespace%{doc_link_end}, %{strong_start}%{namespace_name}%{strong_end} has more than %{free_limit} members. From June 22, 2022, it will be limited to %{free_limit}, and the remaining members will get a %{link_start}status of Over limit%{link_end} and lose access to the namespace. You can go to the Usage Quotas page to manage which %{free_limit} members will remain in your namespace. To get more members, an owner can start a trial or upgrade to a paid tier."
msgstr ""
msgid "Your %{group} membership will now expire in %{days}." msgid "Your %{group} membership will now expire in %{days}."
msgstr "" msgstr ""

View File

@ -75,13 +75,16 @@ module QA
# Download files from GCS bucket by environment name # Download files from GCS bucket by environment name
# Delete the files afterward # Delete the files afterward
def download(ci_project_name) def download(ci_project_name)
files_list = gcs_storage.list_objects(BUCKET, prefix: ci_project_name).items.each_with_object([]) do |obj, arr| bucket_items = gcs_storage.list_objects(BUCKET, prefix: ci_project_name).items
files_list = bucket_items&.each_with_object([]) do |obj, arr|
arr << obj.name arr << obj.name
end end
return puts "\nNothing to download!" if files_list.empty? return puts "\nNothing to download!" if files_list.blank?
FileUtils.mkdir_p('tmp/') FileUtils.mkdir_p('tmp/')
files_list.each do |file_name| files_list.each do |file_name|
local_path = "tmp/#{file_name.split('/').last}" local_path = "tmp/#{file_name.split('/').last}"
Runtime::Logger.info("Downloading #{file_name} to #{local_path}") Runtime::Logger.info("Downloading #{file_name} to #{local_path}")

View File

@ -58,6 +58,11 @@ FactoryBot.define do
end end
end end
trait :task do
issue_type { :task }
association :work_item_type, :default, :task
end
factory :incident do factory :incident do
issue_type { :incident } issue_type { :incident }
association :work_item_type, :default, :incident association :work_item_type, :default, :incident

View File

@ -37,5 +37,10 @@ FactoryBot.define do
base_type { WorkItems::Type.base_types[:requirement] } base_type { WorkItems::Type.base_types[:requirement] }
icon_name { 'issue-type-requirements' } icon_name { 'issue-type-requirements' }
end end
trait :task do
base_type { WorkItems::Type.base_types[:task] }
icon_name { 'issue-type-task' }
end
end end
end end

View File

@ -135,7 +135,7 @@ describe('Grouped code quality reports app', () => {
}); });
it('does not render a help icon', () => { it('does not render a help icon', () => {
expect(findWidget().find('[data-testid="question-icon"]').exists()).toBe(false); expect(findWidget().find('[data-testid="question-o-icon"]').exists()).toBe(false);
}); });
describe('when base report was not found', () => { describe('when base report was not found', () => {
@ -144,7 +144,7 @@ describe('Grouped code quality reports app', () => {
}); });
it('renders a help icon with more information', () => { it('renders a help icon with more information', () => {
expect(findWidget().find('[data-testid="question-icon"]').exists()).toBe(true); expect(findWidget().find('[data-testid="question-o-icon"]').exists()).toBe(true);
}); });
}); });
}); });

View File

@ -13,6 +13,7 @@ import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility'; import { updateHistory } from '~/lib/utils/url_utility';
import { createLocalState } from '~/runner/graphql/list/local_state';
import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue'; import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue';
import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue'; import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue';
import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue'; import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue';
@ -43,6 +44,7 @@ import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered
import { runnersData, runnersCountData, runnersDataPaginated } from '../mock_data'; import { runnersData, runnersCountData, runnersDataPaginated } from '../mock_data';
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN'; const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
const mockRunners = runnersData.data.runners.nodes;
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/runner/sentry_utils'); jest.mock('~/runner/sentry_utils');
@ -58,6 +60,8 @@ describe('AdminRunnersApp', () => {
let wrapper; let wrapper;
let mockRunnersQuery; let mockRunnersQuery;
let mockRunnersCountQuery; let mockRunnersCountQuery;
let cacheConfig;
let localMutations;
const findRunnerStats = () => wrapper.findComponent(RunnerStats); const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell); const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
@ -69,18 +73,30 @@ describe('AdminRunnersApp', () => {
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar); const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch); const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => { const createComponent = ({
props = {},
mountFn = shallowMountExtended,
provide,
...options
} = {}) => {
({ cacheConfig, localMutations } = createLocalState());
const handlers = [ const handlers = [
[adminRunnersQuery, mockRunnersQuery], [adminRunnersQuery, mockRunnersQuery],
[adminRunnersCountQuery, mockRunnersCountQuery], [adminRunnersCountQuery, mockRunnersCountQuery],
]; ];
wrapper = mountFn(AdminRunnersApp, { wrapper = mountFn(AdminRunnersApp, {
apolloProvider: createMockApollo(handlers), apolloProvider: createMockApollo(handlers, {}, cacheConfig),
propsData: { propsData: {
registrationToken: mockRegistrationToken, registrationToken: mockRegistrationToken,
...props, ...props,
}, },
provide: {
localMutations,
...provide,
},
...options,
}); });
}; };
@ -173,7 +189,7 @@ describe('AdminRunnersApp', () => {
}); });
it('shows the runners list', () => { it('shows the runners list', () => {
expect(findRunnerList().props('runners')).toEqual(runnersData.data.runners.nodes); expect(findRunnerList().props('runners')).toEqual(mockRunners);
}); });
it('runner item links to the runner admin page', async () => { it('runner item links to the runner admin page', async () => {
@ -181,7 +197,7 @@ describe('AdminRunnersApp', () => {
await waitForPromises(); await waitForPromises();
const { id, shortSha } = runnersData.data.runners.nodes[0]; const { id, shortSha } = mockRunners[0];
const numericId = getIdFromGraphQLId(id); const numericId = getIdFromGraphQLId(id);
const runnerLink = wrapper.find('tr [data-testid="td-summary"]').find(GlLink); const runnerLink = wrapper.find('tr [data-testid="td-summary"]').find(GlLink);
@ -197,7 +213,7 @@ describe('AdminRunnersApp', () => {
const runnerActions = wrapper.find('tr [data-testid="td-actions"]').find(RunnerActionsCell); const runnerActions = wrapper.find('tr [data-testid="td-actions"]').find(RunnerActionsCell);
const runner = runnersData.data.runners.nodes[0]; const runner = mockRunners[0];
expect(runnerActions.props()).toEqual({ expect(runnerActions.props()).toEqual({
runner, runner,
@ -232,8 +248,7 @@ describe('AdminRunnersApp', () => {
describe('Single runner row', () => { describe('Single runner row', () => {
let showToast; let showToast;
const mockRunner = runnersData.data.runners.nodes[0]; const { id: graphqlId, shortSha } = mockRunners[0];
const { id: graphqlId, shortSha } = mockRunner;
const id = getIdFromGraphQLId(graphqlId); const id = getIdFromGraphQLId(graphqlId);
const COUNT_QUERIES = 7; // Smart queries that display a filtered count of runners const COUNT_QUERIES = 7; // Smart queries that display a filtered count of runners
const FILTERED_COUNT_QUERIES = 4; // Smart queries that display a count of runners in tabs const FILTERED_COUNT_QUERIES = 4; // Smart queries that display a count of runners in tabs
@ -333,6 +348,41 @@ describe('AdminRunnersApp', () => {
expect(findRunnerList().props('loading')).toBe(true); expect(findRunnerList().props('loading')).toBe(true);
}); });
describe('when bulk delete is enabled', () => {
beforeEach(() => {
createComponent({
provide: {
glFeatures: { adminRunnersBulkDelete: true },
},
});
});
it('runner list is checkable', () => {
expect(findRunnerList().props('checkable')).toBe(true);
});
it('responds to checked items by updating the local cache', () => {
const setRunnerCheckedMock = jest
.spyOn(localMutations, 'setRunnerChecked')
.mockImplementation(() => {});
const runner = mockRunners[0];
expect(setRunnerCheckedMock).toHaveBeenCalledTimes(0);
findRunnerList().vm.$emit('checked', {
runner,
isChecked: true,
});
expect(setRunnerCheckedMock).toHaveBeenCalledTimes(1);
expect(setRunnerCheckedMock).toHaveBeenCalledWith({
runner,
isChecked: true,
});
});
});
describe('when no runners are found', () => { describe('when no runners are found', () => {
beforeEach(async () => { beforeEach(async () => {
mockRunnersQuery = jest.fn().mockResolvedValue({ mockRunnersQuery = jest.fn().mockResolvedValue({

View File

@ -0,0 +1,103 @@
import Vue from 'vue';
import { GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createLocalState } from '~/runner/graphql/list/local_state';
import waitForPromises from 'helpers/wait_for_promises';
Vue.use(VueApollo);
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
describe('RunnerBulkDelete', () => {
let wrapper;
let mockState;
let mockCheckedRunnerIds;
const findClearBtn = () => wrapper.findByTestId('clear-btn');
const findDeleteBtn = () => wrapper.findByTestId('delete-btn');
const createComponent = () => {
const { cacheConfig, localMutations } = mockState;
wrapper = shallowMountExtended(RunnerBulkDelete, {
apolloProvider: createMockApollo(undefined, undefined, cacheConfig),
provide: {
localMutations,
},
stubs: {
GlSprintf,
},
});
};
beforeEach(() => {
mockState = createLocalState();
jest
.spyOn(mockState.cacheConfig.typePolicies.Query.fields, 'checkedRunnerIds')
.mockImplementation(() => mockCheckedRunnerIds);
});
afterEach(() => {
wrapper.destroy();
});
describe('When no runners are checked', () => {
beforeEach(async () => {
mockCheckedRunnerIds = [];
createComponent();
await waitForPromises();
});
it('shows no contents', () => {
expect(wrapper.html()).toBe('');
});
});
describe.each`
count | ids | text
${1} | ${['gid:Runner/1']} | ${'1 runner'}
${2} | ${['gid:Runner/1', 'gid:Runner/2']} | ${'2 runners'}
`('When $count runner(s) are checked', ({ count, ids, text }) => {
beforeEach(() => {
mockCheckedRunnerIds = ids;
createComponent();
jest.spyOn(mockState.localMutations, 'clearChecked').mockImplementation(() => {});
});
it(`shows "${text}"`, () => {
expect(wrapper.text()).toContain(text);
});
it('clears selection', () => {
expect(mockState.localMutations.clearChecked).toHaveBeenCalledTimes(0);
findClearBtn().vm.$emit('click');
expect(mockState.localMutations.clearChecked).toHaveBeenCalledTimes(1);
});
it('shows confirmation modal', () => {
expect(confirmAction).toHaveBeenCalledTimes(0);
findDeleteBtn().vm.$emit('click');
expect(confirmAction).toHaveBeenCalledTimes(1);
const [, confirmOptions] = confirmAction.mock.calls[0];
const { title, modalHtmlMessage, primaryBtnText } = confirmOptions;
expect(title).toMatch(text);
expect(primaryBtnText).toMatch(text);
expect(modalHtmlMessage).toMatch(`<strong>${count}</strong>`);
});
});
});

View File

@ -55,7 +55,7 @@ describe('RunnerList', () => {
}); });
it('Sets runner id as a row key', () => { it('Sets runner id as a row key', () => {
createComponent({}); createComponent();
expect(findTable().attributes('primary-key')).toBe('id'); expect(findTable().attributes('primary-key')).toBe('id');
}); });
@ -90,6 +90,35 @@ describe('RunnerList', () => {
expect(findCell({ fieldKey: 'actions' }).exists()).toBe(true); expect(findCell({ fieldKey: 'actions' }).exists()).toBe(true);
}); });
describe('When the list is checkable', () => {
beforeEach(() => {
createComponent(
{
props: {
checkable: true,
},
},
mountExtended,
);
});
it('Displays a checkbox field', () => {
expect(findCell({ fieldKey: 'checkbox' }).find('input').exists()).toBe(true);
});
it('Emits a checked event', () => {
const checkbox = findCell({ fieldKey: 'checkbox' }).find('input');
checkbox.setChecked();
expect(wrapper.emitted('checked')).toHaveLength(1);
expect(wrapper.emitted('checked')[0][0]).toEqual({
isChecked: true,
runner: mockRunners[0],
});
});
});
describe('Scoped cell slots', () => { describe('Scoped cell slots', () => {
it('Render #runner-name slot in "summary" cell', () => { it('Render #runner-name slot in "summary" cell', () => {
createComponent( createComponent(

View File

@ -0,0 +1,72 @@
import createApolloClient from '~/lib/graphql';
import { createLocalState } from '~/runner/graphql/list/local_state';
import getCheckedRunnerIdsQuery from '~/runner/graphql/list/checked_runner_ids.query.graphql';
describe('~/runner/graphql/list/local_state', () => {
let localState;
let apolloClient;
const createSubject = () => {
if (apolloClient) {
throw new Error('test subject already exists!');
}
localState = createLocalState();
const { cacheConfig, typeDefs } = localState;
apolloClient = createApolloClient({}, { cacheConfig, typeDefs });
};
const queryCheckedRunnerIds = () => {
const { checkedRunnerIds } = apolloClient.readQuery({
query: getCheckedRunnerIdsQuery,
});
return checkedRunnerIds;
};
beforeEach(() => {
createSubject();
});
afterEach(() => {
localState = null;
apolloClient = null;
});
describe('default', () => {
it('has empty checked list', () => {
expect(queryCheckedRunnerIds()).toEqual([]);
});
});
describe.each`
inputs | expected
${[['a', true], ['b', true], ['b', true]]} | ${['a', 'b']}
${[['a', true], ['b', true], ['a', false]]} | ${['b']}
${[['c', true], ['b', true], ['a', true], ['d', false]]} | ${['c', 'b', 'a']}
`('setRunnerChecked', ({ inputs, expected }) => {
beforeEach(() => {
inputs.forEach(([id, isChecked]) => {
localState.localMutations.setRunnerChecked({ runner: { id }, isChecked });
});
});
it(`for inputs="${inputs}" has a ids="[${expected}]"`, () => {
expect(queryCheckedRunnerIds()).toEqual(expected);
});
});
describe('clearChecked', () => {
it('clears all checked items', () => {
['a', 'b', 'c'].forEach((id) => {
localState.localMutations.setRunnerChecked({ runner: { id }, isChecked: true });
});
expect(queryCheckedRunnerIds()).toEqual(['a', 'b', 'c']);
localState.localMutations.clearChecked();
expect(queryCheckedRunnerIds()).toEqual([]);
});
});
});

View File

@ -44,6 +44,10 @@ describe('~/runner/utils', () => {
thClass: expect.arrayContaining(mockClasses), thClass: expect.arrayContaining(mockClasses),
}); });
}); });
it('a field with custom options', () => {
expect(tableField({ foo: 'bar' })).toMatchObject({ foo: 'bar' });
});
}); });
describe('getPaginationVariables', () => { describe('getPaginationVariables', () => {

View File

@ -33,6 +33,7 @@ const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
const autoDevopsPath = '/autoDevopsPath'; const autoDevopsPath = '/autoDevopsPath';
const gitlabCiHistoryPath = 'test/historyPath'; const gitlabCiHistoryPath = 'test/historyPath';
const projectFullPath = 'namespace/project'; const projectFullPath = 'namespace/project';
const vulnerabilityTrainingDocsPath = 'user/application_security/vulnerabilities/index';
useLocalStorageSpy(); useLocalStorageSpy();
@ -55,6 +56,7 @@ describe('App component', () => {
autoDevopsHelpPagePath, autoDevopsHelpPagePath,
autoDevopsPath, autoDevopsPath,
projectFullPath, projectFullPath,
vulnerabilityTrainingDocsPath,
glFeatures: { glFeatures: {
secureVulnerabilityTraining, secureVulnerabilityTraining,
}, },
@ -462,9 +464,7 @@ describe('App component', () => {
const trainingLink = findVulnerabilityManagementTab().findComponent(GlLink); const trainingLink = findVulnerabilityManagementTab().findComponent(GlLink);
expect(trainingLink.text()).toBe('Learn more about vulnerability training'); expect(trainingLink.text()).toBe('Learn more about vulnerability training');
expect(trainingLink.attributes('href')).toBe( expect(trainingLink.attributes('href')).toBe(vulnerabilityTrainingDocsPath);
'/help/user/application_security/vulnerabilities/index#enable-security-training-for-vulnerabilities',
);
}); });
}); });

View File

@ -34,7 +34,7 @@ describe('HelpPopover', () => {
it('renders a link button with an icon question', () => { it('renders a link button with an icon question', () => {
expect(findQuestionButton().props()).toMatchObject({ expect(findQuestionButton().props()).toMatchObject({
icon: 'question', icon: 'question-o',
variant: 'link', variant: 'link',
}); });
}); });

View File

@ -10,4 +10,10 @@ RSpec.describe Projects::Security::ConfigurationHelper do
it { is_expected.to eq("https://#{ApplicationHelper.promo_host}/pricing/") } it { is_expected.to eq("https://#{ApplicationHelper.promo_host}/pricing/") }
end end
describe 'vulnerability_training_docs_path' do
subject { helper.vulnerability_training_docs_path }
it { is_expected.to eq(help_page_path('user/application_security/vulnerabilities/index', anchor: 'enable-security-training-for-vulnerabilities')) }
end
end end

View File

@ -5,19 +5,42 @@ require 'spec_helper'
RSpec.describe IssuePresenter do RSpec.describe IssuePresenter do
include Gitlab::Routing.url_helpers include Gitlab::Routing.url_helpers
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:group) { create(:group) } let_it_be(:group) { create(:group) }
let(:project) { create(:project, group: group) } let_it_be(:project) { create(:project, group: group) }
let(:issue) { create(:issue, project: project) } let_it_be(:issue) { create(:issue, project: project) }
let(:presenter) { described_class.new(issue, current_user: user) } let_it_be(:task) { create(:issue, :task, project: project) }
before do let(:presented_issue) { issue }
let(:presenter) { described_class.new(presented_issue, current_user: user) }
before_all do
group.add_developer(user) group.add_developer(user)
end end
describe '#web_url' do describe '#web_url' do
it 'returns correct path' do it 'returns correct path' do
expect(presenter.web_url).to eq("http://localhost/#{group.name}/#{project.name}/-/issues/#{issue.iid}") expect(presenter.web_url).to eq("http://localhost/#{group.name}/#{project.name}/-/issues/#{presented_issue.iid}")
end
context 'when issue type is task' do
let(:presented_issue) { task }
context 'when work_items feature flag is enabled' do
it 'returns a work item url for the task' do
expect(presenter.web_url).to eq(project_work_items_url(project, work_items_path: presented_issue.id))
end
end
context 'when work_items feature flag is disabled' do
before do
stub_feature_flags(work_items: false)
end
it 'returns an issue url for the task' do
expect(presenter.web_url).to eq("http://localhost/#{group.name}/#{project.name}/-/issues/#{presented_issue.iid}")
end
end
end end
end end
@ -29,7 +52,7 @@ RSpec.describe IssuePresenter do
end end
it 'returns subscribed' do it 'returns subscribed' do
create(:subscription, user: user, project: project, subscribable: issue, subscribed: true) create(:subscription, user: user, project: project, subscribable: presented_issue, subscribed: true)
is_expected.to be(true) is_expected.to be(true)
end end
@ -37,7 +60,27 @@ RSpec.describe IssuePresenter do
describe '#issue_path' do describe '#issue_path' do
it 'returns correct path' do it 'returns correct path' do
expect(presenter.issue_path).to eq("/#{group.name}/#{project.name}/-/issues/#{issue.iid}") expect(presenter.issue_path).to eq("/#{group.name}/#{project.name}/-/issues/#{presented_issue.iid}")
end
context 'when issue type is task' do
let(:presented_issue) { task }
context 'when work_items feature flag is enabled' do
it 'returns a work item path for the task' do
expect(presenter.issue_path).to eq(project_work_items_path(project, work_items_path: presented_issue.id))
end
end
context 'when work_items feature flag is disabled' do
before do
stub_feature_flags(work_items: false)
end
it 'returns an issue path for the task' do
expect(presenter.issue_path).to eq("/#{group.name}/#{project.name}/-/issues/#{presented_issue.iid}")
end
end
end end
end end

View File

@ -1,118 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'groups/edit.html.haml' do
include Devise::Test::ControllerHelpers
describe '"Share with group lock" setting' do
let(:root_owner) { create(:user) }
let(:root_group) { create(:group) }
before do
root_group.add_owner(root_owner)
end
shared_examples_for '"Share with group lock" setting' do |checkbox_options|
it 'has the correct label, help text, and checkbox options' do
assign(:group, test_group)
allow(view).to receive(:can?).with(test_user, :admin_group, test_group).and_return(true)
allow(view).to receive(:can_change_group_visibility_level?).and_return(false)
allow(view).to receive(:current_user).and_return(test_user)
expect(view).to receive(:can_change_share_with_group_lock?).and_return(!checkbox_options[:disabled])
expect(view).to receive(:share_with_group_lock_help_text).and_return('help text here')
render
expect(rendered).to have_content("Prevent sharing a project within #{test_group.name} with other groups")
expect(rendered).to have_content('help text here')
expect(rendered).to have_field('group_share_with_group_lock', **checkbox_options)
end
end
context 'for a root group' do
let(:test_group) { root_group }
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
context 'for a subgroup' do
let!(:subgroup) { create(:group, parent: root_group) }
let(:sub_owner) { create(:user) }
let(:test_group) { subgroup }
context 'when the root_group has "Share with group lock" disabled' do
context 'when the subgroup has "Share with group lock" disabled' do
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
end
context 'when the subgroup has "Share with group lock" enabled' do
before do
subgroup.update_column(:share_with_group_lock, true)
end
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
end
end
end
context 'when the root_group has "Share with group lock" enabled' do
before do
root_group.update_column(:share_with_group_lock, true)
end
context 'when the subgroup has "Share with group lock" disabled (parent overridden)' do
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
end
end
context 'when the subgroup has "Share with group lock" enabled (same as parent)' do
before do
subgroup.update_column(:share_with_group_lock, true)
end
context 'as the root_owner' do
let(:test_user) { root_owner }
it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
end
context 'as the sub_owner' do
let(:test_user) { sub_owner }
it_behaves_like '"Share with group lock" setting', { disabled: true, checked: true }
end
end
end
end
end
end