Add latest changes from gitlab-org/gitlab@master
|
@ -293,14 +293,14 @@ coverage-frontend:
|
|||
- *yarn-install
|
||||
- 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:
|
||||
extends: .qa-frontend-node
|
||||
image: ${GITLAB_DEPENDENCY_PROXY}node:14
|
||||
|
||||
qa-frontend-node:16:
|
||||
extends: .qa-frontend-node
|
||||
image: ${GITLAB_DEPENDENCY_PROXY}node:16
|
||||
|
||||
qa-frontend-node:latest:
|
||||
extends:
|
||||
- .qa-frontend-node
|
||||
|
|
|
@ -1252,7 +1252,7 @@ GEM
|
|||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
sqlite3 (1.3.13)
|
||||
sqlite3 (1.4.2)
|
||||
ssh_data (1.2.0)
|
||||
ssrf_filter (1.0.7)
|
||||
stackprof (0.2.15)
|
||||
|
|
|
@ -12,6 +12,7 @@ const PERSISTENT_USER_CALLOUTS = [
|
|||
'.js-security-newsletter-callout',
|
||||
'.js-approaching-seats-count-threshold',
|
||||
'.js-storage-enforcement-banner',
|
||||
'.js-user-over-limit-free-plan-alert',
|
||||
];
|
||||
|
||||
const initCallouts = () => {
|
||||
|
|
|
@ -4,9 +4,11 @@ import { createAlert } from '~/flash';
|
|||
import { updateHistory } from '~/lib/utils/url_utility';
|
||||
import { formatNumber } from '~/locale';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
||||
import RegistrationDropdown from '../components/registration/registration_dropdown.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 RunnerName from '../components/runner_name.vue';
|
||||
import RunnerStats from '../components/stat/runner_stats.vue';
|
||||
|
@ -53,6 +55,7 @@ export default {
|
|||
GlLink,
|
||||
RegistrationDropdown,
|
||||
RunnerFilteredSearchBar,
|
||||
RunnerBulkDelete,
|
||||
RunnerList,
|
||||
RunnerName,
|
||||
RunnerStats,
|
||||
|
@ -60,6 +63,8 @@ export default {
|
|||
RunnerTypeTabs,
|
||||
RunnerActionsCell,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: ['localMutations'],
|
||||
props: {
|
||||
registrationToken: {
|
||||
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: {
|
||||
search: {
|
||||
|
@ -238,6 +248,12 @@ export default {
|
|||
reportToSentry(error) {
|
||||
captureException({ error, component: this.$options.name });
|
||||
},
|
||||
onChecked({ runner, isChecked }) {
|
||||
this.localMutations.setRunnerChecked({
|
||||
runner,
|
||||
isChecked,
|
||||
});
|
||||
},
|
||||
},
|
||||
filteredSearchNamespace: ADMIN_FILTERED_SEARCH_NAMESPACE,
|
||||
INSTANCE_TYPE,
|
||||
|
@ -286,7 +302,13 @@ export default {
|
|||
{{ __('No runners found') }}
|
||||
</div>
|
||||
<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 }">
|
||||
<gl-link :href="runner.adminUrl">
|
||||
<runner-name :runner="runner" />
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { GlToast } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
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';
|
||||
|
||||
Vue.use(GlToast);
|
||||
|
@ -27,8 +28,10 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
|
|||
|
||||
const { runnerInstallHelpPage, registrationToken } = el.dataset;
|
||||
|
||||
const { cacheConfig, typeDefs, localMutations } = createLocalState();
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
defaultClient: createDefaultClient({}, { cacheConfig, typeDefs }),
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
|
@ -36,6 +39,7 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
|
|||
apolloProvider,
|
||||
provide: {
|
||||
runnerInstallHelpPage,
|
||||
localMutations,
|
||||
},
|
||||
render(h) {
|
||||
return h(AdminRunnersApp, {
|
||||
|
|
111
app/assets/javascripts/runner/components/runner_bulk_delete.vue
Normal 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>
|
|
@ -4,11 +4,22 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/toolt
|
|||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { __, s__ } from '~/locale';
|
||||
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 RunnerSummaryCell from './cells/runner_summary_cell.vue';
|
||||
import RunnerStatusCell from './cells/runner_status_cell.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 {
|
||||
components: {
|
||||
GlTableLite,
|
||||
|
@ -22,7 +33,20 @@ export default {
|
|||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
apollo: {
|
||||
checkedRunnerIds: {
|
||||
query: checkedRunnerIdsQuery,
|
||||
skip() {
|
||||
return !this.checkable;
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
checkable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -33,6 +57,10 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['checked'],
|
||||
data() {
|
||||
return { checkedRunnerIds: [] };
|
||||
},
|
||||
computed: {
|
||||
tableClass() {
|
||||
// <gl-table-lite> does not provide a busy state, add
|
||||
|
@ -42,6 +70,18 @@ export default {
|
|||
'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: {
|
||||
formatJobCount(jobCount) {
|
||||
|
@ -55,16 +95,16 @@ export default {
|
|||
}
|
||||
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>
|
||||
<template>
|
||||
|
@ -73,13 +113,29 @@ export default {
|
|||
:aria-busy="loading"
|
||||
:class="tableClass"
|
||||
:items="runners"
|
||||
:fields="$options.fields"
|
||||
:fields="fields"
|
||||
:tbody-tr-attr="runnerTrAttr"
|
||||
data-testid="runner-list"
|
||||
stacked="md"
|
||||
primary-key="id"
|
||||
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 }">
|
||||
<runner-status-cell :runner="item" />
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
query getCheckedRunnerIds {
|
||||
checkedRunnerIds @client
|
||||
}
|
63
app/assets/javascripts/runner/graphql/list/local_state.js
Normal 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,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
extend type Query {
|
||||
checkedRunnerIds: [ID!]!
|
||||
}
|
|
@ -24,7 +24,7 @@ export const formatJobCount = (jobCount) => {
|
|||
* @param {Object} options
|
||||
* @returns Field object to add to GlTable fields
|
||||
*/
|
||||
export const tableField = ({ key, label = '', thClasses = [] }) => {
|
||||
export const tableField = ({ key, label = '', thClasses = [], ...options }) => {
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
|
@ -32,6 +32,7 @@ export const tableField = ({ key, label = '', thClasses = [] }) => {
|
|||
tdAttr: {
|
||||
'data-testid': `td-${key}`,
|
||||
},
|
||||
...options,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import { __, s__ } from '~/locale';
|
|||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.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 AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
|
||||
import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
|
||||
|
@ -52,7 +51,7 @@ export default {
|
|||
TrainingProviderList,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
inject: ['projectFullPath'],
|
||||
inject: ['projectFullPath', 'vulnerabilityTrainingDocsPath'],
|
||||
props: {
|
||||
augmentedSecurityFeatures: {
|
||||
type: Array,
|
||||
|
@ -127,9 +126,6 @@ export default {
|
|||
},
|
||||
},
|
||||
autoDevopsEnabledAlertStorageKey: AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
|
||||
securityTraininDocLink: helpPagePath('user/application_security/vulnerabilities/index', {
|
||||
anchor: 'enable-security-training-for-vulnerabilities',
|
||||
}),
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -268,7 +264,7 @@ export default {
|
|||
{{ $options.i18n.securityTrainingDescription }}
|
||||
</p>
|
||||
<p>
|
||||
<gl-link :href="$options.securityTraininDocLink">{{
|
||||
<gl-link :href="vulnerabilityTrainingDocsPath">{{
|
||||
$options.i18n.securityTrainingDoc
|
||||
}}</gl-link>
|
||||
</p>
|
||||
|
|
|
@ -25,6 +25,7 @@ export const initSecurityConfiguration = (el) => {
|
|||
gitlabCiHistoryPath,
|
||||
autoDevopsHelpPagePath,
|
||||
autoDevopsPath,
|
||||
vulnerabilityTrainingDocsPath,
|
||||
} = el.dataset;
|
||||
|
||||
const { augmentedSecurityFeatures, augmentedComplianceFeatures } = augmentFeatures(
|
||||
|
@ -41,6 +42,7 @@ export const initSecurityConfiguration = (el) => {
|
|||
upgradePath,
|
||||
autoDevopsHelpPagePath,
|
||||
autoDevopsPath,
|
||||
vulnerabilityTrainingDocsPath,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(SecurityConfigurationApp, {
|
||||
|
|
|
@ -25,7 +25,7 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<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">
|
||||
<template v-if="options.title" #title>
|
||||
<span v-safe-html="options.title"></span>
|
||||
|
|
|
@ -4,6 +4,9 @@ class Admin::RunnersController < Admin::ApplicationController
|
|||
include RunnerSetupScripts
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -6,6 +6,10 @@ module Projects
|
|||
def security_upgrade_path
|
||||
"https://#{ApplicationHelper.promo_host}/pricing/"
|
||||
end
|
||||
|
||||
def vulnerability_training_docs_path
|
||||
help_page_path('user/application_security/vulnerabilities/index', anchor: 'enable-security-training-for-vulnerabilities')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,3 +31,5 @@ module Users
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Users::GroupCalloutsHelper.prepend_mod
|
||||
|
|
|
@ -14,7 +14,8 @@ module Users
|
|||
storage_enforcement_banner_first_enforcement_threshold: 3,
|
||||
storage_enforcement_banner_second_enforcement_threshold: 4,
|
||||
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
|
||||
|
|
|
@ -4,7 +4,9 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
|
|||
presents ::Issue, as: :issue
|
||||
|
||||
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
|
||||
|
||||
delegator_override :subscribed?
|
||||
|
@ -15,6 +17,18 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
|
|||
def project_emails_disabled?
|
||||
issue.project.emails_disabled?
|
||||
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
|
||||
|
||||
IssuePresenter.prepend_mod_with('IssuePresenter')
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
- add_page_specific_style 'page_bundles/members'
|
||||
- page_title _('Group members')
|
||||
|
||||
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @group
|
||||
|
||||
.row.gl-mt-3
|
||||
.col-lg-12
|
||||
.gl-display-flex.gl-flex-wrap
|
||||
|
|
|
@ -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/qrtly_reconciliation_alert', group: @group
|
||||
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @group
|
||||
|
||||
- if show_invite_banner?(@group)
|
||||
= content_for :group_invite_members_banner do
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
= dispensable_render_if_exists "shared/namespace_user_cap_reached_alert"
|
||||
= dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert"
|
||||
= yield :page_level_alert
|
||||
= yield :user_over_limit_free_plan_alert
|
||||
= yield :group_invite_members_banner
|
||||
- unless @hide_breadcrumbs
|
||||
= render "layouts/nav/breadcrumbs"
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
- escaped_default_branch_name = default_branch_name.shellescape
|
||||
- @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 "home_panel"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
- page_title _('No repository')
|
||||
- @skip_current_level_breadcrumb = true
|
||||
|
||||
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
|
||||
|
||||
%h2.gl-display-flex
|
||||
.gl-display-flex.gl-align-items-center.gl-justify-content-center
|
||||
= sprite_icon('warning-solid', size: 24, css_class: 'gl-mr-2')
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
- add_page_specific_style 'page_bundles/members'
|
||||
- page_title _("Members")
|
||||
|
||||
= render_if_exists 'shared/user_over_limit_free_plan_alert', source: @project
|
||||
|
||||
.row.gl-mt-3
|
||||
.col-lg-12
|
||||
- if can_invite_members_for_project?(@project)
|
||||
|
|
|
@ -3,5 +3,6 @@
|
|||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
|
||||
#js-security-configuration{ data: { **@configuration.to_html_data_attribute,
|
||||
vulnerability_training_docs_path: vulnerability_training_docs_path,
|
||||
upgrade_path: security_upgrade_path,
|
||||
project_full_path: @project.full_path } }
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
= content_for :meta_tags do
|
||||
= 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 "projects/last_push"
|
||||
|
|
|
@ -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
|
16
data/removals/15_0/15-0-rerequest-review.yml
Normal 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
|
|
@ -14,3 +14,4 @@ swap:
|
|||
needs? to: "Rewrite the sentence, or use 'must', instead of"
|
||||
note that: "Be concise: rewrite the sentence to not use"
|
||||
please: "Remove this word from the sentence: "
|
||||
respectively: "Rewrite the sentence to be more precise, instead of using "
|
||||
|
|
|
@ -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. |
|
||||
| [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). |
|
||||
| [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
|
||||
|
||||
|
@ -263,7 +263,7 @@ GitLab requires the use of Node to compile JavaScript
|
|||
assets, and Yarn to manage JavaScript dependencies. The current minimum
|
||||
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)
|
||||
|
||||
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:
|
||||
|
||||
```shell
|
||||
# install node v14.x
|
||||
curl --location "https://deb.nodesource.com/setup_14.x" | sudo bash -
|
||||
# install node v16.x
|
||||
curl --location "https://deb.nodesource.com/setup_16.x" | sudo bash -
|
||||
sudo apt-get install -y nodejs
|
||||
|
||||
npm install --global yarn
|
||||
|
|
Before Width: | Height: | Size: 43 KiB |
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
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
|
||||
[Prometheus available](../../../user/project/integrations/prometheus.md). You can
|
||||
|
|
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
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
|
||||
[custom dashboards](index.md). Templates provide helpful guidance and
|
||||
|
|
|
@ -11,7 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
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
|
||||
includes a few key metrics, but you can also define your own custom dashboards.
|
||||
|
|
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
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.
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
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
|
||||
display the time zone of your choice, and the links of your choice.
|
||||
|
|
|
@ -11,7 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
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.
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
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
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
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:
|
||||
|
||||
|
|
|
@ -28,6 +28,12 @@ For removal reviewers (Technical Writers only):
|
|||
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
|
||||
|
||||
### Integrated error tracking disabled by default
|
||||
|
|
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 18 KiB |
|
@ -422,7 +422,7 @@ The following table lists group permissions available for each role:
|
|||
| View 2FA status of members | | | | | ✓ |
|
||||
| View [Billing](../subscriptions/gitlab_com/index.md#view-your-gitlab-saas-subscription) | | | | | ✓ (4) |
|
||||
| View [Usage Quotas](usage_quotas.md) Page | | | | ✓ | ✓ (4) |
|
||||
| Manage runners | | | | | ✓ |
|
||||
| Manage group runners | | | | | ✓ |
|
||||
|
||||
<!-- markdownlint-disable MD029 -->
|
||||
|
||||
|
|
Before Width: | Height: | Size: 27 KiB |
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
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
|
||||
with the [Elastic Load Balancer](https://aws.amazon.com/elasticloadbalancing/) (ELB).
|
||||
|
|
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
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.
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
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/).
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
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.
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
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.
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
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.
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
WARNING:
|
||||
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:
|
||||
[NGINX Ingress version 0.16](nginx_ingress.md) and above have built-in Prometheus metrics, which are different than the VTS based metrics.
|
||||
|
|
|
@ -112,7 +112,13 @@ This example shows reviewers and approval rules in a merge request sidebar:
|
|||
|
||||
### 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),
|
||||
the author of the merge request can request a new review from the reviewer:
|
||||
|
|
|
@ -15153,6 +15153,9 @@ msgstr ""
|
|||
msgid "Explore groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "Explore paid plans"
|
||||
msgstr ""
|
||||
|
||||
msgid "Explore projects"
|
||||
msgstr ""
|
||||
|
||||
|
@ -16258,6 +16261,9 @@ msgstr ""
|
|||
msgid "From %{providerTitle}"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22896,6 +22902,9 @@ msgstr ""
|
|||
msgid "Manage labels"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage members"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage milestones"
|
||||
msgstr ""
|
||||
|
||||
|
@ -32184,6 +32193,16 @@ msgstr ""
|
|||
msgid "Runners|%{percentage} spot."
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
|
@ -32229,9 +32248,15 @@ msgstr ""
|
|||
msgid "Runners|Change to project runner"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Checkbox"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Choose your preferred GitLab Runner"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Clear selection"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Command to register runner"
|
||||
msgstr ""
|
||||
|
||||
|
@ -32244,12 +32269,20 @@ msgstr ""
|
|||
msgid "Runners|Copy registration token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Delete %d runner"
|
||||
msgid_plural "Runners|Delete %d runners"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Runners|Delete runner"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Delete runner %{name}?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Delete selected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Deploy GitLab Runner in AWS"
|
||||
msgstr ""
|
||||
|
||||
|
@ -32340,6 +32373,11 @@ msgstr ""
|
|||
msgid "Runners|Paused"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Permanently delete %d runner"
|
||||
msgid_plural "Runners|Permanently delete %d runners"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Runners|Platform"
|
||||
msgstr ""
|
||||
|
||||
|
@ -43243,6 +43281,9 @@ msgstr ""
|
|||
msgid "YouTube URL or ID"
|
||||
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}."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -75,13 +75,16 @@ module QA
|
|||
# Download files from GCS bucket by environment name
|
||||
# Delete the files afterward
|
||||
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
|
||||
end
|
||||
|
||||
return puts "\nNothing to download!" if files_list.empty?
|
||||
return puts "\nNothing to download!" if files_list.blank?
|
||||
|
||||
FileUtils.mkdir_p('tmp/')
|
||||
|
||||
files_list.each do |file_name|
|
||||
local_path = "tmp/#{file_name.split('/').last}"
|
||||
Runtime::Logger.info("Downloading #{file_name} to #{local_path}")
|
||||
|
|
|
@ -58,6 +58,11 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
trait :task do
|
||||
issue_type { :task }
|
||||
association :work_item_type, :default, :task
|
||||
end
|
||||
|
||||
factory :incident do
|
||||
issue_type { :incident }
|
||||
association :work_item_type, :default, :incident
|
||||
|
|
|
@ -37,5 +37,10 @@ FactoryBot.define do
|
|||
base_type { WorkItems::Type.base_types[:requirement] }
|
||||
icon_name { 'issue-type-requirements' }
|
||||
end
|
||||
|
||||
trait :task do
|
||||
base_type { WorkItems::Type.base_types[:task] }
|
||||
icon_name { 'issue-type-task' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -135,7 +135,7 @@ describe('Grouped code quality reports app', () => {
|
|||
});
|
||||
|
||||
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', () => {
|
||||
|
@ -144,7 +144,7 @@ describe('Grouped code quality reports app', () => {
|
|||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@ import { createAlert } from '~/flash';
|
|||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
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 RunnerTypeTabs from '~/runner/components/runner_type_tabs.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';
|
||||
|
||||
const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
|
||||
const mockRunners = runnersData.data.runners.nodes;
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/runner/sentry_utils');
|
||||
|
@ -58,6 +60,8 @@ describe('AdminRunnersApp', () => {
|
|||
let wrapper;
|
||||
let mockRunnersQuery;
|
||||
let mockRunnersCountQuery;
|
||||
let cacheConfig;
|
||||
let localMutations;
|
||||
|
||||
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
|
||||
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
|
||||
|
@ -69,18 +73,30 @@ describe('AdminRunnersApp', () => {
|
|||
const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar);
|
||||
const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
|
||||
|
||||
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
|
||||
const createComponent = ({
|
||||
props = {},
|
||||
mountFn = shallowMountExtended,
|
||||
provide,
|
||||
...options
|
||||
} = {}) => {
|
||||
({ cacheConfig, localMutations } = createLocalState());
|
||||
|
||||
const handlers = [
|
||||
[adminRunnersQuery, mockRunnersQuery],
|
||||
[adminRunnersCountQuery, mockRunnersCountQuery],
|
||||
];
|
||||
|
||||
wrapper = mountFn(AdminRunnersApp, {
|
||||
apolloProvider: createMockApollo(handlers),
|
||||
apolloProvider: createMockApollo(handlers, {}, cacheConfig),
|
||||
propsData: {
|
||||
registrationToken: mockRegistrationToken,
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
localMutations,
|
||||
...provide,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -173,7 +189,7 @@ describe('AdminRunnersApp', () => {
|
|||
});
|
||||
|
||||
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 () => {
|
||||
|
@ -181,7 +197,7 @@ describe('AdminRunnersApp', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
const { id, shortSha } = runnersData.data.runners.nodes[0];
|
||||
const { id, shortSha } = mockRunners[0];
|
||||
const numericId = getIdFromGraphQLId(id);
|
||||
|
||||
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 runner = runnersData.data.runners.nodes[0];
|
||||
const runner = mockRunners[0];
|
||||
|
||||
expect(runnerActions.props()).toEqual({
|
||||
runner,
|
||||
|
@ -232,8 +248,7 @@ describe('AdminRunnersApp', () => {
|
|||
describe('Single runner row', () => {
|
||||
let showToast;
|
||||
|
||||
const mockRunner = runnersData.data.runners.nodes[0];
|
||||
const { id: graphqlId, shortSha } = mockRunner;
|
||||
const { id: graphqlId, shortSha } = mockRunners[0];
|
||||
const id = getIdFromGraphQLId(graphqlId);
|
||||
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
|
||||
|
@ -333,6 +348,41 @@ describe('AdminRunnersApp', () => {
|
|||
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', () => {
|
||||
beforeEach(async () => {
|
||||
mockRunnersQuery = jest.fn().mockResolvedValue({
|
||||
|
|
103
spec/frontend/runner/components/runner_bulk_delete_spec.js
Normal 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>`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -55,7 +55,7 @@ describe('RunnerList', () => {
|
|||
});
|
||||
|
||||
it('Sets runner id as a row key', () => {
|
||||
createComponent({});
|
||||
createComponent();
|
||||
|
||||
expect(findTable().attributes('primary-key')).toBe('id');
|
||||
});
|
||||
|
@ -90,6 +90,35 @@ describe('RunnerList', () => {
|
|||
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', () => {
|
||||
it('Render #runner-name slot in "summary" cell', () => {
|
||||
createComponent(
|
||||
|
|
72
spec/frontend/runner/graphql/local_state_spec.js
Normal 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([]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -44,6 +44,10 @@ describe('~/runner/utils', () => {
|
|||
thClass: expect.arrayContaining(mockClasses),
|
||||
});
|
||||
});
|
||||
|
||||
it('a field with custom options', () => {
|
||||
expect(tableField({ foo: 'bar' })).toMatchObject({ foo: 'bar' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPaginationVariables', () => {
|
||||
|
|
|
@ -33,6 +33,7 @@ const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
|
|||
const autoDevopsPath = '/autoDevopsPath';
|
||||
const gitlabCiHistoryPath = 'test/historyPath';
|
||||
const projectFullPath = 'namespace/project';
|
||||
const vulnerabilityTrainingDocsPath = 'user/application_security/vulnerabilities/index';
|
||||
|
||||
useLocalStorageSpy();
|
||||
|
||||
|
@ -55,6 +56,7 @@ describe('App component', () => {
|
|||
autoDevopsHelpPagePath,
|
||||
autoDevopsPath,
|
||||
projectFullPath,
|
||||
vulnerabilityTrainingDocsPath,
|
||||
glFeatures: {
|
||||
secureVulnerabilityTraining,
|
||||
},
|
||||
|
@ -462,9 +464,7 @@ describe('App component', () => {
|
|||
const trainingLink = findVulnerabilityManagementTab().findComponent(GlLink);
|
||||
|
||||
expect(trainingLink.text()).toBe('Learn more about vulnerability training');
|
||||
expect(trainingLink.attributes('href')).toBe(
|
||||
'/help/user/application_security/vulnerabilities/index#enable-security-training-for-vulnerabilities',
|
||||
);
|
||||
expect(trainingLink.attributes('href')).toBe(vulnerabilityTrainingDocsPath);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ describe('HelpPopover', () => {
|
|||
|
||||
it('renders a link button with an icon question', () => {
|
||||
expect(findQuestionButton().props()).toMatchObject({
|
||||
icon: 'question',
|
||||
icon: 'question-o',
|
||||
variant: 'link',
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,4 +10,10 @@ RSpec.describe Projects::Security::ConfigurationHelper do
|
|||
|
||||
it { is_expected.to eq("https://#{ApplicationHelper.promo_host}/pricing/") }
|
||||
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
|
||||
|
|
|
@ -5,19 +5,42 @@ require 'spec_helper'
|
|||
RSpec.describe IssuePresenter do
|
||||
include Gitlab::Routing.url_helpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:group) { create(:group) }
|
||||
let(:project) { create(:project, group: group) }
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
let(:presenter) { described_class.new(issue, current_user: user) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:issue) { create(:issue, project: project) }
|
||||
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)
|
||||
end
|
||||
|
||||
describe '#web_url' 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
|
||||
|
||||
|
@ -29,7 +52,7 @@ RSpec.describe IssuePresenter do
|
|||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
@ -37,7 +60,27 @@ RSpec.describe IssuePresenter do
|
|||
|
||||
describe '#issue_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
|
||||
|
||||
|
|
|
@ -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
|