Skip to content
Snippets Groups Projects
Commit d1d63b17 authored by Nicolò Mezzopera's avatar Nicolò Mezzopera
Browse files

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
No related branches found
No related tags found
No related merge requests found
Showing
with 0 additions and 718 deletions
Loading
Loading
@@ -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);
},
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment