diff --git a/gms-ui/package-lock.json b/gms-ui/package-lock.json
index 5730c23d018616a030206869255bd436f78ab210..f41f0cbc8b890d9a1afcf665f64a366ceeba8c3c 100644
--- a/gms-ui/package-lock.json
+++ b/gms-ui/package-lock.json
@@ -7219,15 +7219,15 @@
       }
     },
     "lodash": {
-      "version": "4.17.11",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
-      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+      "version": "4.17.15",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
       "dev": true
     },
     "lodash.defaultsdeep": {
-      "version": "4.6.0",
-      "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.0.tgz",
-      "integrity": "sha1-vsECT4WxvZbL6kBbI8FK1kQ6b4E=",
+      "version": "4.6.1",
+      "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz",
+      "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==",
       "dev": true
     },
     "lodash.kebabcase": {
@@ -11810,6 +11810,11 @@
       "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
       "dev": true
     },
+    "vuex": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.1.1.tgz",
+      "integrity": "sha512-ER5moSbLZuNSMBFnEBVGhQ1uCBNJslH9W/Dw2W7GZN23UQA69uapP5GTT9Vm8Trc0PzBSVt6LzF3hGjmv41xcg=="
+    },
     "watchpack": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
diff --git a/gms-ui/package.json b/gms-ui/package.json
index 5d7144d1992e0889e73a87f62c3101d483a54dce..f963d0c13fedab73af4b738b028ce644e54d8418 100644
--- a/gms-ui/package.json
+++ b/gms-ui/package.json
@@ -13,7 +13,8 @@
     "@fortawesome/vue-fontawesome": "^0.1.6",
     "bootstrap-vue": "^2.0.0-rc.19",
     "core-js": "^2.6.5",
