Commit d1d63b17 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '238607-remove-feature-flag-licenses_app-and-code-that-is-enabled-by-it' into 'master'

Remove feature flag licenses_app and code that is enabled by it

Closes #238607

See merge request gitlab-org/gitlab!42520
parents 9c4ea521 5434772f
......@@ -923,8 +923,3 @@ $compare-branches-sticky-header-height: 68px;
- Issue: https://gitlab.com/gitlab-org/design.gitlab.com/issues/242
*/
$enable-validation-icons: false;
/*
Licenses
*/
$license-header-cell-width: 150px;
import LicenseCard from './license_card.vue';
import SkeletonLicenseCard from './skeleton_license_card.vue';
export { LicenseCard, SkeletonLicenseCard };
<script>
import { mapState, mapActions } from 'vuex';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
import LicenseCardBody from './license_card_body.vue';
export default {
name: 'LicenseCard',
components: {
LicenseCardBody,
GlDropdown,
GlDropdownItem,
},
props: {
license: {
type: Object,
required: false,
default() {
return { licensee: {} };
},
},
isCurrentLicense: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState(['activeUserCount', 'guestUserCount', 'deleteQueue', 'downloadLicensePath']),
isRemoving() {
return this.deleteQueue.includes(this.license.id);
},
},
methods: {
...mapActions(['fetchDeleteLicense']),
capitalizeFirstCharacter,
confirmDeleteLicense(...args) {
window.confirm(__('Are you sure you want to permanently delete this license?')); // eslint-disable-line no-alert
this.fetchDeleteLicense(...args);
},
},
};
</script>
<template>
<div class="card license-card mb-5">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h4>
{{
sprintf(__('GitLab Enterprise Edition %{plan}'), {
plan: capitalizeFirstCharacter(license.plan),
})
}}
</h4>
<gl-dropdown right class="js-manage-license" :text="__('Manage')" :disabled="isRemoving">
<gl-dropdown-item
v-if="isCurrentLicense"
class="js-download-license"
:href="downloadLicensePath"
>
{{ __('Download license') }}
</gl-dropdown-item>
<gl-dropdown-item
class="js-delete-license text-danger"
@click="confirmDeleteLicense(license)"
>
{{ __('Delete license') }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</div>
<license-card-body
:license="license"
:is-removing="isRemoving"
:active-user-count="activeUserCount"
:guest-user-count="guestUserCount"
/>
</div>
</template>
<script>
import { GlLink, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { Cell, HeaderCell, InfoCell, DateCell } from '../cells';
export default {
name: 'LicenseCardBody',
components: {
GlIcon,
Cell,
HeaderCell,
InfoCell,
DateCell,
GlLink,
},
props: {
license: {
type: Object,
required: false,
default() {
return {
licensee: {},
};
},
},
isRemoving: {
type: Boolean,
required: false,
default: false,
},
activeUserCount: {
type: Number,
required: true,
},
guestUserCount: {
type: Number,
required: true,
},
},
data() {
return {
info: {
currentActiveUserCount: __(
"Users with a Guest role or those who don't belong to any projects or groups don't count towards seats in use.",
),
historicalMax: __(`This is the maximum number of users that have existed at the same time since the license started.
This is the minimum number of seats you will need to buy when you renew your license.`),
overage: __(`GitLab allows you to continue using your license even if you exceed the number of seats you purchased.
You will be required to pay for these seats when you renew your license.`),
},
};
},
computed: {
seatsInUseComponent() {
return this.license.plan === 'ultimate' ? 'info-cell' : 'cell';
},
seatsInUseForThisLicense() {
return this.license.plan === 'ultimate'
? this.activeUserCount - this.guestUserCount
: this.activeUserCount;
},
},
methods: {
licenseeValue(key) {
return this.license.licensee[key] || __('Unknown');
},
},
};
</script>
<template>
<div class="card-body license-card-body p-0">
<div
v-if="isRemoving"
class="p-5 d-flex justify-content-center align-items-center license-card-loading"
>
<gl-icon name="spinner" /><span class="ml-2">{{ __('Removing license…') }}</span>
</div>
<div v-else class="license-table js-license-table">
<div class="license-row d-flex">
<header-cell :title="__('Usage')" icon="monitor" />
<cell :title="__('Seats in license')" :value="license.userLimit || __('Unlimited')" />
<component
:is="seatsInUseComponent"
:title="__('Seats currently in use')"
:value="seatsInUseForThisLicense"
:popover-content="info.currentActiveUserCount"
/>
<info-cell
:title="__('Max seats used')"
:value="license.historicalMax"
:popover-content="info.historicalMax"
/>
<info-cell
:title="__('Users outside of license')"
:value="license.overage"
:popover-content="info.overage"
/>
</div>
<div class="license-row d-flex">
<header-cell :title="__('Validity')" icon="calendar" />
<date-cell :title="__('Start date')" :value="license.startsAt" />
<date-cell :title="__('End date')" :value="license.expiresAt" :is-expirable="true" />
<date-cell :title="__('Uploaded on')" :value="license.createdAt" />
</div>
<div class="license-row d-flex">
<header-cell :title="__('Registration')" icon="user" />
<cell :title="__('Licensed to')" :value="licenseeValue('Name')" />
<cell :title="__('Email address')" :value="licenseeValue('Email')" />
<cell :title="__('Company')" :value="licenseeValue('Company')" />
</div>
</div>
</div>
</template>
<script>
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { SkeletonCell, SkeletonHeaderCell } from '../cells';
export default {
name: 'SkeletonLicenseCard',
components: {
GlSkeletonLoading,
SkeletonCell,
SkeletonHeaderCell,
},
};
</script>
<template>
<div class="card license-card skeleton-license-card">
<div class="card-header d-flex justify-content-between align-items-center py-3">
<gl-skeleton-loading class="w-75 skeleton-bar" :lines="1" />
</div>
<div class="card-body p-0">
<div class="license-table">
<div class="license-row d-flex">
<skeleton-header-cell />
<skeleton-cell />
<skeleton-cell />
<skeleton-cell />
<skeleton-cell />
</div>
<div class="license-row d-flex">
<skeleton-header-cell />
<skeleton-cell />
<skeleton-cell />
<skeleton-cell />
</div>
<div class="license-row d-flex">
<skeleton-header-cell />
<skeleton-cell />
<skeleton-cell />
<skeleton-cell />
</div>
</div>
</div>
</div>
</template>
<script>
import { isNumber } from 'lodash';
export default {
// name: 'Cell' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'Cell',
props: {
title: {
type: String,
required: false,
default: null,
},
value: {
type: [String, Number],
required: false,
default: null,
},
isFlexible: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
valueClass() {
return { number: isNumber(this.value) };
},
flexClass() {
return { 'flex-grow-1': this.isFlexible };
},
},
};
</script>
<template>
<div class="license-cell p-3 text-nowrap flex-shrink-0" :class="flexClass">
<span class="title d-flex align-items-center justify-content-start">
<slot name="title">
<span>{{ title }}</span>
</slot>
</span>
<div class="value mt-2" :class="valueClass">
<slot name="value">
<span>{{ value }}</span>
</slot>
</div>
</div>
</template>
<script>
import { dateInWords } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import Cell from './cell.vue';
export default {
name: 'DateCell',
components: {
Cell,
},
props: {
title: {
type: String,
required: false,
default: null,
},
value: {
type: [String, Date],
required: false,
default: null,
},
dateNow: {
type: Date,
required: false,
default() {
return new Date();
},
},
isExpirable: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
dateInWordsValue() {
return dateInWords(this.dateValue);
},
dateValue() {
return new Date(this.value);
},
isExpired() {
return this.isExpirable && this.dateValue < this.dateNow;
},
valueClass() {
return { 'text-danger': this.isExpired };
},
fallbackValue() {
return this.isExpirable ? this.dateInWords || __('Never') : this.dateInWords;
},
},
};
</script>
<template>
<cell :title="title" :value="fallbackValue">
<div v-if="value" slot="value" :class="valueClass">
{{ dateInWordsValue }}
<span v-if="isExpired"> - {{ __('Expired') }} </span>
</div>
</cell>
</template>
<script>
import { GlIcon } from '@gitlab/ui';
import Cell from './cell.vue';
export default {
name: 'HeaderCell',
components: {
GlIcon,
Cell,
},
props: {
title: {
type: String,
required: true,
},
icon: {
type: String,
required: true,
},
},
};
</script>
<template>
<cell class="license-header-cell" :is-flexible="false">
<template #title>
<gl-icon class="icon" :name="icon" />
<span class="ml-2 font-weight-bold">{{ title }}</span>
</template>
</cell>
</template>
import Cell from './cell.vue';
import HeaderCell from './header_cell.vue';
import InfoCell from './info_cell.vue';
import DateCell from './date_cell.vue';
import SkeletonCell from './skeleton_cell.vue';
import SkeletonHeaderCell from './skeleton_header_cell.vue';
export { Cell, HeaderCell, InfoCell, DateCell, SkeletonCell, SkeletonHeaderCell };
<script>
import { GlPopover, GlIcon } from '@gitlab/ui';
import Cell from './cell.vue';
export default {
name: 'InfoCell',
components: {
GlIcon,
GlPopover,
Cell,
},
props: {
title: {
type: String,
required: true,
default: null,
},
value: {
type: [Number, String],
required: false,
default: null,
},
popoverContent: {
type: String,
required: false,
default: null,
},
},
data() {
return {
popoverTarget: null,
};
},
mounted() {
this.popoverTarget = this.$refs.popoverTarget;
},
};
</script>
<template>
<cell class="license-info-cell" :value="value">
<template slot="title">
<span class="mr-2 text">{{ title }}</span>
<button ref="popoverTarget" type="button" class="btn-link information-target">
<gl-icon name="information" class="icon d-block" />
</button>
<gl-popover
placement="bottom"
:target="popoverTarget"
:content="popoverContent"
triggers="hover"
/>
</template>
</cell>
</template>
<script>
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import Cell from './cell.vue';
export default {
name: 'SkeletonCell',
components: {
Cell,
GlSkeletonLoading,
},
};
</script>
<template>
<cell>
<gl-skeleton-loading slot="title" class="w-75 skeleton-bar" :lines="1" />
<gl-skeleton-loading slot="value" class="w-50 skeleton-bar" :lines="1" />
</cell>
</template>
<script>
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import Cell from './cell.vue';
export default {
name: 'SkeletonHeaderCell',
components: {
Cell,
GlSkeletonLoading,
},
};
</script>
<template>
<cell class="license-header-cell" :is-flexible="false">
<gl-skeleton-loading slot="title" class="w-75 skeleton-bar" :lines="1" />
</cell>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
import { GlButton } from '@gitlab/ui';
import { LicenseCard, SkeletonLicenseCard } from './cards';
export default {
name: 'LicenseCardsList',
components: {
LicenseCard,
SkeletonLicenseCard,
GlButton,
},
computed: {
...mapState(['licenses', 'isLoadingLicenses', 'newLicensePath']),
...mapGetters(['hasLicenses']),
},
};
</script>
<template>
<div>
<div class="d-flex justify-content-between align-items-center">
<h4>{{ __('Instance license') }}</h4>
<gl-button class="my-3 js-add-license" variant="success" :href="newLicensePath">
{{ __('Add license') }}
</gl-button>
</div>
<ul class="license-list list-unstyled">
<li v-if="isLoadingLicenses">
<skeleton-license-card />
</li>
<li v-for="(license, index) in licenses" v-else-if="hasLicenses" :key="license.id">
<license-card :license="license" :is-current-license="index === 0" />
</li>
<li v-else>
<strong>
{{ __('No licenses found.') }}
</strong>
</li>
</ul>
</div>
</template>
import Vue from 'vue';
import { mapActions } from 'vuex';
import store from './store';
import LicenseCardsList from './components/license_cards_list.vue';
export default function mountInstanceLicenseApp(mountElement) {
if (!mountElement) return undefined;
const {
activeUserCount,
guestUserCount,
licensesPath,
deleteLicensePath,
newLicensePath,
downloadLicensePath,
} = mountElement.dataset;
return new Vue({
el: mountElement,
store,
created() {
this.setInitialData({
licensesPath,
deleteLicensePath,
newLicensePath,
downloadLicensePath,
activeUserCount: parseInt(activeUserCount, 10),
guestUserCount: parseInt(guestUserCount, 10),
});
this.fetchLicenses();
},
methods: {
...mapActions(['setInitialData', 'fetchLicenses']),
},
render(createElement) {
return createElement(LicenseCardsList);
},
});
}
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
import flashMessage from './flash_message';
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const requestLicenses = ({ commit }) => commit(types.REQUEST_LICENSES);
export const receiveLicensesSuccess = ({ commit }, licenses) =>
commit(types.RECEIVE_LICENSES_SUCCESS, licenses);
export const receiveLicensesError = ({ commit }) => commit(types.RECEIVE_LICENSES_ERROR);
export const fetchLicenses = ({ state, dispatch }) => {
dispatch('requestLicenses');
return axios
.get(state.licensesPath)
.then(({ data }) =>
dispatch('receiveLicensesSuccess', convertObjectPropsToCamelCase(data, { deep: true })),
)
.catch(({ response }) => {
flashMessage('fetchLicenses', response.status);
dispatch('receiveLicensesError');
});
};
export const requestDeleteLicense = ({ commit }, license) =>
commit(types.REQUEST_DELETE_LICENSE, license);
export const receiveDeleteLicenseSuccess = ({ commit }, license) =>
commit(types.RECEIVE_DELETE_LICENSE_SUCCESS, license);
export const receiveDeleteLicenseError = ({ commit }, license) =>
commit(types.RECEIVE_DELETE_LICENSE_ERROR, license);
export const fetchDeleteLicense = ({ state, dispatch }, { id }) => {
dispatch('requestDeleteLicense', { id });
return axios
.delete(state.deleteLicensePath.replace(':id', id))
.then(() => dispatch('receiveDeleteLicenseSuccess', { id }))
.catch(({ response }) => {
flashMessage('fetchDeleteLicense', response.status);
dispatch('receiveDeleteLicenseError', { id });
});
};
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
const FLASH_MESSAGES = {
fetchLicenses: {
403: __('Fetching licenses failed. You are not permitted to perform this action.'),
404: __('Fetching licenses failed. The request endpoint was not found.'),
default: __('Fetching licenses failed.'),
},
fetchDeleteLicense: {
403: __('Deleting the license failed. You are not permitted to perform this action.'),
404: __('Deleting the license failed. The license was not found.'),
default: __('Deleting the license failed.'),
},
};
export default function flashMessage(action, status) {
const messages = FLASH_MESSAGES[action];
createFlash(messages[status] || messages.default);
}
export const hasLicenses = state => state.licenses.length > 0;
import Vue from 'vue';
import Vuex from 'vuex';
import createState from './state';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
Vue.use(Vuex);
export const createStore = () =>
new Vuex.Store({
state: createState(),
actions,
getters,
mutations,
});
export default createStore();
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const REQUEST_LICENSES = 'REQUEST_LICENSES';
export const RECEIVE_LICENSES_SUCCESS = 'RECEIVE_LICENSES_SUCCESS';
export const RECEIVE_LICENSES_ERROR = 'RECEIVE_LICENSES_ERROR';
export const REQUEST_DELETE_LICENSE = 'REQUEST_DELETE_LICENSE';
export const RECEIVE_DELETE_LICENSE_SUCCESS = 'RECEIVE_DELETE_LICENSE_SUCCESS';
export const RECEIVE_DELETE_LICENSE_ERROR = 'RECEIVE_DELETE_LICENSE_ERROR';
import * as types from './mutation_types';
export default {
[types.SET_INITIAL_DATA](state, data) {
Object.assign(state, data);
},
[types.REQUEST_LICENSES](state) {
state.isLoadingLicenses = true;
},
[types.RECEIVE_LICENSES_SUCCESS](state, licenses = []) {
state.isLoadingLicenses = false;
state.licenses = licenses;
},
[types.RECEIVE_LICENSES_ERROR](state) {
state.isLoadingLicenses = false;
},
[types.REQUEST_DELETE_LICENSE](state, { id }) {
if (state.deleteQueue.includes(id)) return;
state.deleteQueue.push(id);
},
[types.RECEIVE_DELETE_LICENSE_SUCCESS](state, { id }) {
const queueIndex = state.deleteQueue.indexOf(id);
const licenseIndex = state.licenses.findIndex(license => id === license.id);
if (queueIndex !== -1) state.deleteQueue.splice(queueIndex, 1);
if (licenseIndex !== -1) state.licenses.splice(licenseIndex, 1);
},
[types.RECEIVE_DELETE_LICENSE_ERROR](state, { id }) {
const queueIndex = state.deleteQueue.indexOf(id);
if (queueIndex !== -1) state.deleteQueue.splice(queueIndex, 1);
},
};
export default () => ({
licenses: [],
deleteQueue: [],
isLoadingLicenses: false,
licensesPath: '',
deleteLicensePath: '',
newLicensePath: '',
downloadLicensePath: '',
currentActiveUserCount: null,
});
import mountInstanceLicenseApp from 'ee/licenses';
document.addEventListener('DOMContentLoaded', () => {
const mountElement = document.getElementById('instance-license-mount-element');
mountInstanceLicenseApp(mountElement);
});
......@@ -5,64 +5,3 @@
color: $gray-600;
}
}
.license-card-body {
overflow-x: scroll;
@include media-breakpoint-up(lg) {
overflow-x: hidden;
}
}
.license-table {
min-width: max-content;
.license-row:first-child .license-cell {
border-top: 0;
}
.license-cell:last-child {
border-right: 0;
}
}
.license-cell {
border-color: $gray-100;
border-style: solid;
border-width: 1px 1px 0 0;
flex-basis: 0;
.title {
color: $gray-500;
line-height: $gl-line-height;
}
.value {
color: $gray-900;
&.number {
font-size: 1.25rem;
}
}
}
.license-header-cell {
flex-basis: initial;
width: $license-header-cell-width;
.title {
color: $gray-900;
}
}
.skeleton-license-card {
.skeleton-bar {
max-height: $gl-line-height;
.skeleton-line-1,
.skeleton-line-1::after {
height: 100%;
width: 100%;
}
}
}
......@@ -10,10 +10,6 @@ module LicenseHelper
License.current&.current_active_users_count || active_user_count
end
def guest_user_count
active_user_count - User.active.excluding_guests.count
end
def maximum_user_count
License.current&.maximum_user_count || 0
end
......@@ -53,22 +49,6 @@ module LicenseHelper
!Gitlab::CurrentSettings.should_check_namespace_plan? && show_promotions? && show_callout?('promote_advanced_search_dismissed') && !License.feature_available?(:elastic_search)
end
def license_app_data
{ data: { active_user_count: active_user_count,
guest_user_count: guest_user_count,
licenses_path: api_licenses_url,
delete_license_path: api_license_url(id: ':id'),
new_license_path: new_admin_license_path, download_license_path: download_admin_license_path } }
end
def api_licenses_url
expose_url(api_v4_licenses_path)
end
def api_license_url(args)
expose_url(api_v4_license_path(args))
end
extend self
private
......
- page_title _('License')
- if Feature.enabled?(:licenses_app)
#instance-license-mount-element{ license_app_data }
- else
%h3.page-title
%h3.page-title
= _('Your License')
- if @license&.trial?
= render 'upload_buy_license'
- else
= link_to _('Upload New License'), new_admin_license_path, class: 'btn btn-success float-right', data: { qa_selector: 'license_upload_link' }
%hr
%hr
- if License.future_dated_only?
- if License.future_dated_only?
.gl-alert.gl-alert-info
= sprite_icon('information-o', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
%h4.gl-alert-title= _('You do not have an active license')
= _('You have a license that activates at a future date. Please see the License History table below.')
- if @license.present?
- if @license.present?
= render 'info'
= render 'breakdown'
- if @licenses.present?
- if @licenses.present?
= render 'license_history'
---
name: licenses_app
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: false
......@@ -6,7 +6,6 @@ RSpec.describe "Admin uploads license", :js do
let_it_be(:admin) { create(:admin) }
before do
stub_feature_flags(licenses_app: false)
sign_in(admin)
end
......
......@@ -6,7 +6,6 @@ RSpec.describe "Admin views license" do
let_it_be(:admin) { create(:admin) }
before do
stub_feature_flags(licenses_app: false)
sign_in(admin)
allow_any_instance_of(Gitlab::ExpiringSubscriptionMessage).to receive(:grace_period_effective_from).and_return(Date.today - 45.days)
end
......
# frozen_string_literal: true
require "spec_helper"
RSpec.describe "Licenses app", :js do
let(:admin) { create(:admin) }
let!(:licenses) do
[
create(:license, data: build(:gitlab_license, restrictions: { active_user_count: 2000 }).export),
create(:license, data: build(:gitlab_license, expires_at: Date.today - 10, restrictions: { active_user_count: 2000, plan: 'ultimate' }).export)
]
end
def visit_page
visit(admin_license_path)
find('.js-license-table', match: :first)
end
def assert_usage_row(row, license)
header, seats_in_license, seats_in_use, historical_max, overage = row.find_all('.license-cell').to_a
expect(header).to have_content 'Usage'
expect(seats_in_license).to have_content 'Seats in license'
expect(seats_in_license).to have_content license.restrictions[:active_user_count]
expect(seats_in_use).to have_content 'Seats currently in use'
if license.exclude_guests_from_active_count?
expect(seats_in_use).to have_content User.active.excluding_guests.count
else
expect(seats_in_use).to have_content User.active.count
end
expect(historical_max).to have_content 'Max seats used'
expect(historical_max).to have_content license.historical_max
expect(overage).to have_content 'Users outside of license'
expect(overage).to have_content license.overage
end
def assert_validity_row(row, license)
header, starts_at, expires_at, created_at = row.find_all('.license-cell').to_a
expect(header).to have_content 'Validity'
expect(starts_at).to have_content 'Start date'
expect(starts_at).to have_content license.starts_at.strftime('%B %-d, %Y')
expect(expires_at).to have_content 'End date'
expect(expires_at).to have_content license.expires_at.strftime('%B %-d, %Y')
if license.expired?
expect(expires_at).to have_content 'Expired'
else
expect(expires_at).not_to have_content 'Expired'
end
expect(created_at).to have_content 'Uploaded on'
expect(created_at).to have_content license.created_at.strftime('%B %-d, %Y')
end
def assert_registration_row(row, license)
header, name, email, company = row.find_all('.license-cell').to_a
expect(header).to have_content 'Registration'
expect(name).to have_content 'Licensed to'
expect(name).to have_content license.licensee['Name'] || 'Unknown'
expect(email).to have_content 'Email address'
expect(email).to have_content license.licensee['Email'] || 'Unknown'
expect(company).to have_content 'Company'
expect(company).to have_content license.licensee['Company'] || 'Unknown'
end
def assert_license_card(card, license)
top_row, middle_row, bottom_row = card.find_all('.license-row').to_a
assert_usage_row(top_row, license)
assert_validity_row(middle_row, license)
assert_registration_row(bottom_row, license)
end
before do
stub_feature_flags(licenses_app: true)
sign_in(admin)
end
it 'renders a list of licenses' do
visit_page
licenses.each_with_index do |license, index|
assert_license_card(find_all('.license-table')[index], licenses.reverse[index])
end
end
it 'deletes a license' do
visit_page
license_card = find('.license-card', match: :first)
current_id = License.current.id
license_card.find('.js-manage-license').click
page.accept_alert 'Are you sure you want to permanently delete this license?' do
license_card.find('.js-delete-license').click
end
expect(license_card).not_to have_selector('.license-card-loading')
expect(License.find_by(id: current_id)).to be_nil
end
end
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InstanceCardsList renders a list of license cards 1`] = `
<div>
<div
class="d-flex justify-content-between align-items-center"
>
<h4>
Instance license
</h4>
<gl-button-stub
category="primary"
class="my-3 js-add-license"
href="/newLicensePath"
icon=""
size="medium"
variant="success"
>
Add license
</gl-button-stub>
</div>
<ul
class="license-list list-unstyled"
>
<li>
<license-card-stub
iscurrentlicense="true"
license="[object Object]"
/>
</li>
<li>
<license-card-stub
license="[object Object]"
/>
</li>
</ul>
</div>
`;
exports[`InstanceCardsList renders a message when there are no licenses 1`] = `
<div>
<div
class="d-flex justify-content-between align-items-center"
>
<h4>
Instance license
</h4>
<gl-button-stub
category="primary"
class="my-3 js-add-license"
href="/newLicensePath"
icon=""
size="medium"
variant="success"
>
Add license
</gl-button-stub>
</div>
<ul
class="license-list list-unstyled"
>
<li>
<strong>
No licenses found.
</strong>
</li>
</ul>
</div>
`;
exports[`InstanceCardsList renders a skeleton loading card if loading licenses 1`] = `
<div>
<div
class="d-flex justify-content-between align-items-center"
>
<h4>
Instance license
</h4>
<gl-button-stub
category="primary"
class="my-3 js-add-license"
href="/newLicensePath"
icon=""
size="medium"
variant="success"
>
Add license
</gl-button-stub>
</div>
<ul
class="license-list list-unstyled"
>
<li>
<skeleton-license-card-stub />
</li>
</ul>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LicenseCardBody renders a license card body 1`] = `
<div
class="card-body license-card-body p-0"
>
<div
class="license-table js-license-table"
>
<div
class="license-row d-flex"
>
<header-cell-stub
icon="monitor"
title="Usage"
/>
<cell-stub
isflexible="true"
title="Seats in license"
value="10"
/>
<info-cell-stub
popovercontent="Users with a Guest role or those who don't belong to any projects or groups don't count towards seats in use."
title="Seats currently in use"
value="2"
/>
<info-cell-stub
popovercontent="This is the maximum number of users that have existed at the same time since the license started. This is the minimum number of seats you will need to buy when you renew your license."
title="Max seats used"
value="20"
/>
<info-cell-stub
popovercontent="GitLab allows you to continue using your license even if you exceed the number of seats you purchased. You will be required to pay for these seats when you renew your license."
title="Users outside of license"
value="5"
/>
</div>
<div
class="license-row d-flex"
>
<header-cell-stub
icon="calendar"
title="Validity"
/>
<date-cell-stub
datenow="2017/10/10"
title="Start date"
value="2013/10/10"
/>
<date-cell-stub
datenow="2017/10/10"
isexpirable="true"
title="End date"
value="2015/10/10"
/>
<date-cell-stub
datenow="2017/10/10"
title="Uploaded on"
/>
</div>
<div
class="license-row d-flex"
>
<header-cell-stub
icon="user"
title="Registration"
/>
<cell-stub
isflexible="true"
title="Licensed to"
value="Jon Dough"
/>
<cell-stub
isflexible="true"
title="Email address"
value="email@address.tanuki"
/>
<cell-stub
isflexible="true"
title="Company"
value="TanukiVille"
/>
</div>
</div>
</div>
`;
exports[`LicenseCardBody renders a license card body without free user info for non-ultimate licenses 1`] = `
<div
class="card-body license-card-body p-0"
>
<div
class="license-table js-license-table"
>
<div
class="license-row d-flex"
>
<header-cell-stub
icon="monitor"
title="Usage"
/>
<cell-stub
isflexible="true"
title="Seats in license"
value="10"
/>
<cell-stub
isflexible="true"
popover-content="Users with a Guest role or those who don't belong to any projects or groups don't count towards seats in use."
title="Seats currently in use"
value="10"
/>
<info-cell-stub
popovercontent="This is the maximum number of users that have existed at the same time since the license started. This is the minimum number of seats you will need to buy when you renew your license."
title="Max seats used"
value="20"
/>
<info-cell-stub
popovercontent="GitLab allows you to continue using your license even if you exceed the number of seats you purchased. You will be required to pay for these seats when you renew your license."
title="Users outside of license"
value="5"
/>
</div>
<div
class="license-row d-flex"
>
<header-cell-stub
icon="calendar"
title="Validity"
/>
<date-cell-stub
datenow="2017/10/10"
title="Start date"
value="2013/10/10"
/>
<date-cell-stub
datenow="2017/10/10"
isexpirable="true"
title="End date"
value="2015/10/10"
/>
<date-cell-stub
datenow="2017/10/10"
title="Uploaded on"
/>
</div>
<div
class="license-row d-flex"
>
<header-cell-stub
icon="user"
title="Registration"
/>
<cell-stub
isflexible="true"
title="Licensed to"
value="Jon Dough"
/>
<cell-stub
isflexible="true"
title="Email address"
value="email@address.tanuki"
/>
<cell-stub
isflexible="true"
title="Company"
value="TanukiVille"
/>
</div>
</div>
</div>
`;
exports[`LicenseCardBody renders a loading state if isRemoving 1`] = `
<div
class="card-body license-card-body p-0"
>
<div
class="p-5 d-flex justify-content-center align-items-center license-card-loading"
>
<gl-icon-stub
name="spinner"
size="16"
/>
<span
class="ml-2"
>
Removing license…
</span>
</div>
</div>
`;
exports[`LicenseCardBody renders fallback licensee values 1`] = `
<div
class="card-body license-card-body p-0"
licensee="[object Object]"
>
<div
class="license-table js-license-table"
>
<div
class="license-row d-flex"
>
<header-cell-stub
icon="monitor"
title="Usage"
/>
<cell-stub
isflexible="true"
title="Seats in license"
value="10"
/>
<info-cell-stub
popovercontent="Users with a Guest role or those who don't belong to any projects or groups don't count towards seats in use."
title="Seats currently in use"
value="2"
/>
<info-cell-stub
popovercontent="This is the maximum number of users that have existed at the same time since the license started. This is the minimum number of seats you will need to buy when you renew your license."
title="Max seats used"
value="20"
/>
<info-cell-stub
popovercontent="GitLab allows you to continue using your license even if you exceed the number of seats you purchased. You will be required to pay for these seats when you renew your license."
title="Users outside of license"
value="5"
/>
</div>
<div
class="license-row d-flex"
>
<header-cell-stub
icon="calendar"
title="Validity"
/>
<date-cell-stub
datenow="2017/10/10"
title="Start date"
value="2013/10/10"
/>
<date-cell-stub
datenow="2017/10/10"
isexpirable="true"
title="End date"
value="2015/10/10"
/>
<date-cell-stub
datenow="2017/10/10"
title="Uploaded on"
/>
</div>
<div
class="license-row d-flex"
>
<header-cell-stub
icon="user"
title="Registration"
/>
<cell-stub
isflexible="true"
title="Licensed to"
value="Jon Dough"
/>
<cell-stub
isflexible="true"
title="Email address"
value="email@address.tanuki"
/>
<cell-stub
isflexible="true"
title="Company"
value="TanukiVille"
/>
</div>
</div>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LicenseCard renders license card with a delete button and license body 1`] = `
<div
class="card license-card mb-5"
>
<div
class="card-header"
>
<div
class="d-flex justify-content-between align-items-center"
>
<h4>
GitLab Enterprise Edition Super duper
</h4>
<gl-dropdown-stub
category="tertiary"
class="js-manage-license"
headertext=""
right=""
size="medium"
text="Manage"
variant="default"
>
<!---->
<gl-dropdown-item-stub
avatarurl=""
class="js-delete-license text-danger"
iconcolor=""
iconname=""
iconrightname=""
secondarytext=""
>
Delete license
</gl-dropdown-item-stub>
</gl-dropdown-stub>
</div>
</div>
<license-card-body-stub
activeusercount="10"
guestusercount="8"
license="[object Object]"
/>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SkeletonLicenseCard renders a skeleton license card 1`] = `
<div
class="card license-card skeleton-license-card"
>
<div
class="card-header d-flex justify-content-between align-items-center py-3"
>
<gl-skeleton-loading-stub
class="w-75 skeleton-bar"
lines="1"
/>
</div>
<div
class="card-body p-0"
>
<div
class="license-table"
>
<div
class="license-row d-flex"
>
<skeleton-header-cell-stub />
<skeleton-cell-stub />
<skeleton-cell-stub />
<skeleton-cell-stub />
<skeleton-cell-stub />
</div>
<div
class="license-row d-flex"
>
<skeleton-header-cell-stub />
<skeleton-cell-stub />
<skeleton-cell-stub />
<skeleton-cell-stub />
</div>
<div
class="license-row d-flex"
>
<skeleton-header-cell-stub />
<skeleton-cell-stub />
<skeleton-cell-stub />
<skeleton-cell-stub />
</div>
</div>
</div>
</div>
`;
import { shallowMount } from '@vue/test-utils';
import LicenseCardBody from 'ee/licenses/components/cards/license_card_body.vue';
describe('LicenseCardBody', () => {
let wrapper;
const defaultProps = {
license: {
plan: 'ultimate',
userLimit: 10,
historicalMax: 20,
overage: 5,
startsAt: '2013/10/10',
expiresAt: '2015/10/10',
licensee: {
Name: 'Jon Dough',
Email: 'email@address.tanuki',
Company: 'TanukiVille',
},
},
isRemoving: false,
activeUserCount: 10,
guestUserCount: 8,
};
function createComponent(props = {}) {
let propsData = props;
propsData.license = { ...defaultProps.license, ...(props.license || {}) };
propsData = { ...defaultProps, ...props };
wrapper = shallowMount(LicenseCardBody, {
propsData,
});
}
beforeEach(() => {
jest.spyOn(global.Date.prototype, 'toString').mockReturnValue('2017/10/10');
});
afterEach(() => {
if (wrapper) wrapper.destroy();
global.Date.prototype.toString.mockRestore();
});
it('renders a license card body', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('renders a license card body without free user info for non-ultimate licenses', () => {
createComponent({ license: { plan: 'premium' } });
expect(wrapper.element).toMatchSnapshot();
});
it('renders a loading state if isRemoving', () => {
createComponent({ isRemoving: true });
expect(wrapper.element).toMatchSnapshot();
});
it('renders fallback licensee values', () => {
createComponent({ licensee: {} });
expect(wrapper.element).toMatchSnapshot();
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { LicenseCard } from 'ee/licenses/components/cards';
describe('LicenseCard', () => {
let wrapper;
let actions;
const defaultProps = {
license: {
id: 1,
plan: 'super duper',
},
isCurrentLicense: false,
};
const defaultState = {
activeUserCount: 10,
guestUserCount: 8,
deleteQueue: [],
downloadLicensePath: '/downloadLicensePath',
};
const localVue = createLocalVue();
localVue.use(Vuex);
function createStore(newState) {
const state = { ...defaultState, ...newState };
actions = { fetchDeleteLicense: jest.fn() };
return new Vuex.Store({ state, actions });
}
function createComponent(state, props) {
const propsData = { ...defaultProps, ...props };
wrapper = shallowMount(LicenseCard, {
store: createStore(state),
propsData,
localVue,
});
}
afterEach(() => {
if (wrapper) wrapper.destroy();
});
it('renders license card with a delete button and license body', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
});
import { shallowMount } from '@vue/test-utils';
import { SkeletonLicenseCard } from 'ee/licenses/components/cards';
describe('SkeletonLicenseCard', () => {
let wrapper;
function createComponent() {
wrapper = shallowMount(SkeletonLicenseCard);
}
afterEach(() => {
if (wrapper) wrapper.destroy();
});
it('renders a skeleton license card', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Cell renders a number value and title through props 1`] = `
<div
class="license-cell p-3 text-nowrap flex-shrink-0 flex-grow-1"
>
<span
class="title d-flex align-items-center justify-content-start"
>
<span>
title
</span>
</span>
<div
class="value mt-2 number"
>
<span>
100
</span>
</div>
</div>
`;
exports[`Cell renders a string value and title through props 1`] = `
<div
class="license-cell p-3 text-nowrap flex-shrink-0 flex-grow-1"
>
<span
class="title d-flex align-items-center justify-content-start"
>
<span>
title
</span>
</span>
<div
class="value mt-2"
>
<span>
value
</span>
</div>
</div>
`;
exports[`Cell renders an inflexible variant 1`] = `
<div
class="license-cell p-3 text-nowrap flex-shrink-0"
>
<span
class="title d-flex align-items-center justify-content-start"
>
<span>
title
</span>
</span>
<div
class="value mt-2"
>
<span>
value
</span>
</div>
</div>
`;
exports[`Cell renders value and title slots that override props 1`] = `
<div
class="license-cell p-3 text-nowrap flex-shrink-0 flex-grow-1"
>
<span
class="title d-flex align-items-center justify-content-start"
>
<h1>
tanuki
</h1>
</span>
<div
class="value mt-2"
>
<marquee>
party
</marquee>
</div>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DateCell renders a date value that represents a date in words and title through props 1`] = `
<cell-stub
isflexible="true"
title="title"
>
<div
class=""
>
March 6, 2018
<!---->
</div>
</cell-stub>
`;
exports[`DateCell renders a fallback value if isExpirable and no value 1`] = `
<cell-stub
isflexible="true"
title="title"
value="Never"
>
<!---->
</cell-stub>
`;
exports[`DateCell renders a string value that represents a date in words and title through props 1`] = `
<cell-stub
isflexible="true"
title="title"
>
<div
class=""
>
October 24, 2018
<!---->
</div>
</cell-stub>
`;
exports[`DateCell renders an expired warning if isExpirable and date value is before now 1`] = `
<cell-stub
isflexible="true"
title="title"
value="Never"
>
<div
class="text-danger"
>
October 24, 2018
<span>
- Expired
</span>
</div>
</cell-stub>
`;
exports[`DateCell renders date value with no warning if isExpirable and date value is after now 1`] = `
<cell-stub
isflexible="true"
title="title"
value="Never"
>
<div
class=""
>
October 24, 2018
<!---->
</div>
</cell-stub>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InfoCell renders a number value 1`] = `
<cell-stub
class="license-info-cell"
isflexible="true"
value="100"
>
<template>
<span
class="mr-2 text"
>
title
</span>
<button
class="btn-link information-target"
type="button"
>
<gl-icon-stub
class="icon d-block"
name="information"
size="16"
/>
</button>
<gl-popover-stub
content="popoverContent"
cssclasses=""
placement="bottom"
triggers="hover"
/>
</template>
</cell-stub>
`;
exports[`InfoCell renders a title and string value with an info popover through props 1`] = `
<cell-stub
class="license-info-cell"
isflexible="true"
value="value"
>
<template>
<span
class="mr-2 text"
>
title
</span>
<button
class="btn-link information-target"
type="button"
>
<gl-icon-stub
class="icon d-block"
name="information"
size="16"
/>
</button>
<gl-popover-stub
content="popoverContent"
cssclasses=""
placement="bottom"
triggers="hover"
/>
</template>
</cell-stub>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SkeletonCell renders a skeleton cell with a title and value loading bar 1`] = `
<cell-stub
isflexible="true"
>
<gl-skeleton-loading-stub
class="w-75 skeleton-bar"
lines="1"
/>
<gl-skeleton-loading-stub
class="w-50 skeleton-bar"
lines="1"
/>
</cell-stub>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SkeletonHeaderCell renders a skeleton cell with a single title loading bar 1`] = `
<cell-stub
class="license-header-cell"
>
<gl-skeleton-loading-stub
class="w-75 skeleton-bar"
lines="1"
/>
</cell-stub>
`;
import { shallowMount } from '@vue/test-utils';
import { Cell } from 'ee/licenses/components/cells';
describe('Cell', () => {
let wrapper;
const defaultProps = {
title: 'title',
value: 'value',
};
function createComponent(props, slots) {
const propsData = { ...defaultProps, ...props };
wrapper = shallowMount(Cell, {
propsData,
slots,
});
}
afterEach(() => {
if (wrapper) wrapper.destroy();
});
it('renders a string value and title through props', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('renders a number value and title through props', () => {
createComponent({ value: 100 });
expect(wrapper.element).toMatchSnapshot();
});
it('renders value and title slots that override props', () => {
createComponent(null, { title: '<h1>tanuki</h1>', value: '<marquee>party</marquee>' });
expect(wrapper.element).toMatchSnapshot();
});
it('renders an inflexible variant', () => {
createComponent({ isFlexible: false });
expect(wrapper.element).toMatchSnapshot();
});
});
import { shallowMount } from '@vue/test-utils';
import { DateCell } from 'ee/licenses/components/cells';
describe('DateCell', () => {
let wrapper;
const defaultProps = {
title: 'title',
value: '2018/10/24',
};
function createComponent(props) {
const propsData = { ...defaultProps, ...props };
wrapper = shallowMount(DateCell, {
propsData,
});
}
afterEach(() => {
if (wrapper) wrapper.destroy();
});
it('renders a string value that represents a date in words and title through props', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('renders a date value that represents a date in words and title through props', () => {
createComponent({ value: new Date('2018/03/06') });
expect(wrapper.element).toMatchSnapshot();
});
it('renders an expired warning if isExpirable and date value is before now', () => {
createComponent({ isExpirable: true });
expect(wrapper.element).toMatchSnapshot();
});
it('renders date value with no warning if isExpirable and date value is after now', () => {
createComponent({ isExpirable: true, dateNow: new Date('2017/10/10') });
expect(wrapper.element).toMatchSnapshot();
});
it('renders a fallback value if isExpirable and no value', () => {
createComponent({ isExpirable: true, value: undefined });
expect(wrapper.element).toMatchSnapshot();
});
});
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import Cell from 'ee/licenses/components/cells/cell.vue';
import { HeaderCell } from 'ee/licenses/components/cells';
describe('HeaderCell', () => {
let wrapper;
function createComponent() {
wrapper = shallowMount(HeaderCell, {
propsData: {
title: 'title',
icon: 'retry',
},
stubs: {
Cell,
},
});
}
beforeEach(() => {
createComponent();
});
afterEach(() => {
if (wrapper) wrapper.destroy();
});
it('renders a cell with the correct "inflexible" value', () => {
expect(wrapper.find(Cell).props('isFlexible')).toBe(false);
});
it('renders an icon and title', () => {
expect(wrapper.find(GlIcon).props('name')).toBe('retry');
expect(wrapper.find(Cell).text()).toContain('title');
});
});
import { shallowMount } from '@vue/test-utils';
import { InfoCell } from 'ee/licenses/components/cells';
describe('InfoCell', () => {
let wrapper;
const defaultProps = {
title: 'title',
value: 'value',
popoverContent: 'popoverContent',
};
function createComponent(props, slots) {
const propsData = { ...defaultProps, ...props };
wrapper = shallowMount(InfoCell, {
propsData,
slots,
});
}
afterEach(() => {
if (wrapper) wrapper.destroy();
});
it('renders a title and string value with an info popover through props', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('renders a number value', () => {
createComponent({ value: 100 });
expect(wrapper.element).toMatchSnapshot();
});
});
import { shallowMount } from '@vue/test-utils';
import { SkeletonCell } from 'ee/licenses/components/cells';
describe('SkeletonCell', () => {
let wrapper;
function createComponent() {
wrapper = shallowMount(SkeletonCell);
}
afterEach(() => {
if (wrapper) wrapper.destroy();
});
it('renders a skeleton cell with a title and value loading bar', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
});
import { shallowMount } from '@vue/test-utils';
import { SkeletonHeaderCell } from 'ee/licenses/components/cells';
describe('SkeletonHeaderCell', () => {
let wrapper;
function createComponent() {
wrapper = shallowMount(SkeletonHeaderCell);
}
afterEach(() => {
if (wrapper) wrapper.destroy();
});
it('renders a skeleton cell with a single title loading bar', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import InstanceCardsList from 'ee/licenses/components/license_cards_list.vue';
import * as getters from 'ee/licenses/store/getters';
import createState from 'ee/licenses/store/state';
describe('InstanceCardsList', () => {
const newLicensePath = '/newLicensePath';
let wrapper;
const localVue = createLocalVue();
localVue.use(Vuex);
function createStore(store) {
const state = Object.assign(createState(), store, {
newLicensePath,
});
return new Vuex.Store({ state, getters });
}
function createComponent(store) {
wrapper = shallowMount(InstanceCardsList, {
store: createStore(store),
localVue,
});
}
afterEach(() => {
if (wrapper) wrapper.destroy();
});
it('renders a list of license cards', () => {
createComponent({ licenses: [{ id: 1 }, { id: 2 }], isLoadingLicenses: false });
expect(wrapper.element).toMatchSnapshot();
});
it('renders a skeleton loading card if loading licenses', () => {
createComponent({ isLoadingLicenses: true });
expect(wrapper.element).toMatchSnapshot();
});
it('renders a message when there are no licenses', () => {
createComponent({ licenses: [], isLoadingLicenses: false });
expect(wrapper.element).toMatchSnapshot();
});
});
......@@ -8,20 +8,6 @@ RSpec.describe LicenseHelper do
allow(Rails.application.routes).to receive(:default_url_options).and_return(url_options)
end
describe '#api_license_url' do
it 'returns license API url' do
stub_default_url_options
expect(api_license_url(id: 1)).to eq('http://localhost/api/v4/license/1')
end
it 'returns license API url with relative url' do
stub_default_url_options(script_name: '/gitlab')
expect(api_license_url(id: 1)).to eq('http://localhost/gitlab/api/v4/license/1')
end
end
describe '#current_active_user_count' do
let(:license) { create(:license) }
......@@ -43,12 +29,6 @@ RSpec.describe LicenseHelper do
end
end
describe '#guest_user_count' do
it 'returns the number of active guest users' do
expect(guest_user_count).to eq(User.active.count - User.active.excluding_guests.count)
end
end
describe '#maximum_user_count' do
context 'when current license is set' do
it 'returns the maximum_user_count for the current license' do
......
......@@ -5,10 +5,6 @@ require 'spec_helper'
RSpec.describe 'admin/licenses/show.html.haml' do
let_it_be(:license) { create(:license) }
before do
stub_feature_flags(licenses_app: false)
end
context 'when trial license is present' do
before do
trial_license = create(:license, trial: true)
......
......@@ -8,6 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-09-17 11:26-0400\n"
"PO-Revision-Date: 2020-09-17 11:26-0400\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
......@@ -1617,9 +1619,6 @@ msgstr ""
msgid "Add label(s)"
msgstr ""
msgid "Add license"
msgstr ""
msgid "Add list"
msgstr ""
......@@ -3363,9 +3362,6 @@ msgstr ""
msgid "Are you sure you want to merge immediately?"
msgstr ""
msgid "Are you sure you want to permanently delete this license?"
msgstr ""
msgid "Are you sure you want to re-deploy this environment?"
msgstr ""
......@@ -6456,9 +6452,6 @@ msgstr ""
msgid "Community forum"
msgstr ""
msgid "Company"
msgstr ""
msgid "Company name"
msgstr ""
......@@ -8224,9 +8217,6 @@ msgstr ""
msgid "Delete label: %{label_name} ?"
msgstr ""
msgid "Delete license"
msgstr ""
msgid "Delete list"
msgstr ""
......@@ -8302,15 +8292,6 @@ msgstr ""
msgid "Deleting a project places it into a read-only state until %{date}, at which point the project will be permanently deleted. Are you ABSOLUTELY sure?"
msgstr ""
msgid "Deleting the license failed."
msgstr ""
msgid "Deleting the license failed. The license was not found."
msgstr ""
msgid "Deleting the license failed. You are not permitted to perform this action."
msgstr ""
msgid "Deleting the project will delete its repository and all related resources including issues, merge requests etc."
msgstr ""
......@@ -9324,9 +9305,6 @@ msgstr ""
msgid "Email Notification"
msgstr ""
msgid "Email address"
msgstr ""
msgid "Email could not be sent"
msgstr ""
......@@ -9564,9 +9542,6 @@ msgstr ""
msgid "Encountered an error while rendering: %{err}"
msgstr ""
msgid "End date"
msgstr ""
msgid "Ends at (UTC)"
msgstr ""
......@@ -10985,15 +10960,6 @@ msgstr ""
msgid "Fetching incoming email"
msgstr ""
msgid "Fetching licenses failed."
msgstr ""
msgid "Fetching licenses failed. The request endpoint was not found."
msgstr ""
msgid "Fetching licenses failed. You are not permitted to perform this action."
msgstr ""
msgid "File"
msgstr ""
......@@ -11819,9 +11785,6 @@ msgstr ""
msgid "GitLab API"
msgstr ""
msgid "GitLab Enterprise Edition %{plan}"
msgstr ""
msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr ""
......@@ -11855,9 +11818,6 @@ msgstr ""
msgid "GitLab Workhorse"
msgstr ""
msgid "GitLab allows you to continue using your license even if you exceed the number of seats you purchased. You will be required to pay for these seats when you renew your license."
msgstr ""
msgid "GitLab commit"
msgstr ""
......@@ -13623,9 +13583,6 @@ msgstr ""
msgid "Instance administrators group already exists"
msgstr ""
msgid "Instance license"
msgstr ""
msgid "Integration"
msgstr ""
......@@ -15403,9 +15360,6 @@ msgstr ""
msgid "Max access level"
msgstr ""
msgid "Max seats used"
msgstr ""
msgid "Max size 15 MB"
msgstr ""
......@@ -17152,9 +17106,6 @@ msgstr ""
msgid "No license. All rights reserved"
msgstr ""
msgid "No licenses found."
msgstr ""
msgid "No matches found"
msgstr ""
......@@ -20900,9 +20851,6 @@ msgstr ""
msgid "Register with two-factor app"
msgstr ""
msgid "Registration"
msgstr ""
msgid "Registration|Checkout"
msgstr ""
......@@ -21229,9 +21177,6 @@ msgstr ""
msgid "Removes time estimate."
msgstr ""
msgid "Removing license…"
msgstr ""
msgid "Removing this group also removes all child projects, including archived projects, and their resources."
msgstr ""
......@@ -22337,12 +22282,6 @@ msgstr ""
msgid "Seat Link is disabled, and cannot be configured through this form."
msgstr ""
msgid "Seats currently in use"
msgstr ""
msgid "Seats in license"
msgstr ""
msgid "Secondary"
msgstr ""
......@@ -25954,9 +25893,6 @@ msgstr ""
msgid "This is the highest peak of users on your installation since the license started."
msgstr ""
msgid "This is the maximum number of users that have existed at the same time since the license started. This is the minimum number of seats you will need to buy when you renew your license."
msgstr ""
msgid "This is the number of currently active users on your installation, and this is the minimum number you need to purchase when you renew your license."
msgstr ""
......@@ -27869,9 +27805,6 @@ msgstr ""
msgid "Users or groups set as approvers in the project's or merge request's settings."
msgstr ""
msgid "Users outside of license"
msgstr ""
msgid "Users over License:"
msgstr ""
......@@ -27887,9 +27820,6 @@ msgstr ""
msgid "Users with a Guest role or those who don't belong to a Project or Group will not use a seat from your license."
msgstr ""
msgid "Users with a Guest role or those who don't belong to any projects or groups don't count towards seats in use."
msgstr ""
msgid "UsersSelect|%{name} + %{length} more"
msgstr ""
......@@ -27929,9 +27859,6 @@ msgstr ""
msgid "Validations failed."
msgstr ""
msgid "Validity"
msgstr ""
msgid "Value"
msgstr ""
......
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