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, ...@@ -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 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). [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 ## 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). 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: ...@@ -47,8 +52,9 @@ NOTE:
|If the feature is... | Use this text | |If the feature is... | Use this text |
|-|-| |-|-|
|Available| `On GitLab SaaS, this feature is available.` | |Available| `On GitLab.com, this feature is available.` |
|Unavailable| `On GitLab SaaS, this feature is not 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 ### Optional information
...@@ -63,7 +69,8 @@ When the state of a flag changes (for example, disabled by default to enabled by ...@@ -63,7 +69,8 @@ When the state of a flag changes (for example, disabled by default to enabled by
Possible version history entries are: Possible version history entries are:
```markdown ```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. > - [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. > - [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: ...@@ -96,7 +103,7 @@ And, when the feature is done and fully available to all users:
```markdown ```markdown
> - Introduced in GitLab 13.7. > - 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. > - [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. > - [Feature flag `forti_token_cloud`](https://gitlab.com/issue/etc) removed in GitLab X.X.
``` ```
...@@ -1110,8 +1110,9 @@ Prerequisites: ...@@ -1110,8 +1110,9 @@ Prerequisites:
To validate a site profile: To validate a site profile:
1. From your project's home page, go to **Security & Compliance > Configuration**. 1. On the top bar, select **Menu > Projects** and find your project.
1. In the **DAST Profiles** row select **Manage**. 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. Select the **Site Profiles** tab.
1. In the profile's row select **Validate** or **Retry validation**. 1. In the profile's row select **Validate** or **Retry validation**.
1. Select the validation method. 1. Select the validation method.
......
...@@ -2,6 +2,12 @@ import { buildApiUrl } from '~/api/api_utils'; ...@@ -2,6 +2,12 @@ import { buildApiUrl } from '~/api/api_utils';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
const SUBSCRIPTIONS_PATH = '/api/:version/subscriptions'; 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) { export function createSubscription(groupId, customer, subscription) {
const url = buildApiUrl(SUBSCRIPTIONS_PATH); const url = buildApiUrl(SUBSCRIPTIONS_PATH);
...@@ -13,3 +19,25 @@ export function createSubscription(groupId, customer, subscription) { ...@@ -13,3 +19,25 @@ export function createSubscription(groupId, customer, subscription) {
return axios.post(url, { params }); 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 { ...@@ -9,7 +9,7 @@ export default {
}, },
inject: { inject: {
namespaceId: { namespaceId: {
default: '', default: null,
}, },
}, },
created() { created() {
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
DAYS_FOR_RENEWAL, DAYS_FOR_RENEWAL,
PLAN_TITLE_TRIAL_TEXT, PLAN_TITLE_TRIAL_TEXT,
} from 'ee/billings/constants'; } from 'ee/billings/constants';
import ExtendReactivateTrialButton from 'ee/trials/extend_reactivate_trial/components/extend_reactivate_trial_button.vue';
import createFlash from '~/flash'; import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { getDayDifference } from '~/lib/utils/datetime/date_calculation_utility'; import { getDayDifference } from '~/lib/utils/datetime/date_calculation_utility';
...@@ -24,6 +25,7 @@ export default { ...@@ -24,6 +25,7 @@ export default {
GlButton, GlButton,
GlLoadingIcon, GlLoadingIcon,
SubscriptionTableRow, SubscriptionTableRow,
ExtendReactivateTrialButton,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
inject: { inject: {
...@@ -34,7 +36,7 @@ export default { ...@@ -34,7 +36,7 @@ export default {
default: '', default: '',
}, },
namespaceId: { namespaceId: {
default: '', default: null,
}, },
customerPortalUrl: { customerPortalUrl: {
default: '', default: '',
...@@ -54,6 +56,9 @@ export default { ...@@ -54,6 +56,9 @@ export default {
refreshSeatsHref: { refreshSeatsHref: {
default: '', default: '',
}, },
availableTrialAction: {
default: null,
},
}, },
computed: { computed: {
...mapState([ ...mapState([
...@@ -182,7 +187,14 @@ export default { ...@@ -182,7 +187,14 @@ export default {
data-testid="subscription-header" data-testid="subscription-header"
> >
<strong>{{ subscriptionHeader }}</strong> <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 <gl-button
v-for="(button, index) in buttons" v-for="(button, index) in buttons"
:key="button.text" :key="button.text"
......
...@@ -24,13 +24,14 @@ export default (containerId = 'js-billing-plans') => { ...@@ -24,13 +24,14 @@ export default (containerId = 'js-billing-plans') => {
planName, planName,
freePersonalNamespace, freePersonalNamespace,
refreshSeatsHref, refreshSeatsHref,
action,
} = containerEl.dataset; } = containerEl.dataset;
return new Vue({ return new Vue({
el: containerEl, el: containerEl,
store: new Vuex.Store(initialStore()), store: new Vuex.Store(initialStore()),
provide: { provide: {
namespaceId, namespaceId: Number(namespaceId),
namespaceName, namespaceName,
addSeatsHref, addSeatsHref,
planUpgradeHref, planUpgradeHref,
...@@ -40,6 +41,7 @@ export default (containerId = 'js-billing-plans') => { ...@@ -40,6 +41,7 @@ export default (containerId = 'js-billing-plans') => {
planName, planName,
freePersonalNamespace: parseBoolean(freePersonalNamespace), freePersonalNamespace: parseBoolean(freePersonalNamespace),
refreshSeatsHref, refreshSeatsHref,
availableTrialAction: action,
}, },
render(createElement) { render(createElement) {
return createElement(SubscriptionApp); return createElement(SubscriptionApp);
......
import { shouldQrtlyReconciliationMount } from 'ee/billings/qrtly_reconciliation'; import { shouldQrtlyReconciliationMount } from 'ee/billings/qrtly_reconciliation';
import initSubscriptions from 'ee/billings/subscriptions'; import initSubscriptions from 'ee/billings/subscriptions';
import { shouldExtendReactivateTrialButtonMount } from 'ee/trials/extend_reactivate_trial';
import PersistentUserCallout from '~/persistent_user_callout'; import PersistentUserCallout from '~/persistent_user_callout';
PersistentUserCallout.factory(document.querySelector('.js-gold-trial-callout')); PersistentUserCallout.factory(document.querySelector('.js-gold-trial-callout'));
initSubscriptions(); initSubscriptions();
shouldExtendReactivateTrialButtonMount();
shouldQrtlyReconciliationMount(); shouldQrtlyReconciliationMount();
import initSubscriptions from 'ee/billings/subscriptions'; import initSubscriptions from 'ee/billings/subscriptions';
import { shouldExtendReactivateTrialButtonMount } from 'ee/trials/extend_reactivate_trial';
import PersistentUserCallout from '~/persistent_user_callout'; import PersistentUserCallout from '~/persistent_user_callout';
PersistentUserCallout.factory(document.querySelector('.js-gold-trial-callout')); PersistentUserCallout.factory(document.querySelector('.js-gold-trial-callout'));
shouldExtendReactivateTrialButtonMount();
initSubscriptions(); 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> <script>
import { GlAlert, GlButton, GlDropdown, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; import { GlButton, GlDropdown, GlSprintf, GlAlert, GlModal } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__, __ } from '~/locale';
import assignSecurityPolicyProject from '../graphql/mutations/assign_security_policy_project.mutation.graphql'; import assignSecurityPolicyProject from '../../graphql/mutations/assign_security_policy_project.mutation.graphql';
import InstanceProjectSelector from './instance_project_selector.vue'; import InstanceProjectSelector from '../instance_project_selector.vue';
export default { export default {
PROJECT_SELECTOR_HEIGHT: 204, PROJECT_SELECTOR_HEIGHT: 204,
i18n: { i18n: {
assignError: s__( modal: {
'SecurityOrchestration|An error occurred assigning your security policy project', okTitle: __('Save'),
), header: s__('SecurityOrchestration|Select security project'),
assignSuccess: s__('SecurityOrchestration|Security policy project was linked successfully'), },
disabledButtonTooltip: s__( save: {
'SecurityOrchestration|Only owners can update Security Policy Project', ok: s__('SecurityOrchestration|Security policy project was linked successfully'),
), error: s__('SecurityOrchestration|An error occurred assigning your 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}', 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: { components: {
GlAlert,
GlButton, GlButton,
GlDropdown, GlDropdown,
GlSprintf, GlSprintf,
GlModal,
GlAlert,
InstanceProjectSelector, InstanceProjectSelector,
}, },
directives: { inject: [
GlTooltip: GlTooltipDirective, 'disableSecurityPolicyProject',
}, 'documentationPath',
inject: ['disableSecurityPolicyProject', 'documentationPath', 'projectPath'], 'projectPath',
'assignedPolicyProject',
],
props: { props: {
assignedPolicyProject: { visible: {
type: Object, type: Boolean,
required: false, required: false,
default: () => { default: false,
return { id: '', name: '' };
},
}, },
}, },
data() { data() {
return { return {
currentProjectId: this.assignedPolicyProject.id, selectedProject: { ...this.assignedPolicyProject },
selectedProject: this.assignedPolicyProject, hasSelectedNewProject: false,
isAssigningProject: false,
showAssignError: false,
showAssignSuccess: false,
}; };
}, },
computed: { computed: {
hasSelectedNewProject() { selectedProjectId() {
return this.currentProjectId !== this.selectedProject.id; return this.selectedProject?.id || '';
},
selectedProjectName() {
return this.selectedProject?.name || '';
},
isModalOkButtonDisabled() {
return this.disableSecurityPolicyProject || !this.hasSelectedNewProject;
}, },
}, },
methods: { methods: {
dismissAlert(type) {
this[type] = false;
},
async saveChanges() { async saveChanges() {
this.isAssigningProject = true; this.$emit('updating-project');
this.showAssignError = false;
this.showAssignSuccess = false;
const { id } = this.selectedProject;
try { try {
const { data } = await this.$apollo.mutate({ const { data } = await this.$apollo.mutate({
mutation: assignSecurityPolicyProject, mutation: assignSecurityPolicyProject,
variables: { variables: {
input: { input: {
projectPath: this.projectPath, projectPath: this.projectPath,
securityPolicyProjectId: id, securityPolicyProjectId: this.selectedProjectId,
}, },
}, },
}); });
if (data?.securityPolicyProjectAssign?.errors?.length) { if (data?.securityPolicyProjectAssign?.errors?.length) {
this.showAssignError = true; throw new Error(data.securityPolicyProjectAssign.errors);
} else {
this.showAssignSuccess = true;
this.currentProjectId = id;
} }
this.$emit('project-updated', { text: this.$options.i18n.save.ok, variant: 'success' });
} catch { } catch {
this.showAssignError = true; this.$emit('project-updated', { text: this.$options.i18n.save.error, variant: 'danger' });
} finally { } finally {
this.isAssigningProject = false; this.hasSelectedNewProject = false;
} }
}, },
setSelectedProject(data) { setSelectedProject(data) {
this.hasSelectedNewProject = true;
this.selectedProject = data; this.selectedProject = data;
this.$refs.dropdown.hide(true); this.$refs.dropdown.hide();
},
closeModal() {
this.$emit('close');
}, },
}, },
}; };
</script> </script>
<template> <template>
<section> <gl-modal
<gl-alert v-bind="$attrs"
v-if="showAssignError" ref="modal"
class="gl-mt-3" cancel-variant="light"
data-testid="policy-project-assign-error" size="sm"
variant="danger" modal-id="scan-new-policy"
:dismissible="true" :scrollable="false"
@dismiss="dismissAlert('showAssignError')" :ok-title="$options.i18n.modal.okTitle"
> :title="$options.i18n.modal.header"
{{ $options.i18n.assignError }} :ok-disabled="isModalOkButtonDisabled"
</gl-alert> :visible="visible"
<gl-alert @ok="saveChanges"
v-else-if="showAssignSuccess" @change="closeModal"
class="gl-mt-3" >
data-testid="policy-project-assign-success" <div>
variant="success" <gl-alert
:dismissible="true" v-if="disableSecurityPolicyProject"
@dismiss="dismissAlert('showAssignSuccess')" class="gl-mb-4"
> variant="warning"
{{ $options.i18n.assignSuccess }} :dismissible="false"
</gl-alert> >
<h2 class="gl-mb-8"> {{ $options.i18n.disabledWarning }}
{{ s__('SecurityOrchestration|Create a policy') }} </gl-alert>
</h2>
<div class="gl-w-half">
<h4>
{{ s__('SecurityOrchestration|Security policy project') }}
</h4>
<gl-dropdown <gl-dropdown
ref="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!" menu-class="gl-w-full! gl-max-w-full!"
:disabled="disableSecurityPolicyProject" :disabled="disableSecurityPolicyProject"
:text="selectedProject.name || ''" :text="selectedProjectName"
> >
<instance-project-selector <instance-project-selector
class="gl-w-full" class="gl-w-full"
...@@ -135,7 +135,7 @@ export default { ...@@ -135,7 +135,7 @@ export default {
/> />
</gl-dropdown> </gl-dropdown>
<div class="gl-pb-5"> <div class="gl-pb-5">
<gl-sprintf :message="$options.i18n.securityProject"> <gl-sprintf :message="$options.i18n.description">
<template #link="{ content }"> <template #link="{ content }">
<gl-button class="gl-pb-1!" variant="link" :href="documentationPath" target="_blank"> <gl-button class="gl-pb-1!" variant="link" :href="documentationPath" target="_blank">
{{ content }} {{ content }}
...@@ -143,24 +143,6 @@ export default { ...@@ -143,24 +143,6 @@ export default {
</template> </template>
</gl-sprintf> </gl-sprintf>
</div> </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> </div>
</section> </gl-modal>
</template> </template>
...@@ -2,7 +2,7 @@ import Vue from 'vue'; ...@@ -2,7 +2,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils'; 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); Vue.use(VueApollo);
...@@ -16,24 +16,22 @@ export default () => { ...@@ -16,24 +16,22 @@ export default () => {
assignedPolicyProject, assignedPolicyProject,
disableSecurityPolicyProject, disableSecurityPolicyProject,
documentationPath, documentationPath,
newPolicyPath,
projectPath, projectPath,
} = el.dataset; } = el.dataset;
const policyProject = JSON.parse(assignedPolicyProject);
const props = policyProject ? { assignedPolicyProject: policyProject } : {};
return new Vue({ return new Vue({
apolloProvider, apolloProvider,
el, el,
provide: { provide: {
assignedPolicyProject: JSON.parse(assignedPolicyProject),
disableSecurityPolicyProject: parseBoolean(disableSecurityPolicyProject), disableSecurityPolicyProject: parseBoolean(disableSecurityPolicyProject),
documentationPath, documentationPath,
newPolicyPath,
projectPath, projectPath,
}, },
render(createElement) { render(createElement) {
return createElement(SecurityPolicyProjectSelector, { return createElement(SecurityPoliciesApp);
props,
});
}, },
}); });
}; };
<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; ...@@ -123,7 +123,6 @@ $badge-height: $gl-spacing-scale-7;
.card-wrapper { .card-wrapper {
margin-bottom: $gutter-small; margin-bottom: $gutter-small;
padding-top: $badge-height;
width: calc(50% - #{$gutter-small} / 2); width: calc(50% - #{$gutter-small} / 2);
&-has-badge { &-has-badge {
......
...@@ -69,7 +69,7 @@ module EE ...@@ -69,7 +69,7 @@ module EE
{ {
namespace_id: namespace.id, namespace_id: namespace.id,
plan_name: namespace.actual_plan_name.titleize, plan_name: ::Plan::ULTIMATE.titleize,
action: action action: action
} }
end end
......
...@@ -9,4 +9,5 @@ ...@@ -9,4 +9,5 @@
- else - else
= render 'shared/billings/billing_plans', plans_data: @plans_data, namespace: @group, current_plan: current_plan = 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') - 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 = 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 @@ ...@@ -4,4 +4,5 @@
#js-security-policies-list{ data: { assigned_policy_project: assigned_policy_project(project).to_json, #js-security-policies-list{ data: { assigned_policy_project: assigned_policy_project(project).to_json,
disable_security_policy_project: disable_security_policy_project.to_s, 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'), 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 } } project_path: project.full_path } }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- if namespace_for_user - if namespace_for_user
= render_if_exists 'trials/banner', namespace: namespace = 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 .billing-plan-logo
- if namespace_for_user - if namespace_for_user
.avatar-container.s96.home-panel-avatar.gl-mr-3.float-none.mx-auto.mb-4.mt-1 .avatar-container.s96.home-panel-avatar.gl-mr-3.float-none.mx-auto.mb-4.mt-1
...@@ -34,3 +34,7 @@ ...@@ -34,3 +34,7 @@
- if show_start_free_trial_messages?(namespace) - if show_start_free_trial_messages?(namespace)
- glm_content = namespace_for_user ? 'user-billing' : 'group-billing' - 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' } %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 ...@@ -20,12 +20,22 @@ FactoryBot.define do
trial_ends_on { Date.current.advance(days: 15) } trial_ends_on { Date.current.advance(days: 15) }
end end
trait :extended_trial do
active_trial
trial_extension_type { GitlabSubscription.trial_extension_types[:extended] }
end
trait :expired_trial do trait :expired_trial do
trial { true } trial { true }
trial_starts_on { Date.current.advance(days: -31) } trial_starts_on { Date.current.advance(days: -31) }
trial_ends_on { Date.current.advance(days: -1) } trial_ends_on { Date.current.advance(days: -1) }
end end
trait :reactivated_trial do
expired_trial
trial_extension_type { GitlabSubscription.trial_extension_types[:reactivated] }
end
trait :default do trait :default do
association :hosted_plan, factory: :default_plan association :hosted_plan, factory: :default_plan
end 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 ...@@ -6,6 +6,7 @@ import SubscriptionTable from 'ee/billings/subscriptions/components/subscription
import SubscriptionTableRow from 'ee/billings/subscriptions/components/subscription_table_row.vue'; import SubscriptionTableRow from 'ee/billings/subscriptions/components/subscription_table_row.vue';
import initialStore from 'ee/billings/subscriptions/store'; import initialStore from 'ee/billings/subscriptions/store';
import * as types from 'ee/billings/subscriptions/store/mutation_types'; 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 { mockDataSubscription } from 'ee_jest/billings/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
...@@ -361,4 +362,33 @@ describe('SubscriptionTable component', () => { ...@@ -361,4 +362,33 @@ describe('SubscriptionTable component', () => {
expect(findRefreshSeatsButton().exists()).toBe(false); 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 { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import InstanceProjectSelector from 'ee/threat_monitoring/components/instance_project_selector.vue'; 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 assignSecurityPolicyProject from 'ee/threat_monitoring/graphql/mutations/assign_security_policy_project.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { stubComponent } from 'helpers/stub_component';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import { mockAssignSecurityPolicyProjectResponses } from '../../mocks/mock_apollo';
import {
apolloFailureResponse,
mockAssignSecurityPolicyProjectResponses,
} from '../mocks/mock_apollo';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
describe('SecurityPolicyProjectSelector Component', () => { describe('ScanNewPolicyModal Component', () => {
let wrapper; let wrapper;
let projectUpdatedListener;
const findSaveButton = () => wrapper.findByTestId('save-policy-project');
const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
const findErrorAlert = () => wrapper.findByTestId('policy-project-assign-error');
const findInstanceProjectSelector = () => wrapper.findComponent(InstanceProjectSelector); const findInstanceProjectSelector = () => wrapper.findComponent(InstanceProjectSelector);
const findSuccessAlert = () => wrapper.findByTestId('policy-project-assign-success'); const findAlert = () => wrapper.findComponent(GlAlert);
const findTooltip = () => wrapper.findByTestId('disabled-button-tooltip'); const findModal = () => wrapper.findComponent(GlModal);
const selectProject = async () => { const selectProject = async (
findInstanceProjectSelector().vm.$emit('projectClicked', { project = {
id: 'gid://gitlab/Project/1', id: 'gid://gitlab/Project/1',
name: 'Test 1', name: 'Test 1',
}); },
) => {
findInstanceProjectSelector().vm.$emit('projectClicked', project);
await wrapper.vm.$nextTick();
findModal().vm.$emit('ok');
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
findSaveButton().vm.$emit('click');
await waitForPromises();
}; };
const createWrapper = ({ const createWrapper = ({
mount = shallowMountExtended,
mutationResult = mockAssignSecurityPolicyProjectResponses.success, mutationResult = mockAssignSecurityPolicyProjectResponses.success,
propsData = {},
provide = {}, provide = {},
} = {}) => { } = {}) => {
wrapper = mount(SecurityPolicyProjectSelector, { wrapper = mountExtended(ScanNewPolicyModal, {
localVue, localVue,
apolloProvider: createMockApollo([[assignSecurityPolicyProject, mutationResult]]), apolloProvider: createMockApollo([[assignSecurityPolicyProject, mutationResult]]),
directives: { stubs: {
GlTooltip: createMockDirective(), GlModal: stubComponent(GlModal, {
template:
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
}),
}, },
propsData,
provide: { provide: {
disableSecurityPolicyProject: false, disableSecurityPolicyProject: false,
documentationPath: 'test/path/index.md', documentationPath: 'test/path/index.md',
projectPath: 'path/to/project', projectPath: 'path/to/project',
assignedPolicyProject: null,
...provide, ...provide,
}, },
}); });
projectUpdatedListener = jest.fn();
wrapper.vm.$on('project-updated', projectUpdatedListener);
};
const createWrapperAndSelectProject = async (data) => {
createWrapper(data);
await selectProject();
}; };
afterEach(() => { afterEach(() => {
...@@ -67,72 +73,69 @@ describe('SecurityPolicyProjectSelector Component', () => { ...@@ -67,72 +73,69 @@ describe('SecurityPolicyProjectSelector Component', () => {
createWrapper(); createWrapper();
}); });
it.each` it('passes down correct properties/attributes to the gl-modal component', () => {
findComponent | state | title expect(findModal().props()).toMatchObject({
${findDropdown} | ${true} | ${'does display the dropdown'} modalId: 'scan-new-policy',
${findInstanceProjectSelector} | ${true} | ${'does display the project selector'} size: 'sm',
${findErrorAlert} | ${false} | ${'does not display the error alert'} visible: false,
${findSuccessAlert} | ${false} | ${'does not display the success alert'} title: 'Select security project',
`('$title', ({ findComponent, state }) => { });
expect(findComponent().exists()).toBe(state);
});
it('renders the "Save Changes" button', () => { expect(findModal().attributes()).toEqual({
const button = findSaveButton(); 'ok-disabled': 'true',
expect(button.exists()).toBe(true); 'ok-title': 'Save',
expect(button.attributes('disabled')).toBe('true'); 'cancel-variant': 'light',
});
}); });
it('does not display a tooltip', () => { it('does not display a warning', () => {
const tooltip = getBinding(findTooltip().element, 'gl-tooltip'); expect(findAlert().exists()).toBe(false);
expect(tooltip.value.disabled).toBe(true);
}); });
}); });
it('emits close event when gl-modal emits change event', () => {
createWrapper();
findModal().vm.$emit('change');
expect(wrapper.emitted('close')).toEqual([[]]);
});
describe('project selection', () => { 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({ createWrapper({
mount: mountExtended, provide: { assignedPolicyProject: { id: 'gid://gitlab/Project/0', name: 'Test 0' } },
propsData: { 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', { findInstanceProjectSelector().vm.$emit('projectClicked', {
id: 'gid://gitlab/Project/1', id: 'gid://gitlab/Project/1',
name: 'Test 1', name: 'Test 1',
}); });
await wrapper.vm.$nextTick(); 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 () => { it('emits an event with success message', async () => {
createWrapper({ mount: mountExtended }); await createWrapperAndSelectProject();
expect(findErrorAlert().exists()).toBe(false);
expect(findSuccessAlert().exists()).toBe(false); expect(projectUpdatedListener).toHaveBeenCalledWith({
await selectProject(); text: 'Security policy project was linked successfully',
expect(findErrorAlert().exists()).toBe(false); variant: 'success',
expect(findSuccessAlert().exists()).toBe(true); });
}); });
it('shows an alert if the security policy project selection fails', async () => { it('emits an event with an error message', async () => {
createWrapper({ await createWrapperAndSelectProject({
mount: mountExtended,
mutationResult: mockAssignSecurityPolicyProjectResponses.failure, 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 () => { expect(projectUpdatedListener).toHaveBeenCalledWith({
createWrapper({ mount: mountExtended, mutationResult: apolloFailureResponse }); text: 'An error occurred assigning your security policy project',
expect(findErrorAlert().exists()).toBe(false); variant: 'danger',
expect(findSuccessAlert().exists()).toBe(false); });
await selectProject();
expect(findErrorAlert().exists()).toBe(true);
expect(findSuccessAlert().exists()).toBe(false);
}); });
}); });
...@@ -142,12 +145,11 @@ describe('SecurityPolicyProjectSelector Component', () => { ...@@ -142,12 +145,11 @@ describe('SecurityPolicyProjectSelector Component', () => {
}); });
it('disables the dropdown', () => { it('disables the dropdown', () => {
expect(findDropdown().attributes('disabled')).toBe('true'); expect(findDropdown().props('disabled')).toBe(true);
}); });
it('displays a tooltip', () => { it('displays a warning', () => {
const tooltip = getBinding(findTooltip().element, 'gl-tooltip'); expect(findAlert().text()).toBe('Only owners can update Security Policy Project');
expect(tooltip.value.disabled).toBe(false);
}); });
}); });
}); });
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 ...@@ -197,24 +197,20 @@ RSpec.describe EE::TrialHelper do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
context 'when feature flag is enabled' do where(:can_extend_trial, :can_reactivate_trial, :result) do
where(:can_extend_trial, :can_reactivate_trial, :result) do false | false | false
false | false | false true | false | true
true | false | true false | true | true
false | true | true true | true | true
true | true | true end
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
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 end
it { is_expected.to eq(result) }
end end
end end
...@@ -253,22 +249,20 @@ RSpec.describe EE::TrialHelper do ...@@ -253,22 +249,20 @@ RSpec.describe EE::TrialHelper do
end end
end end
context 'when feature flag is enabled' do context 'when trial can be extended' do
context 'when trial can be extended' do before do
before do allow(namespace).to receive(:can_extend_trial?).and_return(true)
allow(namespace).to receive(:can_extend_trial?).and_return(true)
end
it { is_expected.to eq({ namespace_id: 1, plan_name: 'Ultimate', action: 'extend' }) }
end end
context 'when trial can be reactivated' do it { is_expected.to eq({ namespace_id: 1, plan_name: 'Ultimate', action: 'extend' }) }
before do end
allow(namespace).to receive(:can_reactivate_trial?).and_return(true)
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 end
it { is_expected.to eq({ namespace_id: 1, plan_name: 'Ultimate', action: 'reactivate' }) }
end end
end end
end end
...@@ -1261,22 +1261,20 @@ RSpec.describe Namespace do ...@@ -1261,22 +1261,20 @@ RSpec.describe Namespace do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
context 'when feature flag is enabled' do where(:trial_active, :trial_extended_or_reactivated, :can_extend_trial) do
where(:trial_active, :trial_extended_or_reactivated, :can_extend_trial) do false | false | false
false | false | false false | true | false
false | true | false true | false | true
true | false | true true | true | false
true | true | false end
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
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 end
it { is_expected.to be can_extend_trial }
end end
end end
...@@ -1296,36 +1294,34 @@ RSpec.describe Namespace do ...@@ -1296,36 +1294,34 @@ RSpec.describe Namespace do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end end
context 'when feature flag is enabled' do where(:trial_active, :never_had_trial, :trial_extended_or_reactivated, :free_plan, :can_reactivate_trial) 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 | false | false false | false | false | true | true
false | false | false | true | true false | false | true | false | false
false | false | true | false | false false | false | true | true | false
false | false | true | true | false false | true | false | false | false
false | true | false | false | false false | true | false | true | false
false | true | false | true | false false | true | true | false | false
false | true | true | false | false false | true | true | true | false
false | true | true | true | false true | false | false | false | false
true | false | false | false | false true | false | false | true | false
true | false | false | true | false true | false | true | false | false
true | false | true | false | false true | false | true | true | false
true | false | true | true | false true | true | false | false | false
true | true | false | false | false true | true | false | true | false
true | true | false | true | false true | true | true | false | false
true | true | true | false | false true | true | true | true | false
true | true | true | true | false end
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
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 end
it { is_expected.to be can_reactivate_trial }
end end
end end
......
...@@ -5178,6 +5178,27 @@ msgstr "" ...@@ -5178,6 +5178,27 @@ msgstr ""
msgid "BillingPlan|Upgrade for free" msgid "BillingPlan|Upgrade for free"
msgstr "" 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." msgid "Billings|Shared runners cannot be enabled until a valid credit card is on file."
msgstr "" msgstr ""
...@@ -21819,6 +21840,12 @@ msgstr "" ...@@ -21819,6 +21840,12 @@ msgstr ""
msgid "NetworkPolicies|Edit policy" msgid "NetworkPolicies|Edit policy"
msgstr "" msgstr ""
msgid "NetworkPolicies|Edit policy project"
msgstr ""
msgid "NetworkPolicies|Enforce security for this project. %{linkStart}More information.%{linkEnd}"
msgstr ""
msgid "NetworkPolicies|Enforcement status" msgid "NetworkPolicies|Enforcement status"
msgstr "" msgstr ""
...@@ -21861,6 +21888,9 @@ 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." msgid "NetworkPolicies|Please %{installLinkStart}install%{installLinkEnd} and %{configureLinkStart}configure a Kubernetes Agent for this project%{configureLinkEnd} to enable alerts."
msgstr "" 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." msgid "NetworkPolicies|Policies are a specification of how groups of pods are allowed to communicate with each other's network endpoints."
msgstr "" msgstr ""
...@@ -29313,22 +29343,19 @@ msgstr "" ...@@ -29313,22 +29343,19 @@ msgstr ""
msgid "SecurityConfiguration|You can quickly enable all security scanning tools by enabling %{linkStart}Auto DevOps%{linkEnd}." msgid "SecurityConfiguration|You can quickly enable all security scanning tools by enabling %{linkStart}Auto DevOps%{linkEnd}."
msgstr "" 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" msgid "SecurityOrchestration|An error occurred assigning your security policy project"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Create a policy" msgid "SecurityOrchestration|Only owners can update Security Policy Project"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Only owners can update Security Policy Project" msgid "SecurityOrchestration|Security policy project was linked successfully"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Security policy project" msgid "SecurityOrchestration|Select a project to store your security policies in. %{linkStart}More information.%{linkEnd}"
msgstr "" msgstr ""
msgid "SecurityOrchestration|Security policy project was linked successfully" msgid "SecurityOrchestration|Select security project"
msgstr "" msgstr ""
msgid "SecurityPolicies|+%{count} more" 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