Commit bd685e53 authored by Fernando's avatar Fernando

Paginate license management and add license search

First pass at license pagination

* Paginate license management client side
* Refactor license list into seperate component

Add string filtering to license names

* Add search input to query on license name

Add add license button

* Refactor add license button to be a slot

Clean up styles and button state logic

* Clean up alignment
* Disable button when dorpdown is open

Remove client side alphabetical sorting

* Let the databse return order by date

Refactor list to use row slot

Further abstract pagination list compnent

Finish refactor of paginated list

* Refactor component into generic paginated list component
* Add additional style tweaks + responsive classes

Run prettier

Update license_management_spec

Run Prettier

Add unit tests for paginated list component

* Refactor template to be valid html (li in ul)
* Add jest unit tests

Add additional unit tests

* Add unit tests around pagination and search states

Add unit tests for filter props

Pretty print, lint, and add changelog

Update po files

Update doc assets

* Update screenshots of license management page

Code review adjustments

* remove $props and access on object intannce
* clean up empty message copy for paginated list

Run prettier

Update POT File

Update paginated list default empty messages

* update emptyMessage and emptySearchMessage copy

first pass at code review changes

Update unit tests and underscore import

* Use older style underscore import
* Refactor async unit tests

Run linter, prettier, tweak unit test names

Refactor wrapper.destroy usage

Run prettier

Code review adjustments

* Update call to slice and remove usage of underscore
* Add test case for invalid filterKey and gracefully
fallback when invalid filterKey provided

Revert doc changes

* Revert doc image updates at doc team's request since
we are working towardsa single code base

Tweak spec description

Use paginated list from gitlab-ui

* Remove usage of paginated list from local repo
* Remove pagianted list component and specs

Update button order for licence management

Add gl-paginateion list wrapper

Add paginated list wrapper specs

Add spec and refactor markup

* Add paginated list specs
* Refactor license row markup to not include double li

Run prettier

Link to gitlab-ui artifacts job

update pot file

Fix broken licensen mangement row specs

Backport doc changes from CE to EE

Fix and run linter

Refactor component to use pagination constants

Refactor slots to use newer syntax

Update filter prop name

Resolve maintainer feedback

Set package to temp artificat

Refactor vue init

Remove unnecessary param
parent f6a5dda9
<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);
});
});
......@@ -7829,6 +7829,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