-    "vue": "^2.6.10"
+    "vue": "^2.6.10",
+    "vuex": "^3.1.1"
   },
   "devDependencies": {
     "@babel/polyfill": "^7.4.4",
diff --git a/gms-ui/src/App.vue b/gms-ui/src/App.vue
index ef4aaf8b226eb57a3cbd620b3f1a465b1203e46c..c5635dbef5351ad33721a430cecb0fdec2c49c73 100644
--- a/gms-ui/src/App.vue
+++ b/gms-ui/src/App.vue
@@ -2,15 +2,15 @@
   <div id="app" v-if="model">
     <TopMenu v-bind:user="model.user" />
     <div class="container">
-      <Main v-bind:model="model" />
+      <Main />
     </div>
   </div>
 </template>
 
 <script>
-import TopMenu from './components/TopMenu.vue'
-import Main from './components/Main.vue'
-import client from 'api-client'
+import TopMenu from './components/TopMenu.vue';
+import Main from './components/Main.vue';
+import { mapState } from 'vuex';
 
 export default {
   name: 'app',
@@ -18,15 +18,11 @@ export default {
     TopMenu,
     Main
   },
-  data: function() {
-    return {
-      model: null
-    }
-  },
+  computed: mapState({
+    model: state => state.model
+  }),
   mounted: function() {
-    client
-      .fetchMainModel()
-      .then(model => this.$data.model = model);
+    this.$store.commit('fetchGroupsModel');
   }
 }
 </script>
diff --git a/gms-ui/src/api/server/index.js b/gms-ui/src/api/server/index.js
index 389498988f25284c54a7daa65696a3fc1258cfd0..1fc56867c964a2cb00b49a2da4ffb2ea37e995c4 100644
--- a/gms-ui/src/api/server/index.js
+++ b/gms-ui/src/api/server/index.js
@@ -1,8 +1,9 @@
 const BASE_API_URL = "http://localhost:8081/"
 
 export default {
-  fetchMainModel () {
-    return fetch(BASE_API_URL + 'groups?groupId=ROOT&tab=groups&paginatorPageSize=20&paginatorPage=1', {
+  fetchGroupsModel (input) {
+    let url = BASE_API_URL + 'groups?groupId=' + input.selectedGroupId + '&tab=' + input.selectedTab + '&paginatorPageSize=' + input.paginatorPageSize + '&paginatorPage=' + input.paginatorPage;
+    return fetch(url, {
       method: 'GET',
       cache: 'no-cache',
       credentials: 'include',
diff --git a/gms-ui/src/components/GroupsBreadcrumb.vue b/gms-ui/src/components/GroupsBreadcrumb.vue
index ff1d70b7dba2f35bb60f49b4ebc884fac82fff9e..b2458774e7ad2a5cc01a00a0c7f9adffdbe22fd5 100644
--- a/gms-ui/src/components/GroupsBreadcrumb.vue
+++ b/gms-ui/src/components/GroupsBreadcrumb.vue
@@ -2,51 +2,44 @@
   <nav aria-label="breadcrumb">
     <ol class="breadcrumb">
       <li class="breadcrumb-item" v-for="group in groups" v-bind:class="{ active: group.active }">
-        <a href="#" v-on:click="changeBreadcrumb(group.id)" v-if="!group.active">{{group.name}}</a>
-        <span v-if="group.active">{{group.name}}</span>
+        <a href="#" v-on:click="changeBreadcrumb(group.groupId)" v-if="!group.active">{{group.groupName}}</a>
+        <span v-if="group.active">{{group.groupName}}</span>
       </li>
     </ol>
   </nav>
 </template>
 
 <script>
-  function buildItems(values) {
-    let groups = [];
+import { mapState } from 'vuex';
 
-    groups.push({
-      name: 'Root',
-      id: null,
-      active: false
-    });
+function buildItems(values) {
+  let groups = [];
 
-    for(let i = 0; i < values.length; i++) {
-      let group = values[i];
-      group.active = false;
-      groups.push(group);
-    }
+  for(let i = 0; i < values.length; i++) {
+    let group = values[i];
+    group.active = false;
+    groups.push(group);
+  }
 
-    // Activate the last item
-    groups[groups.length - 1].active = true;
+  // Activate the last item
+  groups[groups.length - 1].active = true;
 
-    return groups;
-  }
+  return groups;
+}
 
-  export default {
-    name: 'GroupsBreadcrumb',
-    props: {
-      values: Array
-    },
-    data() {
-      return {
-        groups: buildItems(this.values)
-      };
-    },
-    methods: {
-      changeBreadcrumb: function(groupId) {
-        console.log('changeBreadcrumb', groupId);
-      }
+export default {
+  name: 'GroupsBreadcrumb',
+  computed: mapState({
+    model: state => state.model,
+    groups: state => buildItems(state.model.breadcrumbs)
+  }),
+  methods: {
+    changeBreadcrumb: function(groupId) {
+      this.$store.state.input.selectedGroupId = groupId;
+      this.$store.commit('fetchGroupsModel');
     }
   }
+}
 </script>
 
 <style scoped>
diff --git a/gms-ui/src/components/GroupsPanel.vue b/gms-ui/src/components/GroupsPanel.vue
index d2d2558775c02cb580d2495231aab1c6b06453bf..38fc4bcedb6eec49e1884f0a16a5e01c252ea6b9 100644
--- a/gms-ui/src/components/GroupsPanel.vue
+++ b/gms-ui/src/components/GroupsPanel.vue
@@ -7,9 +7,9 @@
     </b-row>
     <div id="groups-list">
       <b-list-group v-for="group in model.groupsPanel.items">
-        <b-list-group-item href="#">
+        <b-list-group-item href="#" v-on:click="openGroup(group)">
           <span class="float-left">{{group.name}}</span>
-          <span v-if="group.permission === 'ADMIN'" class="float-right">
+          <span v-if="group.permissions.includes('ADMIN')" class="float-right">
             <a href="#" v-on:click="renameGroup(group)">
               <font-awesome-icon icon="edit"></font-awesome-icon>
             </a>
@@ -38,7 +38,7 @@
               <label for="page-size">Page size:</label>
             </b-col>
             <b-col sm="6">
-              <b-form-select id="page-size" v-model="selectedPageSize" :options="pageSizeOptions" v-on:change="changePageSize"></b-form-select>
+              <b-form-select id="page-size" v-model="input.paginatorPageSize" :options="pageSizeOptions" v-on:change="changePageSize"></b-form-select>
             </b-col>
           </b-row>
         </b-container>
@@ -50,14 +50,16 @@
 </template>
 
 <script>
+import { mapState, mapActions } from 'vuex'
+
 export default {
   name: 'GroupsPanel',
-  props: {
-    model: Object
-  },
+  computed: mapState({
+    model: state => state.model,
+    input: state => state.input
+  }),
   data: function() {
     return {
-      selectedPageSize: this.model.groupsPanel.pageSize,
       pageSizeOptions: [
         { value: 20, text: "20" },
         { value: 50, text: "50" },
@@ -67,6 +69,10 @@ export default {
     };
   },
   methods: {
+    openGroup: function(group) {
+      this.$store.state.input.selectedGroupId = group.id;
+      this.$store.commit('fetchGroupsModel');
+    },
     renameGroup: function(group) {
       console.log('rename ' + group.id);
     },
diff --git a/gms-ui/src/components/Main.vue b/gms-ui/src/components/Main.vue
index 92f06146c6787acd784c580c684d230287162aa5..95aa9fb5532ed423159b01a2c419f2c421d783a3 100644
--- a/gms-ui/src/components/Main.vue
+++ b/gms-ui/src/components/Main.vue
@@ -1,14 +1,14 @@
 <template>
   <div>
-    <GroupsBreadcrumb v-bind:values="model.breadcrumbs" />
+    <GroupsBreadcrumb />
     <div class="">
-      <button type="button" class="btn btn-primary float-right" v-if="model.permission === 'ADMIN'">Add member</button>
-      <button type="button" class="btn btn-primary float-right" v-if="model.permission === 'ADMIN'">Add group</button>
-      <button type="button" class="btn btn-primary float-right" v-if="model.permission === 'PI'">Add collaborator</button>
+      <button type="button" class="btn btn-primary float-right" v-if="model.permissions.includes('ADMIN')">Add member</button>
+      <button type="button" class="btn btn-primary float-right" v-if="model.permissions.includes('ADMIN')">Add group</button>
+      <button type="button" class="btn btn-primary float-right" v-if="model.permissions.includes('ADMIN') || model.permissions.includes('MANAGE_MEMBERS')">Add collaborator</button>
     </div>
     <b-tabs content-class="mt-3">
-      <GroupsPanel v-bind:model="model" />
-      <MembersPanel v-bind:model="model" />
+      <GroupsPanel />
+      <MembersPanel />
       </b-tab>
     </b-tabs>
   </div>
@@ -18,6 +18,7 @@
 import GroupsBreadcrumb from './GroupsBreadcrumb.vue'
 import GroupsPanel from './GroupsPanel.vue'
 import MembersPanel from './MembersPanel.vue'
+import { mapState } from 'vuex';
 
 export default {
   name: 'Main',
@@ -26,9 +27,9 @@ export default {
     GroupsPanel,
     MembersPanel
   },
-  props: {
-    model: Object
-  },
+  computed: mapState({
+    model: state => state.model
+  }),
   methods: {
     addGroup: function() {
 
diff --git a/gms-ui/src/components/MembersPanel.vue b/gms-ui/src/components/MembersPanel.vue
index f11b9608449b486aac6e3353630cb77feeed09da..a1d4d3a203569a3b84ac2feb95d0d8ca3e9b8ee8 100644
--- a/gms-ui/src/components/MembersPanel.vue
+++ b/gms-ui/src/components/MembersPanel.vue
@@ -27,11 +27,13 @@
 </template>
 
 <script>
+import { mapState } from 'vuex';
+
 export default {
   name: 'MembersPanel',
-  props: {
-    model: Object
-  },
+  computed: mapState({
+    model: state => state.model
+  }),
   methods: {
     deleteMember: function(member) {
       console.log('deleteMember ' + member.id);
diff --git a/gms-ui/src/main.js b/gms-ui/src/main.js
index 52efbdcafbbb96c2cd44d87012fac6563a83da5a..9fb81c612d27ae5ef1f51dd5516bd8cab7609e85 100644
--- a/gms-ui/src/main.js
+++ b/gms-ui/src/main.js
@@ -1,6 +1,7 @@
 import '@babel/polyfill'
 import 'mutationobserver-shim'
 import Vue from 'vue'
+import store from './store.js'
 import './plugins/bootstrap-vue'
 import App from './App.vue'
 import { library } from '@fortawesome/fontawesome-svg-core'
@@ -15,4 +16,5 @@ Vue.config.productionTip = false;
 
 new Vue({
   render: h => h(App),
+  store
 }).$mount('#app');
diff --git a/gms-ui/src/store.js b/gms-ui/src/store.js
new file mode 100644
index 0000000000000000000000000000000000000000..dcf9906887a82740bc935afdcf71d9575664a89a
--- /dev/null
+++ b/gms-ui/src/store.js
@@ -0,0 +1,29 @@
+/* Vuex store, for centralized state management */
+
+import Vue from 'vue';
+import Vuex from 'vuex';
+import client from 'api-client'
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+  state: {
+    // values populated from API calls
+    model: null,
+    // values used to perform API calls
+    input: {
+      selectedGroupId: 'ROOT',
+      paginatorPageSize: 20,
+      paginatorPage: 1,
+      selectedTab: 'groups'
+    }
+  },
+  mutations: {
+    fetchGroupsModel(state) {
+      client.fetchGroupsModel(this.state.input)
+        .then(model => {
+          this.state.model = model;
+        });
+    }
+  }
+});