Commit 772911ad authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'paginate-license-management' into 'master'

Paginate license management

See merge request gitlab-org/gitlab-ee!10983
parents 8346651f bd685e53
<script>
import { GlPaginatedList } from '@gitlab/ui';
import { PREV, NEXT } from '~/vue_shared/components/pagination/constants';
export default {
components: {
GlPaginatedList,
},
labels: {
prev: PREV,
next: NEXT,
},
};
</script>
<template>
<gl-paginated-list
v-bind="$attrs"
:prev-text="$options.labels.prev"
:next-text="$options.labels.next"
>
<!-- proxy the slots -->
<template #header>
<slot name="header"></slot>
</template>
<template #subheader>
<slot name="subheader"></slot>
</template>
<template #default="{ listItem, query }">
<slot :listItem="listItem" :query="query"></slot>
</template>
</gl-paginated-list>
</template>
......@@ -262,6 +262,8 @@ To approve or blacklist a license:
navigate to the project's **Settings > CI/CD** and expand the
**License Management** section.
1. Click the **Add a license** button.
![License Management Add License](img/license_management_add_license.png)
1. In the **License name** dropdown, either:
- Select one of the available licenses. You can search for licenses in the field
at the top of the list.
......@@ -270,8 +272,22 @@ To approve or blacklist a license:
1. Select the **Approve** or **Blacklist** radio button to approve or blacklist respectively
the selected license.
To modify an existing license:
1. In the **License Management** list, click the **Approved/Declined** dropdown to change it to the desired status.
![License Management Settings](img/license_management_settings.png)
Searching for Licenses:
1. Use the **Search** box to search for a specific license.
![License Management Search](img/license_management_search.png)
## License Management report under pipelines
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5491)
......
......@@ -55,7 +55,7 @@ export default {
};
</script>
<template>
<li class="list-group-item">
<div>
<issue-status-icon :status="status" class="float-left append-right-default" />
<span class="js-license-name">{{ license.name }}</span>
<div class="float-right">
......@@ -85,5 +85,5 @@ export default {
</button>
</div>
</div>
</li>
</div>
</template>
......@@ -5,6 +5,7 @@ import { s__ } from '~/locale';
import AddLicenseForm from './components/add_license_form.vue';
import LicenseManagementRow from './components/license_management_row.vue';
import DeleteConfirmationModal from './components/delete_confirmation_modal.vue';
import PaginatedList from '~/vue_shared/components/paginated_list.vue';
import createStore from './store/index';
const store = createStore();
......@@ -17,6 +18,7 @@ export default {
LicenseManagementRow,
GlButton,
GlLoadingIcon,
PaginatedList,
},
props: {
apiUrl: {
......@@ -28,9 +30,6 @@ export default {
return { formIsOpen: false };
},
store,
emptyMessage: s__(
'LicenseManagement|There are currently no approved or blacklisted licenses in this project.',
),
computed: {
...mapState(['managedLicenses', 'isLoadingManagedLicenses']),
},
......@@ -49,30 +48,49 @@ export default {
this.formIsOpen = false;
},
},
emptyMessage: s__(
'LicenseManagement|There are currently no approved or blacklisted licenses in this project.',
),
emptySearchMessage: s__(
'LicenseManagement|There are currently no approved or blacklisted licenses that match in this project.',
),
};
</script>
<template>
<div class="license-management">
<gl-loading-icon v-if="isLoadingManagedLicenses" />
<div v-else class="license-management">
<delete-confirmation-modal />
<gl-loading-icon v-if="isLoadingManagedLicenses" />
<ul v-if="managedLicenses.length" class="list-group list-group-flush">
<license-management-row
v-for="license in managedLicenses"
:key="license.name"
:license="license"
/>
</ul>
<div v-else class="bs-callout bs-callout-warning">{{ $options.emptyMessage }}</div>
<div class="prepend-top-default">
<add-license-form
v-if="formIsOpen"
:managed-licenses="managedLicenses"
@addLicense="setLicenseApproval"
@closeForm="closeAddLicenseForm"
/>
<gl-button v-else class="js-open-form" variant="default" @click="openAddLicenseForm">
{{ s__('LicenseManagement|Add a license') }}
</gl-button>
</div>
<paginated-list
:list="managedLicenses"
:empty-search-message="$options.emptySearchMessage"
:empty-message="$options.emptyMessage"
filter="name"
>
<template #header>
<gl-button
class="js-open-form order-1"
:disabled="formIsOpen"
variant="success"
@click="openAddLicenseForm"
>
{{ s__('LicenseManagement|Add a license') }}
</gl-button>
</template>
<template #subheader>
<div v-if="formIsOpen" class="prepend-top-default append-bottom-default">
<add-license-form
:managed-licenses="managedLicenses"
@addLicense="setLicenseApproval"
@closeForm="closeAddLicenseForm"
/>
</div>
</template>
<template #default="{ listItem }">
<license-management-row :license="listItem" />
</template>
</paginated-list>
</div>
</template>
import * as types from './mutation_types';
import { normalizeLicense, byLicenseNameComparator } from './utils';
import { normalizeLicense } from './utils';
export default {
[types.SET_LICENSE_IN_MODAL](state, license) {
......@@ -17,7 +17,7 @@ export default {
},
[types.RECEIVE_LOAD_MANAGED_LICENSES](state, licenses = []) {
const managedLicenses = licenses.map(normalizeLicense).sort(byLicenseNameComparator);
const managedLicenses = licenses.map(normalizeLicense).reverse();
Object.assign(state, {
managedLicenses,
......
---
title: Paginate license management
merge_request: 10983
author:
type: added
......@@ -132,8 +132,8 @@ describe('LicenseManagementRow', () => {
});
describe('template', () => {
it('renders component container element with class `list-group-item`', () => {
expect(vm.$el.classList.contains('list-group-item')).toBe(true);
it('renders component container element as a div', () => {
expect(vm.$el.tagName).toBe('DIV');
});
it('renders status icon', () => {
......
......@@ -9,25 +9,28 @@ import { approvedLicense, blacklistedLicense } from 'ee_spec/license_management/
describe('LicenseManagement', () => {
const Component = Vue.extend(LicenseManagement);
const apiUrl = `${TEST_HOST}/license_management`;
let vm;
let store;
let actions;
beforeEach(() => {
actions = {
setAPISettings: jasmine.createSpy('setAPISettings').and.callFake(() => {}),
loadManagedLicenses: jasmine.createSpy('loadManagedLicenses').and.callFake(() => {}),
};
store = new Vuex.Store({
const initVue = (mergeState = {}) => {
const store = new Vuex.Store({
state: {
managedLicenses: [approvedLicense, blacklistedLicense],
currentLicenseInModal: approvedLicense,
isLoadingManagedLicenses: true,
...mergeState,
},
actions,
});
vm = mountComponentWithStore(Component, { props: { apiUrl }, store });
return mountComponentWithStore(Component, { props: { apiUrl }, store });
};
beforeEach(() => {
actions = {
setAPISettings: jasmine.createSpy('setAPISettings').and.callFake(() => {}),
loadManagedLicenses: jasmine.createSpy('loadManagedLicenses').and.callFake(() => {}),
};
});
afterEach(() => {
......@@ -35,7 +38,8 @@ describe('LicenseManagement', () => {
});
describe('License Form', () => {
it('should render the form if the form is open', done => {
it('should render the form if the form is open and disable the form button', done => {
vm = initVue({ isLoadingManagedLicenses: false });
vm.formIsOpen = true;
return Vue.nextTick()
......@@ -45,13 +49,14 @@ describe('LicenseManagement', () => {
expect(formEl).not.toBeNull();
const buttonEl = vm.$el.querySelector('.js-open-form');
expect(buttonEl).toBeNull();
expect(buttonEl).toHaveClass('disabled');
done();
})
.catch(done.fail);
});
it('should render the button if the form is closed', done => {
vm = initVue({ isLoadingManagedLicenses: false });
vm.formIsOpen = false;
return Vue.nextTick()
......@@ -67,30 +72,37 @@ describe('LicenseManagement', () => {
.catch(done.fail);
});
it('clicking the Add a license button opens the form', () => {
const linkEl = vm.$el.querySelector('.js-open-form');
it('clicking the Add a license button opens the form', done => {
vm = initVue({ isLoadingManagedLicenses: false });
expect(vm.formIsOpen).toBe(false);
return Vue.nextTick()
.then(() => {
const linkEl = vm.$el.querySelector('.js-open-form');
linkEl.click();
expect(vm.formIsOpen).toBe(false);
expect(vm.formIsOpen).toBe(true);
linkEl.click();
expect(vm.formIsOpen).toBe(true);
done();
})
.catch(done.fail);
});
});
it('should render loading icon', done => {
store.replaceState({ ...store.state, isLoadingManagedLicenses: true });
vm = initVue({ isLoadingManagedLicenses: true });
return Vue.nextTick()
.then(() => {
expect(vm.$el.querySelector('.loading-container')).not.toBeNull();
expect(vm.$el.classList.contains('loading-container')).toEqual(true);
done();
})
.catch(done.fail);
});
it('should render callout if no licenses are managed', done => {
store.replaceState({ ...store.state, managedLicenses: [], isLoadingManagedLicenses: false });
vm = initVue({ managedLicenses: [], isLoadingManagedLicenses: false });
return Vue.nextTick()
.then(() => {
......@@ -104,7 +116,7 @@ describe('LicenseManagement', () => {
});
it('should render delete confirmation modal', done => {
store.replaceState({ ...store.state });
vm = initVue({ isLoadingManagedLicenses: false });
return Vue.nextTick()
.then(() => {
......@@ -115,7 +127,7 @@ describe('LicenseManagement', () => {
});
it('should render list of managed licenses', done => {
store.replaceState({ ...store.state, isLoadingManagedLicenses: false });
vm = initVue({ isLoadingManagedLicenses: false });
return Vue.nextTick()
.then(() => {
......@@ -127,18 +139,24 @@ describe('LicenseManagement', () => {
.catch(done.fail);
});
it('should set api settings after mount and init API calls', () =>
Vue.nextTick().then(() => {
expect(actions.setAPISettings).toHaveBeenCalledWith(
jasmine.any(Object),
{ apiUrlManageLicenses: apiUrl },
undefined,
);
expect(actions.loadManagedLicenses).toHaveBeenCalledWith(
jasmine.any(Object),
undefined,
undefined,
);
}));
it('should set api settings after mount and init API calls', done => {
vm = initVue();
return Vue.nextTick()
.then(() => {
expect(actions.setAPISettings).toHaveBeenCalledWith(
jasmine.any(Object),
{ apiUrlManageLicenses: apiUrl },
undefined,
);
expect(actions.loadManagedLicenses).toHaveBeenCalledWith(
jasmine.any(Object),
undefined,
undefined,
);
done();
})
.catch(done.fail);
});
});
......@@ -7826,6 +7826,9 @@ msgstr ""
msgid "LicenseManagement|There are currently no approved or blacklisted licenses in this project."
msgstr ""
msgid "LicenseManagement|There are currently no approved or blacklisted licenses that match in this project."
msgstr ""
msgid "LicenseManagement|This license already exists in this project."
msgstr ""
......
import PaginatedList from '~/vue_shared/components/paginated_list.vue';
import { PREV, NEXT } from '~/vue_shared/components/pagination/constants';
import { mount } from '@vue/test-utils';
describe('Pagination links component', () => {
let wrapper;
let glPaginatedList;
const template = `
<div class="slot" slot-scope="{ listItem }">
<span class="item">Item Name: {{listItem.id}}</span>
</div>
`;
const props = {
prevText: PREV,
nextText: NEXT,
};
beforeEach(() => {
wrapper = mount(PaginatedList, {
scopedSlots: {
default: template,
},
propsData: {
list: [{ id: 'foo' }, { id: 'bar' }],
props,
},
});
[glPaginatedList] = wrapper.vm.$children;
});
afterEach(() => {
wrapper.destroy();
});
describe('Paginated List Component', () => {
describe('props', () => {
// We test attrs and not props because we pass through to child component using v-bind:"$attrs"
it('should pass prevText to GitLab UI paginated list', () => {
expect(glPaginatedList.$attrs['prev-text']).toBe(props.prevText);
});
it('should pass nextText to GitLab UI paginated list', () => {
expect(glPaginatedList.$attrs['next-text']).toBe(props.nextText);
});
});
describe('rendering', () => {
it('it renders the gl-paginated-list', () => {
expect(wrapper.contains('ul.list-group')).toBe(true);
expect(wrapper.findAll('li.list-group-item').length).toBe(2);
});
});
});
});
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