Commit fabfde31 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 94289eb0 6a1d05d4
......@@ -18,6 +18,11 @@ Every feature introduced to the codebase, even if it's behind a feature flag,
must be documented. For context, see the
[latest merge request that updated this guideline](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47917#note_459984428).
When you document feature flags, you must:
- [Add a note at the start of the topic](#use-a-note-to-describe-the-state-of-the-feature-flag).
- [Add version history text](#add-version-history-text).
## Use a note to describe the state of the feature flag
Information about feature flags should be in a **Note** at the start of the topic (just below the version history).
......@@ -47,8 +52,9 @@ NOTE:
|If the feature is... | Use this text |
|-|-|
|Available| `On GitLab SaaS, this feature is available.` |
|Unavailable| `On GitLab SaaS, this feature is not available.`|
|Available| `On GitLab.com, this feature is available.` |
|Available to GitLab.com admins only| `On GitLab.com, this feature is available but can be configured by GitLab.com administrators only.`
|Unavailable| `On GitLab.com, this feature is not available.`|
### Optional information
......@@ -63,7 +69,8 @@ When the state of a flag changes (for example, disabled by default to enabled by
Possible version history entries are:
```markdown
> - [Enabled for GitLab.com](issue-link) in GitLab X.X and is ready for production use.
> - [Enabled on GitLab.com](issue-link) in GitLab X.X and is ready for production use.
> - [Enabled on GitLab.com](issue-link) in GitLab X.X and is ready for production use. Available to GitLab.com administrators only.
> - [Enabled with <flag name> flag](issue-link) for self-managed GitLab in GitLab X.X and is ready for production use.
> - [Feature flag <flag name> removed](issue-line) in GitLab X.X.
```
......@@ -96,7 +103,7 @@ And, when the feature is done and fully available to all users:
```markdown
> - Introduced in GitLab 13.7.
> - [Enabled for GitLab.com](https://gitlab.com/issue/etc) in GitLab X.X and is ready for production use.
> - [Enabled on GitLab.com](https://gitlab.com/issue/etc) in GitLab X.X and is ready for production use.
> - [Enabled with `forti_token_cloud` flag](https://gitlab.com/issue/etc) for self-managed GitLab in GitLab X.X and is ready for production use.
> - [Feature flag `forti_token_cloud`](https://gitlab.com/issue/etc) removed in GitLab X.X.
```
......@@ -1110,8 +1110,9 @@ Prerequisites:
To validate a site profile:
1. From your project's home page, go to **Security & Compliance > Configuration**.
1. In the **DAST Profiles** row select **Manage**.
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Security & Compliance > Configuration**.
1. In the **Dynamic Application Security Testing (DAST)** section, select **Manage scans**.
1. Select the **Site Profiles** tab.
1. In the profile's row select **Validate** or **Retry validation**.
1. Select the validation method.
......
......@@ -2,6 +2,12 @@ import { buildApiUrl } from '~/api/api_utils';
import axios from '~/lib/utils/axios_utils';
const SUBSCRIPTIONS_PATH = '/api/:version/subscriptions';
const EXTEND_REACTIVATE_TRIAL_PATH = '/-/trials/extend_reactivate';
const TRIAL_EXTENSION_TYPE = Object.freeze({
extended: 1,
reactivated: 2,
});
export function createSubscription(groupId, customer, subscription) {
const url = buildApiUrl(SUBSCRIPTIONS_PATH);
......@@ -13,3 +19,25 @@ export function createSubscription(groupId, customer, subscription) {
return axios.post(url, { params });
}
const updateTrial = async (namespaceId, trialExtensionType) => {
if (!Object.values(TRIAL_EXTENSION_TYPE).includes(trialExtensionType)) {
throw new TypeError('The "trialExtensionType" argument is invalid.');
}
const url = buildApiUrl(EXTEND_REACTIVATE_TRIAL_PATH);
const params = {
namespace_id: namespaceId,
trial_extension_type: trialExtensionType,
};
return axios.put(url, params);
};
export const extendTrial = async (namespaceId) => {
return updateTrial(namespaceId, TRIAL_EXTENSION_TYPE.extended);
};
export const reactivateTrial = async (namespaceId) => {
return updateTrial(namespaceId, TRIAL_EXTENSION_TYPE.reactivated);
};
......@@ -9,7 +9,7 @@ export default {
},
inject: {
namespaceId: {
default: '',
default: null,
},
},
created() {
......
......@@ -9,6 +9,7 @@ import {
DAYS_FOR_RENEWAL,
PLAN_TITLE_TRIAL_TEXT,
} from 'ee/billings/constants';
import ExtendReactivateTrialButton from 'ee/trials/extend_reactivate_trial/components/extend_reactivate_trial_button.vue';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { getDayDifference } from '~/lib/utils/datetime/date_calculation_utility';
......@@ -24,6 +25,7 @@ export default {
GlButton,
GlLoadingIcon,
SubscriptionTableRow,
ExtendReactivateTrialButton,
},
mixins: [glFeatureFlagsMixin()],
inject: {
......@@ -34,7 +36,7 @@ export default {
default: '',
},
namespaceId: {
default: '',
default: null,
},
customerPortalUrl: {
default: '',
......@@ -54,6 +56,9 @@ export default {
refreshSeatsHref: {
default: '',
},
availableTrialAction: {
default: null,
},
},
computed: {
...mapState([
......@@ -182,7 +187,14 @@ export default {
data-testid="subscription-header"
>
<strong>{{ subscriptionHeader }}</strong>
<div class="controls">
<div class="gl-display-flex">
<extend-reactivate-trial-button
v-if="availableTrialAction"
:namespace-id="namespaceId"
:action="availableTrialAction"
:plan-name="planName"
class="gl-mr-3"
/>
<gl-button
v-for="(button, index) in buttons"
:key="button.text"
......
......@@ -24,13 +24,14 @@ export default (containerId = 'js-billing-plans') => {
planName,
freePersonalNamespace,
refreshSeatsHref,
action,
} = containerEl.dataset;
return new Vue({
el: containerEl,
store: new Vuex.Store(initialStore()),
provide: {
namespaceId,
namespaceId: Number(namespaceId),
namespaceName,
addSeatsHref,
planUpgradeHref,
......@@ -40,6 +41,7 @@ export default (containerId = 'js-billing-plans') => {
planName,
freePersonalNamespace: parseBoolean(freePersonalNamespace),
refreshSeatsHref,
availableTrialAction: action,
},
render(createElement) {
return createElement(SubscriptionApp);
......
import { shouldQrtlyReconciliationMount } from 'ee/billings/qrtly_reconciliation';
import initSubscriptions from 'ee/billings/subscriptions';
import { shouldExtendReactivateTrialButtonMount } from 'ee/trials/extend_reactivate_trial';
import PersistentUserCallout from '~/persistent_user_callout';
PersistentUserCallout.factory(document.querySelector('.js-gold-trial-callout'));
initSubscriptions();
shouldExtendReactivateTrialButtonMount();
shouldQrtlyReconciliationMount();
import initSubscriptions from 'ee/billings/subscriptions';
import { shouldExtendReactivateTrialButtonMount } from 'ee/trials/extend_reactivate_trial';
import PersistentUserCallout from '~/persistent_user_callout';
PersistentUserCallout.factory(document.querySelector('.js-gold-trial-callout'));
shouldExtendReactivateTrialButtonMount();
initSubscriptions();
<script>
import PoliciesHeader from './policies_header.vue';
export default {
components: {
PoliciesHeader,
},
};
</script>
<template>
<policies-header />
</template>
<script>
import { GlAlert, GlSprintf, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import ScanNewPolicyModal from './scan_new_policy_modal.vue';
export default {
components: {
GlSprintf,
GlButton,
GlAlert,
ScanNewPolicyModal,
},
inject: ['documentationPath', 'assignedPolicyProject', 'newPolicyPath'],
i18n: {
title: s__('NetworkPolicies|Policies'),
subtitle: s__(
'NetworkPolicies|Enforce security for this project. %{linkStart}More information.%{linkEnd}',
),
newPolicyButtonText: s__('NetworkPolicies|New policy'),
editPolicyButtonText: s__('NetworkPolicies|Edit policy project'),
},
data() {
return {
projectIsBeingLinked: false,
showAlert: false,
alertVariant: '',
alertText: '',
modalVisible: false,
};
},
computed: {
hasAssignedPolicyProject() {
return Boolean(this.assignedPolicyProject?.id);
},
},
methods: {
updateAlertText({ text, variant }) {
this.projectIsBeingLinked = false;
if (text) {
this.showAlert = true;
this.alertVariant = variant;
this.alertText = text;
}
},
isUpdatingProject() {
this.projectIsBeingLinked = true;
this.showAlert = false;
this.alertVariant = '';
this.alertText = '';
},
dismissAlert() {
this.showAlert = false;
},
showNewPolicyModal() {
this.modalVisible = true;
},
},
};
</script>
<template>
<div>
<gl-alert
v-if="showAlert"
class="gl-mt-3"
data-testid="policy-project-alert"
:dismissible="true"
:variant="alertVariant"
@dismiss="dismissAlert"
>
{{ alertText }}
</gl-alert>
<header class="gl-my-6 gl-display-flex gl-align-items-flex-start">
<div class="gl-flex-grow-1 gl-my-0">
<h2 class="gl-mt-0">
{{ $options.i18n.title }}
</h2>
<p data-testid="policies-subheader">
<gl-sprintf :message="$options.i18n.subtitle">
<template #link="{ content }">
<gl-button class="gl-pb-1!" variant="link" :href="documentationPath" target="_blank">
{{ content }}
</gl-button>
</template>
</gl-sprintf>
</p>
</div>
<gl-button
data-testid="edit-project-policy-button"
class="gl-mr-4"
:loading="projectIsBeingLinked"
@click="showNewPolicyModal"
>
{{ $options.i18n.editPolicyButtonText }}
</gl-button>
<gl-button data-testid="new-policy-button" variant="confirm" :href="newPolicyPath">
{{ $options.i18n.newPolicyButtonText }}
</gl-button>
<scan-new-policy-modal
:visible="modalVisible"
@close="modalVisible = false"
@project-updated="updateAlertText"
@updating-project="isUpdatingProject"
/>
</header>
</div>
</template>
<script>
import { GlAlert, GlButton, GlDropdown, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import assignSecurityPolicyProject from '../graphql/mutations/assign_security_policy_project.mutation.graphql';
import InstanceProjectSelector from './instance_project_selector.vue';
import { GlButton, GlDropdown, GlSprintf, GlAlert, GlModal } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import assignSecurityPolicyProject from '../../graphql/mutations/assign_security_policy_project.mutation.graphql';
import InstanceProjectSelector from '../instance_project_selector.vue';
export default {
PROJECT_SELECTOR_HEIGHT: 204,
i18n: {
assignError: s__(
'SecurityOrchestration|An error occurred assigning your security policy project',
),
assignSuccess: s__('SecurityOrchestration|Security policy project was linked successfully'),
disabledButtonTooltip: s__(
'SecurityOrchestration|Only owners can update Security Policy Project',
),
securityProject: s__(
'SecurityOrchestration|A security policy project can enforce policies for a given project, group, or instance. With a security policy project, you can specify security policies that are important to you and enforce them with every commit. %{linkStart}More information.%{linkEnd}',
modal: {
okTitle: __('Save'),
header: s__('SecurityOrchestration|Select security project'),
},
save: {
ok: s__('SecurityOrchestration|Security policy project was linked successfully'),
error: s__('SecurityOrchestration|An error occurred assigning your security policy project'),
},
disabledWarning: s__('SecurityOrchestration|Only owners can update Security Policy Project'),
description: s__(
'SecurityOrchestration|Select a project to store your security policies in. %{linkStart}More information.%{linkEnd}',
),
},
components: {
GlAlert,
GlButton,
GlDropdown,
GlSprintf,
GlModal,
GlAlert,
InstanceProjectSelector,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['disableSecurityPolicyProject', 'documentationPath', 'projectPath'],
inject: [
'disableSecurityPolicyProject',
'documentationPath',
'projectPath',
'assignedPolicyProject',
],
props: {
assignedPolicyProject: {
type: Object,
visible: {
type: Boolean,
required: false,
default: () => {
return { id: '', name: '' };
},
default: false,
},
},
data() {
return {
currentProjectId: this.assignedPolicyProject.id,
selectedProject: this.assignedPolicyProject,
isAssigningProject: false,
showAssignError: false,
showAssignSuccess: false,
selectedProject: { ...this.assignedPolicyProject },
hasSelectedNewProject: false,
};
},
computed: {
hasSelectedNewProject() {
return this.currentProjectId !== this.selectedProject.id;
selectedProjectId() {
return this.selectedProject?.id || '';
},
selectedProjectName() {
return this.selectedProject?.name || '';
},
isModalOkButtonDisabled() {
return this.disableSecurityPolicyProject || !this.hasSelectedNewProject;
},
},
methods: {
dismissAlert(type) {
this[type] = false;
},
async saveChanges() {
this.isAssigningProject = true;
this.showAssignError = false;
this.showAssignSuccess = false;
const { id } = this.selectedProject;
this.$emit('updating-project');
try {
const { data } = await this.$apollo.mutate({
mutation: assignSecurityPolicyProject,
variables: {
input: {
projectPath: this.projectPath,
securityPolicyProjectId: id,
securityPolicyProjectId: this.selectedProjectId,
},
},
});
if (data?.securityPolicyProjectAssign?.errors?.length) {
this.showAssignError = true;
} else {
this.showAssignSuccess = true;
this.currentProjectId = id;
throw new Error(data.securityPolicyProjectAssign.errors);
}
this.$emit('project-updated', { text: this.$options.i18n.save.ok, variant: 'success' });
} catch {
this.showAssignError = true;
this.$emit('project-updated', { text: this.$options.i18n.save.error, variant: 'danger' });
} finally {
this.isAssigningProject = false;
this.hasSelectedNewProject = false;
}
},
setSelectedProject(data) {
this.hasSelectedNewProject = true;
this.selectedProject = data;
this.$refs.dropdown.hide(true);
this.$refs.dropdown.hide();
},
closeModal() {
this.$emit('close');
},
},
};
</script>
<template>
<section>
<gl-alert
v-if="showAssignError"
class="gl-mt-3"
data-testid="policy-project-assign-error"
variant="danger"
:dismissible="true"
@dismiss="dismissAlert('showAssignError')"
>
{{ $options.i18n.assignError }}
</gl-alert>
<gl-alert
v-else-if="showAssignSuccess"
class="gl-mt-3"
data-testid="policy-project-assign-success"
variant="success"
:dismissible="true"
@dismiss="dismissAlert('showAssignSuccess')"
>
{{ $options.i18n.assignSuccess }}
</gl-alert>
<h2 class="gl-mb-8">
{{ s__('SecurityOrchestration|Create a policy') }}
</h2>
<div class="gl-w-half">
<h4>
{{ s__('SecurityOrchestration|Security policy project') }}
</h4>
<gl-modal
v-bind="$attrs"
ref="modal"
cancel-variant="light"
size="sm"
modal-id="scan-new-policy"
:scrollable="false"
:ok-title="$options.i18n.modal.okTitle"
:title="$options.i18n.modal.header"
:ok-disabled="isModalOkButtonDisabled"
:visible="visible"
@ok="saveChanges"
@change="closeModal"
>
<div>
<gl-alert
v-if="disableSecurityPolicyProject"
class="gl-mb-4"
variant="warning"
:dismissible="false"
>
{{ $options.i18n.disabledWarning }}
</gl-alert>
<gl-dropdown
ref="dropdown"
class="gl-w-full gl-pb-5 security-policy-dropdown"
class="gl-w-full gl-pb-5"
menu-class="gl-w-full! gl-max-w-full!"
:disabled="disableSecurityPolicyProject"
:text="selectedProject.name || ''"
:text="selectedProjectName"
>
<instance-project-selector
class="gl-w-full"
......@@ -135,7 +135,7 @@ export default {
/>
</gl-dropdown>
<div class="gl-pb-5">
<gl-sprintf :message="$options.i18n.securityProject">
<gl-sprintf :message="$options.i18n.description">
<template #link="{ content }">
<gl-button class="gl-pb-1!" variant="link" :href="documentationPath" target="_blank">
{{ content }}
......@@ -143,24 +143,6 @@ export default {
</template>
</gl-sprintf>
</div>
<span
v-gl-tooltip="{
disabled: !disableSecurityPolicyProject,
title: $options.i18n.disabledButtonTooltip,
placement: 'bottom',
}"
data-testid="disabled-button-tooltip"
>
<gl-button
data-testid="save-policy-project"
variant="confirm"
:disabled="disableSecurityPolicyProject || !hasSelectedNewProject"
:loading="isAssigningProject"
@click="saveChanges"
>
{{ __('Save changes') }}
</gl-button>
</span>
</div>
</section>
</gl-modal>
</template>
......@@ -2,7 +2,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import SecurityPolicyProjectSelector from './components/security_policy_project_selector.vue';
import SecurityPoliciesApp from './components/policies/policies_app.vue';
Vue.use(VueApollo);
......@@ -16,24 +16,22 @@ export default () => {
assignedPolicyProject,
disableSecurityPolicyProject,
documentationPath,
newPolicyPath,
projectPath,
} = el.dataset;
const policyProject = JSON.parse(assignedPolicyProject);
const props = policyProject ? { assignedPolicyProject: policyProject } : {};
return new Vue({
apolloProvider,
el,
provide: {
assignedPolicyProject: JSON.parse(assignedPolicyProject),
disableSecurityPolicyProject: parseBoolean(disableSecurityPolicyProject),
documentationPath,
newPolicyPath,
projectPath,
},
render(createElement) {
return createElement(SecurityPolicyProjectSelector, {
props,
});
return createElement(SecurityPoliciesApp);
},
});
};
<script>
import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { extendTrial, reactivateTrial } from 'ee/api/subscriptions_api';
import createFlash from '~/flash';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { sprintf, __ } from '~/locale';
import { i18n, TRIAL_ACTION_EXTEND, TRIAL_ACTIONS } from '../constants';
export default {
name: 'ExtendReactivateTrialButton',
components: { GlButton, GlModal },
directives: {
GlModal: GlModalDirective,
},
props: {
namespaceId: {
type: Number,
required: true,
},
action: {
type: String,
required: true,
default: TRIAL_ACTION_EXTEND,
validator: (value) => TRIAL_ACTIONS.includes(value),
},
planName: {
type: String,
required: true,
},
},
data() {
return {
isLoading: false,
};
},
computed: {
i18nContext() {
return this.action === TRIAL_ACTION_EXTEND
? this.$options.i18n.extend
: this.$options.i18n.reactivate;
},
modalText() {
return sprintf(this.i18nContext.modalText, {
action: this.actionName,
planName: sprintf(this.$options.i18n.planName, { planName: this.planName }),
});
},
actionPrimary() {
return {
text: this.i18nContext.buttonText,
};
},
actionSecondary() {
return {
text: __('Cancel'),
};
},
},
methods: {
async submit() {
this.isLoading = true;
this.$refs.modal.hide();
const action = this.action === TRIAL_ACTION_EXTEND ? extendTrial : reactivateTrial;
await action(this.namespaceId)
.then(() => {
refreshCurrentPage();
})
.catch((error) => {
createFlash({
message: this.i18nContext.trialActionError,
captureError: true,
error,
});
})
.finally(() => {
this.isLoading = false;
});
},
},
i18n,
};
</script>
<template>
<div>
<gl-button v-gl-modal.extend-trial :loading="isLoading" category="primary" variant="info">
{{ i18nContext.buttonText }}
</gl-button>
<gl-modal
ref="modal"
modal-id="extend-trial"
:title="i18nContext.buttonText"
:action-primary="actionPrimary"
:action-secondary="actionSecondary"
data-testid="extend-reactivate-trial-modal"
@primary="submit"
>
{{ modalText }}
</gl-modal>
</div>
</template>
import { s__ } from '~/locale';
export const TRIAL_ACTION_EXTEND = 'extend';
export const TRIAL_ACTION_REACTIVATE = 'reactivate';
export const TRIAL_ACTIONS = [TRIAL_ACTION_EXTEND, TRIAL_ACTION_REACTIVATE];
export const i18n = Object.freeze({
planName: s__('Billings|%{planName} plan'),
extend: {
buttonText: s__('Billings|Extend trial'),
modalText: s__(
'Billings|By extending your trial, you will receive an additional 30 days of %{planName}. Your trial can be only extended once.',
),
trialActionError: s__('Billings|An error occurred while extending your trial.'),
},
reactivate: {
buttonText: s__('Billings|Reactivate trial'),
modalText: s__(
'Billings|By reactivating your trial, you will receive an additional 30 days of %{planName}. Your trial can be only reactivated once.',
),
trialActionError: s__('Billings|An error occurred while reactivating your trial.'),
},
});
export const shouldExtendReactivateTrialButtonMount = async () => {
const el = document.querySelector('.js-extend-reactivate-trial-button');
if (el) {
const { initExtendReactivateTrialButton } = await import(
/* webpackChunkName: 'init_extend_reactivate_trial_button' */ './init_extend_reactivate_trial_button'
);
initExtendReactivateTrialButton(el);
}
};
import Vue from 'vue';
import ExtendReactivateTrialButton from 'ee/trials/extend_reactivate_trial/components/extend_reactivate_trial_button.vue';
export const initExtendReactivateTrialButton = (el) => {
const { namespaceId, action, planName } = el.dataset;
return new Vue({
el,
render(createElement) {
return createElement(ExtendReactivateTrialButton, {
props: {
namespaceId: Number(namespaceId),
planName,
action,
},
});
},
});
};
......@@ -123,7 +123,6 @@ $badge-height: $gl-spacing-scale-7;
.card-wrapper {
margin-bottom: $gutter-small;
padding-top: $badge-height;
width: calc(50% - #{$gutter-small} / 2);
&-has-badge {
......
......@@ -69,7 +69,7 @@ module EE
{
namespace_id: namespace.id,
plan_name: namespace.actual_plan_name.titleize,
plan_name: ::Plan::ULTIMATE.titleize,
action: action
}
end
......
......@@ -9,4 +9,5 @@
- else
= render 'shared/billings/billing_plans', plans_data: @plans_data, namespace: @group, current_plan: current_plan
#js-billing-plans{ data: subscription_plan_data_attributes(@group, current_plan) }
- data_attributes = subscription_plan_data_attributes(@group, current_plan).merge(extend_reactivate_trial_button_data(@group))
#js-billing-plans{ data: data_attributes }
- page_title _('Billing')
- current_plan = subscription_plan_info(@plans_data, current_user.namespace.actual_plan_name)
- namespace = current_user.namespace
- current_plan = subscription_plan_info(@plans_data, namespace.actual_plan_name)
- data_attributes = subscription_plan_data_attributes(namespace, current_plan).merge(extend_reactivate_trial_button_data(namespace))
= render 'shared/billings/billing_plans', plans_data: @plans_data, namespace: current_user.namespace, current_plan: current_plan
#js-billing-plans{ data: subscription_plan_data_attributes(current_user.namespace, current_plan) }
#js-billing-plans{ data: data_attributes }
......@@ -4,4 +4,5 @@
#js-security-policies-list{ data: { assigned_policy_project: assigned_policy_project(project).to_json,
disable_security_policy_project: disable_security_policy_project.to_s,
documentation_path: help_page_path('user/project/clusters/protect/container_network_security/quick_start_guide'),
new_policy_path: new_project_threat_monitoring_policy_path(project),
project_path: project.full_path } }
......@@ -3,7 +3,7 @@
- if namespace_for_user
= render_if_exists 'trials/banner', namespace: namespace
.billing-plan-header.content-block.center.gl-mb-5
.billing-plan-header.content-block.center.gl-mb-3
.billing-plan-logo
- if namespace_for_user
.avatar-container.s96.home-panel-avatar.gl-mr-3.float-none.mx-auto.mb-4.mt-1
......@@ -34,3 +34,7 @@
- if show_start_free_trial_messages?(namespace)
- glm_content = namespace_for_user ? 'user-billing' : 'group-billing'
%p= link_to 'Start your free trial', new_trial_registration_path(glm_source: 'gitlab.com', glm_content: glm_content), class: 'btn btn-confirm gl-button', data: { qa_selector: 'start_your_free_trial' }
- if show_extend_reactivate_trial_button?(namespace)
.gl-mt-3
.js-extend-reactivate-trial-button.gl-mt-3{ data: extend_reactivate_trial_button_data(namespace) }
......@@ -20,12 +20,22 @@ FactoryBot.define do
trial_ends_on { Date.current.advance(days: 15) }
end
trait :extended_trial do
active_trial
trial_extension_type { GitlabSubscription.trial_extension_types[:extended] }
end
trait :expired_trial do
trial { true }
trial_starts_on { Date.current.advance(days: -31) }
trial_ends_on { Date.current.advance(days: -1) }
end
trait :reactivated_trial do
expired_trial
trial_extension_type { GitlabSubscription.trial_extension_types[:reactivated] }
end
trait :default do
association :hosted_plan, factory: :default_plan
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Billings > Extend / Reactivate Trial', :js do
include SubscriptionPortalHelpers
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:plan) { create(:free_plan) }
let_it_be(:plans_data) do
Gitlab::Json.parse(File.read(Rails.root.join('ee/spec/fixtures/gitlab_com_plans.json'))).map do |data|
data.deep_symbolize_keys
end
end
let(:initial_trial_end_date) { Date.current }
let(:extended_or_reactivated_trial_end_date) { initial_trial_end_date + 30.days }
before do
group.add_owner(user)
allow(Gitlab).to receive(:com?).and_return(true)
stub_ee_application_setting(should_check_namespace_plan: true)
stub_feature_flags(allow_extend_reactivate_trial: true)
stub_full_request("#{EE::SUBSCRIPTIONS_URL}/gitlab_plans?plan=#{plan.name}&namespace_id=#{group.id}")
.to_return(status: 200, body: plans_data.to_json)
stub_full_request("#{EE::SUBSCRIPTIONS_URL}/trials/extend_reactivate_trial", method: :put)
.to_return(status: 200)
sign_in(user)
end
shared_examples 'a non-reactivatable trial' do
before do
visit group_billings_path(group)
end
it 'does not display the "Reactivate trial" button' do
expect(page).not_to have_button('Reactivate trial')
end
end
shared_examples 'a non-extendable trial' do
before do
visit group_billings_path(group)
end
it 'does not display the "Extend trial" button' do
expect(page).not_to have_button('Extend trial')
end
end
shared_examples 'a reactivatable trial' do
before do
allow_next_instance_of(GitlabSubscriptions::ExtendReactivateTrialService) do |service|
group.gitlab_subscription.update!(trial_extension_type: GitlabSubscription.trial_extension_types[:reactivated],
end_date: extended_or_reactivated_trial_end_date,
trial_ends_on: extended_or_reactivated_trial_end_date)
end
visit group_billings_path(group)
end
it 'reactivates trial' do
expect(page).to have_content("trial expired on #{initial_trial_end_date}")
within '.billing-plan-header' do
click_button('Reactivate trial')
end
within '[data-testid="extend-reactivate-trial-modal"]' do
click_button('Reactivate trial')
end
wait_for_requests
expect(page).to have_content("trial will expire after #{extended_or_reactivated_trial_end_date}")
expect(page).not_to have_button('Reactivate trial')
end
end
shared_examples 'an extendable trial' do
before do
allow_next_instance_of(GitlabSubscriptions::ExtendReactivateTrialService) do |service|
group.gitlab_subscription.update!(trial_extension_type: GitlabSubscription.trial_extension_types[:extended],
end_date: initial_trial_end_date,
trial_ends_on: extended_or_reactivated_trial_end_date)
end
visit group_billings_path(group)
end
it 'extends the trial' do
expect(page).to have_content("trial will expire after #{initial_trial_end_date}")
within '.billing-plan-header' do
click_button('Extend trial')
end
within '[data-testid="extend-reactivate-trial-modal"]' do
click_button('Extend trial')
end
wait_for_requests
expect(page).to have_content("trial will expire after #{extended_or_reactivated_trial_end_date}")
expect(page).not_to have_button('Extend trial')
end
end
context 'with paid subscription' do
context 'when expired' do
let_it_be(:subscription) { create(:gitlab_subscription, :expired, hosted_plan: plan, namespace: group) }
it_behaves_like 'a non-reactivatable trial'
it_behaves_like 'a non-extendable trial'
context 'when the feature flag is disabled' do
before do
stub_feature_flags(allow_extend_reactivate_trial: false)
end
it_behaves_like 'a non-reactivatable trial'
it_behaves_like 'a non-extendable trial'
end
end
context 'when not expired' do
let_it_be(:subscription) { create(:gitlab_subscription, hosted_plan: plan, namespace: group) }
it_behaves_like 'a non-reactivatable trial'
it_behaves_like 'a non-extendable trial'
end
end
context 'without a subscription' do
it_behaves_like 'a non-reactivatable trial'
it_behaves_like 'a non-extendable trial'
end
context 'with active trial near the expiration date' do
let(:initial_trial_end_date) { Date.tomorrow }
let_it_be(:subscription) { create(:gitlab_subscription, :active_trial, trial_ends_on: Date.tomorrow, hosted_plan: plan, namespace: group) }
it_behaves_like 'an extendable trial'
it_behaves_like 'a non-reactivatable trial'
end
context 'with extended trial' do
let_it_be(:subscription) { create(:gitlab_subscription, :extended_trial, hosted_plan: plan, namespace: group) }
it_behaves_like 'a non-extendable trial'
it_behaves_like 'a non-reactivatable trial'
end
context 'with reactivated trial' do
let_it_be(:subscription) { create(:gitlab_subscription, :reactivated_trial, hosted_plan: plan, namespace: group) }
it_behaves_like 'a non-extendable trial'
it_behaves_like 'a non-reactivatable trial'
end
context 'with expired trial' do
let(:initial_trial_end_date) { Date.current.advance(days: -1) }
let_it_be(:subscription) { create(:gitlab_subscription, :expired_trial, hosted_plan: plan, namespace: group) }
it_behaves_like 'a reactivatable trial'
it_behaves_like 'a non-extendable trial'
end
end
......@@ -6,6 +6,7 @@ import SubscriptionTable from 'ee/billings/subscriptions/components/subscription
import SubscriptionTableRow from 'ee/billings/subscriptions/components/subscription_table_row.vue';
import initialStore from 'ee/billings/subscriptions/store';
import * as types from 'ee/billings/subscriptions/store/mutation_types';
import ExtendReactivateTrialButton from 'ee/trials/extend_reactivate_trial/components/extend_reactivate_trial_button.vue';
import { mockDataSubscription } from 'ee_jest/billings/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
......@@ -361,4 +362,33 @@ describe('SubscriptionTable component', () => {
expect(findRefreshSeatsButton().exists()).toBe(false);
});
});
describe.each`
availableTrialAction | buttonVisible
${null} | ${false}
${'extend'} | ${true}
${'reactivate'} | ${true}
`(
'with availableTrialAction=$availableTrialAction',
({ availableTrialAction, buttonVisible }) => {
beforeEach(() => {
createComponentWithStore({
provide: {
namespaceId: 1,
availableTrialAction,
},
});
});
if (buttonVisible) {
it('renders the trial button', () => {
expect(wrapper.findComponent(ExtendReactivateTrialButton).isVisible()).toBe(true);
});
} else {
it('does not render the trial button', () => {
expect(wrapper.findComponent(ExtendReactivateTrialButton).exists()).toBe(false);
});
}
},
);
});
import PoliciesApp from 'ee/threat_monitoring/components/policies/policies_app.vue';
import PoliciesHeader from 'ee/threat_monitoring/components/policies/policies_header.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('Policies App', () => {
let wrapper;
const findPoliciesHeader = () => wrapper.findComponent(PoliciesHeader);
beforeEach(() => {
wrapper = shallowMountExtended(PoliciesApp);
});
afterEach(() => {
wrapper.destroy();
});
it('mounts the policies header component', () => {
expect(findPoliciesHeader().exists()).toBe(true);
});
});
import { GlAlert, GlButton, GlSprintf } from '@gitlab/ui';
import PoliciesHeader from 'ee/threat_monitoring/components/policies/policies_header.vue';
import ScanNewPolicyModal from 'ee/threat_monitoring/components/policies/scan_new_policy_modal.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('Policies Header Component', () => {
let wrapper;
const documentationPath = '/path/to/docs';
const newPolicyPath = '/path/to/new/policy/page';
const findAlert = () => wrapper.findComponent(GlAlert);
const findScanNewPolicyModal = () => wrapper.findComponent(ScanNewPolicyModal);
const findHeader = () => wrapper.findByRole('heading');
const findMoreInformationLink = () => wrapper.findComponent(GlButton);
const findEditPolicyProjectButton = () => wrapper.findByTestId('edit-project-policy-button');
const findNewPolicyButton = () => wrapper.findByTestId('new-policy-button');
const findSubheader = () => wrapper.findByTestId('policies-subheader');
const createWrapper = ({ provide } = {}) => {
wrapper = shallowMountExtended(PoliciesHeader, {
provide: {
documentationPath,
newPolicyPath,
assignedPolicyProject: null,
...provide,
},
stubs: {
GlSprintf,
GlButton,
},
});
};
afterEach(() => {
wrapper.destroy();
});
beforeEach(() => {
createWrapper();
});
it('displays New policy button with correct text and link', () => {
expect(findNewPolicyButton().text()).toBe('New policy');
expect(findNewPolicyButton().attributes('href')).toBe(newPolicyPath);
});
it('displays the Edit policy project button', () => {
expect(findEditPolicyProjectButton().text()).toBe('Edit policy project');
});
it('does not display the alert component by default', () => {
expect(findAlert().exists()).toBe(false);
});
it('displays the alert component when scan new modal policy emits events', async () => {
const text = 'Project was linked successfully.';
findScanNewPolicyModal().vm.$emit('project-updated', {
text,
variant: 'success',
});
// When the project is updated it displays the output message.
await wrapper.vm.$nextTick();
expect(findAlert().text()).toBe(text);
// When the project is being updated once again, it removes the alert so that
// the new one will be displayed.
findScanNewPolicyModal().vm.$emit('updating-project');
await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(false);
});
it('mounts the scan new policy modal', () => {
expect(findScanNewPolicyModal().exists()).toBe(true);
});
it('displays scan new policy modal when the action button is clicked', async () => {
await findEditPolicyProjectButton().trigger('click');
expect(findScanNewPolicyModal().props().visible).toBe(true);
});
it('displays the header', () => {
expect(findHeader().text()).toBe('Policies');
});
it('displays the subheader', () => {
expect(findSubheader().text()).toContain('Enforce security for this project.');
expect(findMoreInformationLink().attributes('href')).toBe(documentationPath);
});
});
import { GlDropdown } from '@gitlab/ui';
import { GlDropdown, GlModal, GlAlert } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import InstanceProjectSelector from 'ee/threat_monitoring/components/instance_project_selector.vue';
import SecurityPolicyProjectSelector from 'ee/threat_monitoring/components/security_policy_project_selector.vue';
import ScanNewPolicyModal from 'ee/threat_monitoring/components/policies/scan_new_policy_modal.vue';
import assignSecurityPolicyProject from 'ee/threat_monitoring/graphql/mutations/assign_security_policy_project.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
apolloFailureResponse,
mockAssignSecurityPolicyProjectResponses,
} from '../mocks/mock_apollo';
import { stubComponent } from 'helpers/stub_component';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { mockAssignSecurityPolicyProjectResponses } from '../../mocks/mock_apollo';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('SecurityPolicyProjectSelector Component', () => {
describe('ScanNewPolicyModal Component', () => {
let wrapper;
let projectUpdatedListener;
const findSaveButton = () => wrapper.findByTestId('save-policy-project');
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findErrorAlert = () => wrapper.findByTestId('policy-project-assign-error');
const findInstanceProjectSelector = () => wrapper.findComponent(InstanceProjectSelector);
const findSuccessAlert = () => wrapper.findByTestId('policy-project-assign-success');
const findTooltip = () => wrapper.findByTestId('disabled-button-tooltip');
const findAlert = () => wrapper.findComponent(GlAlert);
const findModal = () => wrapper.findComponent(GlModal);
const selectProject = async () => {
findInstanceProjectSelector().vm.$emit('projectClicked', {
const selectProject = async (
project = {
id: 'gid://gitlab/Project/1',
name: 'Test 1',
});
},
) => {
findInstanceProjectSelector().vm.$emit('projectClicked', project);
await wrapper.vm.$nextTick();
findModal().vm.$emit('ok');
await wrapper.vm.$nextTick();
findSaveButton().vm.$emit('click');
await waitForPromises();
};
const createWrapper = ({
mount = shallowMountExtended,
mutationResult = mockAssignSecurityPolicyProjectResponses.success,
propsData = {},
provide = {},
} = {}) => {
wrapper = mount(SecurityPolicyProjectSelector, {
wrapper = mountExtended(ScanNewPolicyModal, {
localVue,
apolloProvider: createMockApollo([[assignSecurityPolicyProject, mutationResult]]),
directives: {
GlTooltip: createMockDirective(),
stubs: {
GlModal: stubComponent(GlModal, {
template:
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
}),
},
propsData,
provide: {
disableSecurityPolicyProject: false,
documentationPath: 'test/path/index.md',
projectPath: 'path/to/project',
assignedPolicyProject: null,
...provide,
},
});
projectUpdatedListener = jest.fn();
wrapper.vm.$on('project-updated', projectUpdatedListener);
};
const createWrapperAndSelectProject = async (data) => {
createWrapper(data);
await selectProject();
};
afterEach(() => {
......@@ -67,72 +73,69 @@ describe('SecurityPolicyProjectSelector Component', () => {
createWrapper();
});
it.each`
findComponent | state | title
${findDropdown} | ${true} | ${'does display the dropdown'}
${findInstanceProjectSelector} | ${true} | ${'does display the project selector'}
${findErrorAlert} | ${false} | ${'does not display the error alert'}
${findSuccessAlert} | ${false} | ${'does not display the success alert'}
`('$title', ({ findComponent, state }) => {
expect(findComponent().exists()).toBe(state);
});
it('passes down correct properties/attributes to the gl-modal component', () => {
expect(findModal().props()).toMatchObject({
modalId: 'scan-new-policy',
size: 'sm',
visible: false,
title: 'Select security project',
});
it('renders the "Save Changes" button', () => {
const button = findSaveButton();
expect(button.exists()).toBe(true);
expect(button.attributes('disabled')).toBe('true');
expect(findModal().attributes()).toEqual({
'ok-disabled': 'true',
'ok-title': 'Save',
'cancel-variant': 'light',
});
});
it('does not display a tooltip', () => {
const tooltip = getBinding(findTooltip().element, 'gl-tooltip');
expect(tooltip.value.disabled).toBe(true);
it('does not display a warning', () => {
expect(findAlert().exists()).toBe(false);
});
});
it('emits close event when gl-modal emits change event', () => {
createWrapper();
findModal().vm.$emit('change');
expect(wrapper.emitted('close')).toEqual([[]]);
});
describe('project selection', () => {
it('enables the "Save Changes" button if a new project is selected', async () => {
it('enables the "Save" button only if a new project is selected', async () => {
createWrapper({
mount: mountExtended,
propsData: { assignedPolicyProject: { id: 'gid://gitlab/Project/0', name: 'Test 0' } },
provide: { assignedPolicyProject: { id: 'gid://gitlab/Project/0', name: 'Test 0' } },
});
const button = findSaveButton();
expect(button.attributes('disabled')).toBe('disabled');
expect(findModal().attributes('ok-disabled')).toBe('true');
findInstanceProjectSelector().vm.$emit('projectClicked', {
id: 'gid://gitlab/Project/1',
name: 'Test 1',
});
await wrapper.vm.$nextTick();
expect(button.attributes('disabled')).toBe(undefined);
expect(findModal().attributes('ok-disabled')).toBeUndefined();
});
it('displays an alert if the security policy project selection succeeds', async () => {
createWrapper({ mount: mountExtended });
expect(findErrorAlert().exists()).toBe(false);
expect(findSuccessAlert().exists()).toBe(false);
await selectProject();
expect(findErrorAlert().exists()).toBe(false);
expect(findSuccessAlert().exists()).toBe(true);
it('emits an event with success message', async () => {
await createWrapperAndSelectProject();
expect(projectUpdatedListener).toHaveBeenCalledWith({
text: 'Security policy project was linked successfully',
variant: 'success',
});
});
it('shows an alert if the security policy project selection fails', async () => {
createWrapper({
mount: mountExtended,
it('emits an event with an error message', async () => {
await createWrapperAndSelectProject({
mutationResult: mockAssignSecurityPolicyProjectResponses.failure,
});
expect(findErrorAlert().exists()).toBe(false);
expect(findSuccessAlert().exists()).toBe(false);
await selectProject();
expect(findErrorAlert().exists()).toBe(true);
expect(findSuccessAlert().exists()).toBe(false);
});
it('shows an alert if GraphQL fails', async () => {
createWrapper({ mount: mountExtended, mutationResult: apolloFailureResponse });
expect(findErrorAlert().exists()).toBe(false);
expect(findSuccessAlert().exists()).toBe(false);
await selectProject();
expect(findErrorAlert().exists()).toBe(true);
expect(findSuccessAlert().exists()).toBe(false);
expect(projectUpdatedListener).toHaveBeenCalledWith({
text: 'An error occurred assigning your security policy project',
variant: 'danger',
});
});
});
......@@ -142,12 +145,11 @@ describe('SecurityPolicyProjectSelector Component', () => {
});
it('disables the dropdown', () => {
expect(findDropdown().attributes('disabled')).toBe('true');
expect(findDropdown().props('disabled')).toBe(true);
});
it('displays a tooltip', () => {
const tooltip = getBinding(findTooltip().element, 'gl-tooltip');
expect(tooltip.value.disabled).toBe(false);
it('displays a warning', () => {
expect(findAlert().text()).toBe('Only owners can update Security Policy Project');
});
});
});
import { GlButton, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ExtendReactivateTrialButton from 'ee/trials/extend_reactivate_trial/components/extend_reactivate_trial_button.vue';
import {
i18n,
TRIAL_ACTION_EXTEND,
TRIAL_ACTION_REACTIVATE,
} from 'ee/trials/extend_reactivate_trial/constants';
import { sprintf } from '~/locale';
describe('ExtendReactivateTrialButton', () => {
let wrapper;
const createComponent = (props = {}) => {
return shallowMount(ExtendReactivateTrialButton, {
propsData: {
...props,
},
});
};
const findButton = () => wrapper.findComponent(GlButton);
const findModal = () => wrapper.findComponent(GlModal);
afterEach(() => {
wrapper.destroy();
});
describe('rendering', () => {
beforeEach(() => {
wrapper = createComponent({
namespaceId: 1,
action: TRIAL_ACTION_EXTEND,
planName: 'Ultimate',
});
});
it('does not have loading icon', () => {
expect(findButton().props('loading')).toBe(false);
});
});
describe('when extending trial', () => {
beforeEach(() => {
wrapper = createComponent({
namespaceId: 1,
action: TRIAL_ACTION_EXTEND,
planName: 'Ultimate',
});
});
it('has the "Extend trial" text on the button', () => {
expect(findButton().text()).toBe(i18n.extend.buttonText);
});
it('has the correct text in the modal', () => {
expect(findModal().text()).toBe(
sprintf(i18n.extend.modalText, { planName: 'Ultimate plan' }),
);
});
});
describe('when reactivating trial', () => {
beforeEach(() => {
wrapper = createComponent({
namespaceId: 1,
action: TRIAL_ACTION_REACTIVATE,
planName: 'Ultimate',
});
});
it('has the "Reactivate trial" text on the button', () => {
expect(findButton().text()).toBe(i18n.reactivate.buttonText);
});
it('has the correct text in the modal', () => {
expect(findModal().text()).toBe(
sprintf(i18n.reactivate.modalText, { planName: 'Ultimate plan' }),
);
});
});
});
......@@ -197,24 +197,20 @@ RSpec.describe EE::TrialHelper do
it { is_expected.to be_falsey }
end
context 'when feature flag is enabled' do
where(:can_extend_trial, :can_reactivate_trial, :result) do
false | false | false
true | false | true
false | true | true
true | true | true
end
with_them do
before do
stub_feature_flags(allow_extend_reactivate_trial: true)
allow(namespace).to receive(:can_extend_trial?).and_return(can_extend_trial)
allow(namespace).to receive(:can_reactivate_trial?).and_return(can_reactivate_trial)
end
where(:can_extend_trial, :can_reactivate_trial, :result) do
false | false | false
true | false | true
false | true | true
true | true | true
end
it { is_expected.to eq(result) }
with_them do
before do
allow(namespace).to receive(:can_extend_trial?).and_return(can_extend_trial)
allow(namespace).to receive(:can_reactivate_trial?).and_return(can_reactivate_trial)
end
it { is_expected.to eq(result) }
end
end
......@@ -253,22 +249,20 @@ RSpec.describe EE::TrialHelper do
end
end
context 'when feature flag is enabled' do
context 'when trial can be extended' do
before do
allow(namespace).to receive(:can_extend_trial?).and_return(true)
end
it { is_expected.to eq({ namespace_id: 1, plan_name: 'Ultimate', action: 'extend' }) }
context 'when trial can be extended' do
before do
allow(namespace).to receive(:can_extend_trial?).and_return(true)
end
context 'when trial can be reactivated' do
before do
allow(namespace).to receive(:can_reactivate_trial?).and_return(true)
end
it { is_expected.to eq({ namespace_id: 1, plan_name: 'Ultimate', action: 'extend' }) }
end
it { is_expected.to eq({ namespace_id: 1, plan_name: 'Ultimate', action: 'reactivate' }) }
context 'when trial can be reactivated' do
before do
allow(namespace).to receive(:can_reactivate_trial?).and_return(true)
end
it { is_expected.to eq({ namespace_id: 1, plan_name: 'Ultimate', action: 'reactivate' }) }
end
end
end
......@@ -1261,22 +1261,20 @@ RSpec.describe Namespace do
it { is_expected.to be_falsey }
end
context 'when feature flag is enabled' do
where(:trial_active, :trial_extended_or_reactivated, :can_extend_trial) do
false | false | false
false | true | false
true | false | true
true | true | false
end
with_them do
before do
allow(namespace).to receive(:trial_active?).and_return(trial_active)
allow(namespace).to receive(:trial_extended_or_reactivated?).and_return(trial_extended_or_reactivated)
end
where(:trial_active, :trial_extended_or_reactivated, :can_extend_trial) do
false | false | false
false | true | false
true | false | true
true | true | false
end
it { is_expected.to be can_extend_trial }
with_them do
before do
allow(namespace).to receive(:trial_active?).and_return(trial_active)
allow(namespace).to receive(:trial_extended_or_reactivated?).and_return(trial_extended_or_reactivated)
end
it { is_expected.to be can_extend_trial }
end
end
......@@ -1296,36 +1294,34 @@ RSpec.describe Namespace do
it { is_expected.to be_falsey }
end
context 'when feature flag is enabled' do
where(:trial_active, :never_had_trial, :trial_extended_or_reactivated, :free_plan, :can_reactivate_trial) do
false | false | false | false | false
false | false | false | true | true
false | false | true | false | false
false | false | true | true | false
false | true | false | false | false
false | true | false | true | false
false | true | true | false | false
false | true | true | true | false
true | false | false | false | false
true | false | false | true | false
true | false | true | false | false
true | false | true | true | false
true | true | false | false | false
true | true | false | true | false
true | true | true | false | false
true | true | true | true | false
end
with_them do
before do
allow(namespace).to receive(:trial_active?).and_return(trial_active)
allow(namespace).to receive(:never_had_trial?).and_return(never_had_trial)
allow(namespace).to receive(:trial_extended_or_reactivated?).and_return(trial_extended_or_reactivated)
allow(namespace).to receive(:free_plan?).and_return(free_plan)
end
where(:trial_active, :never_had_trial, :trial_extended_or_reactivated, :free_plan, :can_reactivate_trial) do
false | false | false | false | false
false | false | false | true | true
false | false | true | false | false
false | false | true | true | false
false | true | false | false | false
false | true | false | true | false
false | true | true | false | false
false | true | true | true | false
true | false | false | false | false
true | false | false | true | false
true | false | true | false | false
true | false | true | true | false
true | true | false | false | false
true | true | false | true | false
true | true | true | false | false
true | true | true | true | false
end
it { is_expected.to be can_reactivate_trial }
with_them do
before do
allow(namespace).to receive(:trial_active?).and_return(trial_active)
allow(namespace).to receive(:never_had_trial?).and_return(never_had_trial)
allow(namespace).to receive(:trial_extended_or_reactivated?).and_return(trial_extended_or_reactivated)
allow(namespace).to receive(:free_plan?).and_return(free_plan)
end
it { is_expected.to be can_reactivate_trial }
end
end
......
......@@ -5178,6 +5178,27 @@ msgstr ""
msgid "BillingPlan|Upgrade for free"
msgstr ""
msgid "Billings|%{planName} plan"
msgstr ""
msgid "Billings|An error occurred while extending your trial."
msgstr ""
msgid "Billings|An error occurred while reactivating your trial."
msgstr ""
msgid "Billings|By extending your trial, you will receive an additional 30 days of %{planName}. Your trial can be only extended once."
msgstr ""
msgid "Billings|By reactivating your trial, you will receive an additional 30 days of %{planName}. Your trial can be only reactivated once."
msgstr ""
msgid "Billings|Extend trial"
msgstr ""
msgid "Billings|Reactivate trial"
msgstr ""
msgid "Billings|Shared runners cannot be enabled until a valid credit card is on file."
msgstr ""
......@@ -21819,6 +21840,12 @@ msgstr ""
msgid "NetworkPolicies|Edit policy"
msgstr ""
msgid "NetworkPolicies|Edit policy project"
msgstr ""
msgid "NetworkPolicies|Enforce security for this project. %{linkStart}More information.%{linkEnd}"
msgstr ""
msgid "NetworkPolicies|Enforcement status"
msgstr ""
......@@ -21861,6 +21888,9 @@ msgstr ""
msgid "NetworkPolicies|Please %{installLinkStart}install%{installLinkEnd} and %{configureLinkStart}configure a Kubernetes Agent for this project%{configureLinkEnd} to enable alerts."
msgstr ""
msgid "NetworkPolicies|Policies"
msgstr ""
msgid "NetworkPolicies|Policies are a specification of how groups of pods are allowed to communicate with each other's network endpoints."
msgstr ""
......@@ -29313,22 +29343,19 @@ msgstr ""
msgid "SecurityConfiguration|You can quickly enable all security scanning tools by enabling %{linkStart}Auto DevOps%{linkEnd}."
msgstr ""
msgid "SecurityOrchestration|A security policy project can enforce policies for a given project, group, or instance. With a security policy project, you can specify security policies that are important to you and enforce them with every commit. %{linkStart}More information.%{linkEnd}"
msgstr ""
msgid "SecurityOrchestration|An error occurred assigning your security policy project"
msgstr ""
msgid "SecurityOrchestration|Create a policy"
msgid "SecurityOrchestration|Only owners can update Security Policy Project"
msgstr ""
msgid "SecurityOrchestration|Only owners can update Security Policy Project"
msgid "SecurityOrchestration|Security policy project was linked successfully"
msgstr ""
msgid "SecurityOrchestration|Security policy project"
msgid "SecurityOrchestration|Select a project to store your security policies in. %{linkStart}More information.%{linkEnd}"
msgstr ""
msgid "SecurityOrchestration|Security policy project was linked successfully"
msgid "SecurityOrchestration|Select security project"
msgstr ""
msgid "SecurityPolicies|+%{count} more"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment