Commit d9e8fd00 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Search functionality

parent b9d62a20
......@@ -19,3 +19,21 @@ To build the image:
To run:
docker run --env-file docker-env -d -p 8081:8081 -i -t gms:latest
## Developer notes
Backend and frontend are 2 separate applications:
* the backend is the Maven application in the gms folder, based on Java and Spring Boot;
* the frontend is the npm application is the gms-ui folder, based on Vue.js.
The Maven application automatically packs the Vue.js products inside the final jar, however the frontend application can be tested isolatedly running `npm run serve` in order to take advantage of the npm autoreload functionalities.
By default http calls are mocked inside the Vue.js application.
In order to rely on real server calls edit the .env.development file in this way:
VUE_APP_API_CLIENT = 'server'
VUE_APP_API_BASE_URL = 'http://localhost:8081/gms/'
This assumes that your backend runs on 8081 port (with dev profile active, in order to enable the CORS policy) and the frontend runs on 8080 port.
First, do the login using the application running on the 8081 port, then you can access the frontend on the 8080.
<template>
<div id="app" v-if="model">
<TopMenu v-bind:user="model.user" />
<div class="container">
<Main />
</div>
<div id="loading" v-if="loading">
<div id="spinner-wrapper">
<b-spinner variant="primary" style="width: 3rem; height: 3rem;" label="Loading"></b-spinner>
</div>
<div id="app" v-if="model">
<TopMenu v-bind:user="model.user" />
<div class="container">
<Main v-if="page === 'main'" />
<GenericSearchResults v-if="page === 'search'" />
<UserSearchResult v-if="page === 'userSearch'" />
</div>
<div id="loading" v-if="loading">
<div id="spinner-wrapper">
<b-spinner variant="primary" style="width: 3rem; height: 3rem;" label="Loading"></b-spinner>
</div>
</div>
</div>
</template>
<script>
import TopMenu from './components/TopMenu.vue';
import Main from './components/Main.vue';
import { mapState } from 'vuex';
import GenericSearchResults from './components/GenericSearchResults.vue';
import UserSearchResult from './components/UserSearchResult.vue';
import {
mapState
} from 'vuex';
import client from 'api-client';
export default {
name: 'app',
components: {
TopMenu,
Main
Main,
GenericSearchResults,
UserSearchResult
},
computed: mapState({
model: state => state.model,
input: state => state.input,
loading: state => state.loading
loading: state => state.loading,
page: state => state.page
}),
mounted: function() {
var self = this;
document.addEventListener('apiError', function (event) {
document.addEventListener('apiError', function(event) {
self.$bvToast.toast(event.message, {
title: "Error",
variant: 'danger',
solid: true
});
});
document.addEventListener('loading', function (event) {
document.addEventListener('loading', function(event) {
self.$store.commit('setLoading', event.value);
});
......
{
"groups": [{
"id": "744e38e8f6d04e4e9418ae5f131c9b6b",
"name": "LBT",
"path": "744e38e8f6d04e4e9418ae5f131c9b6b"
}],
"permissions": [{
"userId": "4",
"groupId": "744e38e8f6d04e4e9418ae5f131c9b6b",
"permission": "VIEW_MEMBERS",
"groupPath": "744e38e8f6d04e4e9418ae5f131c9b6b"
}]
}
{
"items": [{
"id": "4",
"type": "USER",
"label": "Name Surname"
},
{
"id": "group_id",
"type": "GROUP",
"label": "Group 1"
}
],
"currentPage": 1,
"links": [1],
"totalItems": 2,
"pageSize": 20,
"totalPages": 1,
"hasPreviousPages": false,
"hasFollowingPages": false
}
......@@ -5,6 +5,8 @@ import membersPanel from './data/membersPanel';
import permissionsPanel from './data/permissionsPanel';
import searchUser from './data/searchUser';
import permission from './data/permission';
import search from './data/search';
import openUserSearchResult from './data/openUserSearchResult';
const fetch = (mockData, time = 0) => {
return new Promise((resolve) => {
......@@ -56,5 +58,11 @@ export default {
},
removeMember() {
return fetch(membersPanel, 500);
},
search() {
return fetch(search, 500);
},
openUserSearchResult() {
return fetch(openUserSearchResult, 500);
}
}
......@@ -275,5 +275,32 @@ export default {
'Accept': 'application/json',
}
});
},
search(input) {
let url = BASE_API_URL + 'search?query=' + input.genericSearch.filter +
'&page=' + input.genericSearch.paginatorPage + '&pageSize=' + input.genericSearch.paginatorPageSize;
return apiRequest(url, {
method: 'GET',
cache: 'no-cache',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
});
},
openUserSearchResult(userId) {
let url = BASE_API_URL + 'search/user/' + userId;
return apiRequest(url, {
method: 'GET',
cache: 'no-cache',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
});
}
};
<template>
<div class="mt-sm-3">
<div>
<p>Search results:</p>
<b-list-group v-for="item in model.genericSearchResults.items" v-bind:key="item.id">
<b-list-group-item href="#" v-on:click="openSearchResult(item)">
<span class="float-left">
<font-awesome-icon icon="folder" v-if="item.type === 'GROUP'"></font-awesome-icon>
<font-awesome-icon icon="user" v-if="item.type === 'USER'"></font-awesome-icon>
{{item.label}}
</span>
</b-list-group-item>
</b-list-group>
<Paginator :paginatedPanel="model.genericSearchResults" :onUpdate="updateSearchResults" />
</div>
</div>
</template>
<script>
import client from 'api-client';
import Paginator from './Paginator.vue';
import {
mapState
} from 'vuex';
export default {
name: 'GenericSearchResults',
components: {
Paginator
},
computed: mapState({
model: state => state.model,
input: state => state.input
}),
methods: {
openSearchResult: function(result) {
switch (result.type) {
case 'GROUP':
this.$store.commit('openGroup', result.id);
break;
case 'USER':
client.openUserSearchResult(result.id)
.then(model => {
this.$store.commit('displayUserSearchResults', [result.label, model]);
});
break;
}
},
updateSearchResults: function() {
}
}
}
</script>
......@@ -50,24 +50,9 @@ export default {
model: state => state.model,
input: state => state.input
}),
data: function() {
return {
groupFilter: ''
};
},
methods: {
openGroup: function(group) {
this.$store.state.input.selectedGroupId = group.groupId;
this.$store.state.input.searchFilter = null;
client.fetchGroupsTab(this.input)
.then(model => {
if (model.groupsPanel.items.length > 0) {
this.$store.commit('updateGroups', model);
} else {
// If there are no subgroups show the members panel
this.$store.commit('setTabIndex', '1');
}
});
this.$store.commit('openGroup', group.groupId);
},
openRenameGroupModal: function(group) {
this.$refs.renameGroupModal.openRenameGroupModal(group);
......
<template>
<div>
<b-navbar toggleable="lg" type="dark" variant="info">
<b-navbar-brand href="#" class="d-none d-md-block">Group Membership Service</b-navbar-brand>
<b-navbar-brand href="#" class="d-none d-md-block" v-on:click="showMainPage">Group Membership Service</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
......@@ -9,12 +9,10 @@
<!-- Right aligned nav items -->
<b-navbar-nav class="ml-auto">
<!--
<b-nav-form>
<b-form-input size="sm" class="mr-sm-2" placeholder="Search"></b-form-input>
<b-button size="sm" class="my-2 my-sm-0" type="submit">Search</b-button>
<b-form-input size="sm" class="mr-sm-2" placeholder="Search" v-model="input.genericSearch.filter"></b-form-input>
<b-button size="sm" class="my-2 my-sm-0" type="button" v-on:click="genericSearch()">Search</b-button>
</b-nav-form>
-->
<b-nav-item-dropdown :text="user" right v-if="user">
<b-dropdown-item href="logout">Logout</b-dropdown-item>
</b-nav-item-dropdown>
......@@ -25,10 +23,31 @@
</template>
<script>
import client from 'api-client';
import {
mapState
} from 'vuex';
export default {
name: 'TopMenu',
props: {
user: String
},
computed: mapState({
input: state => state.input,
}),
methods: {
showMainPage() {
this.$store.commit('showMainPage');
},
genericSearch() {
this.input.genericSearch.page = 1;
this.input.genericSearch.pageSize = 20;
client.search(this.input)
.then(results => {
this.$store.commit('displaySearchResults', results);
});
}
}
}
</script>
......
<template>
<div class="mt-sm-3" v-if="userLabel !== null">
<b-button variant="primary" class="float-right" v-on:click="back()">Back</b-button>
<h5>Results for <strong>{{userLabel}}</strong>:</h5>
<b-container class="mt-sm-5">
<b-row>
<b-col class="text-left">
<h5>Is member of</h5>
<div v-if="groups.length === 0">
No groups to show
</div>
<div v-if="groups.length > 0">
<ul>
<li v-for="group in groups" v-bind:key="group.groupId">
<a href="#" v-on:click="openGroup(group.groupId)">
{{group.groupCompleteName.join(' / ')}}
</a>
</li>
</ul>
</div>
</b-col>
<b-col v-if="permissions.length > 0">
<h5>Permissions</h5>
<table class="table table-striped">
<thead>
<tr>
<th>Group</th>
<th>Permission</th>
</tr>
</thead>
<tbody>
<tr v-for="(p, rowIndex) in permissions" v-bind:key="rowIndex">
<td>
<a href="#" v-on:click="openGroup(p.groupId)">
{{p.groupCompleteName.join(' / ')}}
</a>
</td>
<td>{{p.permission}}</td>
</tr>
</tbody>
</table>
</b-col>
</b-row>
</b-container>
</div>
</template>
<script>
import {
mapState
} from 'vuex';
export default {
name: 'UserSearchResult',
computed: mapState({
userLabel: state => state.model.userSearchResults.userLabel,
groups: state => state.model.userSearchResults.groups,
permissions: state => state.model.userSearchResults.permissions
}),
methods: {
back() {
this.$store.commit('displaySearchResults');
},
openGroup(groupId) {
this.$store.commit('openGroup', groupId);
}
}
}
</script>
......@@ -5,10 +5,10 @@ import store from './store.js'
import './plugins/bootstrap-vue'
import App from './App.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faTrash, faEdit, faSpinner } from '@fortawesome/free-solid-svg-icons'
import { faTrash, faEdit, faSpinner, faFolder, faUser } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
library.add(faTrash, faEdit, faSpinner);
library.add(faTrash, faEdit, faSpinner, faFolder, faUser);
Vue.component('font-awesome-icon', FontAwesomeIcon);
......
......@@ -2,6 +2,7 @@
import Vue from 'vue';
import Vuex from 'vuex';
import client from 'api-client';
Vue.use(Vuex);
......@@ -14,7 +15,13 @@ export default new Vuex.Store({
permissionsPanel: null,
membersPanel: null,
permission: null,
user: null
user: null,
genericSearchResults: [],
userSearchResults: {
userLabel: null,
groups: {},
permissions: {}
}
},
// values used to perform API calls
input: {
......@@ -23,9 +30,15 @@ export default new Vuex.Store({
paginatorPage: 1,
selectedTab: 'groups',
tabIndex: 0,
searchFilter: null
searchFilter: null,
genericSearch: {
filter: null,
paginatorPage: 1,
paginatorPageSize: 20
}
},
loading: false
loading: false,
page: 'main'
},
mutations: {
updateHomePageModel(state, model) {
......@@ -34,6 +47,22 @@ export default new Vuex.Store({
this.state.model.permission = model.permission;
this.state.model.user = model.user;
},
openGroup(state, groupId) {
let input = this.state.input;
input.selectedGroupId = groupId;
input.searchFilter = null;
client.fetchGroupsTab(input)
.then(model => {
if (model.groupsPanel.items.length > 0) {
this.commit('setTabIndex', 0);
this.commit('updateGroups', model);
} else {
// If there are no subgroups show the members panel
this.commit('setTabIndex', 1);
}
this.commit('showMainPage');
});
},
updateGroups(state, model) {
this.state.model.breadcrumbs = model.breadcrumbs;
this.state.model.groupsPanel = model.groupsPanel;
......@@ -54,6 +83,24 @@ export default new Vuex.Store({
},
setLoading(state, loading) {
this.state.loading = loading;
},
showMainPage(state) {
this.state.page = 'main';
},
displaySearchResults(state, results) {
this.state.page = 'search';
if (results) {
this.state.model.genericSearchResults = results;
}
},
updateSearchResults(state, results) {
this.state.model.genericSearchResults = results;
},
displayUserSearchResults(state, data) {
this.state.page = 'userSearch';
this.state.model.userSearchResults.userLabel = data[0];
this.state.model.userSearchResults.groups = data[1].groups;
this.state.model.userSearchResults.permissions = data[1].permissions;
}
},
getters: {
......
package it.inaf.ia2.gms.authn;
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
......@@ -23,6 +25,8 @@ import org.springframework.web.filter.CorsFilter;
@EnableOAuth2Sso
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final Logger LOG = LoggerFactory.getLogger(SecurityConfig.class);
@Autowired
private Environment env;
......@@ -92,6 +96,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Profile("dev")
public FilterRegistrationBean corsFilter() {
LOG.warn("Development profile active: CORS filter enabled");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration().applyPermitDefaultValues();
config.addAllowedMethod(HttpMethod.PUT);
......
package it.inaf.ia2.gms.model.response;
import java.util.List;
public class UserGroup {
private String groupId;
private List<String> groupCompleteName;
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public List<String> getGroupCompleteName() {
return groupCompleteName;
}
public void setGroupCompleteName(List<String> groupCompleteName) {
this.groupCompleteName = groupCompleteName;
}
}
package it.inaf.ia2.gms.model.response;
import it.inaf.ia2.gms.model.Permission;
public class UserPermission extends UserGroup {
private Permission permission;
public Permission getPermission() {
return permission;
}
public void setPermission(Permission permission) {
this.permission = permission;
}
}
package it.inaf.ia2.gms.model.response;
import it.inaf.ia2.gms.persistence.model.GroupEntity;
import it.inaf.ia2.gms.persistence.model.PermissionEntity;
import java.util.List;
public class UserSearchResponse {
private List<GroupEntity> groups;
private List<PermissionEntity> permissions;
private List<UserGroup> groups;
private List<UserPermission> permissions;
public List<GroupEntity> getGroups() {
public List<UserGroup> getGroups() {
return groups;
}
public void setGroups(List<GroupEntity> groups) {
public void setGroups(List<UserGroup> groups) {
this.groups = groups;
}
public List<PermissionEntity> getPermissions() {
public List<UserPermission> getPermissions() {
return permissions;
}
public void setPermissions(List<PermissionEntity> permissions) {
public void setPermissions(List<UserPermission> permissions) {
this.permissions = permissions;
}
}