From 34fc882337f7f0f3c1408d86227f5dbb0bca0ac8 Mon Sep 17 00:00:00 2001 From: Darren Satkunas Date: Tue, 30 Mar 2021 10:55:03 -0400 Subject: [PATCH] feature(admin(vue,js)): add tenants to configuration ui, implements #6005 --- .../src/views/Configuration/_router/index.js | 4 +- .../root/src/views/Configuration/index.vue | 3 +- .../src/views/Configuration/tenants/_api.js | 45 +++ .../tenants/_components/TheForm.vue | 89 +++++ .../tenants/_components/TheList.vue | 304 ++++++++++++++++++ .../tenants/_components/TheView.js | 26 ++ .../tenants/_components/index.js | 20 ++ .../tenants/_composables/useCollection.js | 84 +++++ .../views/Configuration/tenants/_router.js | 52 +++ .../src/views/Configuration/tenants/_store.js | 134 ++++++++ .../src/views/Configuration/tenants/config.js | 62 ++++ .../src/views/Configuration/tenants/schema.js | 43 +++ 12 files changed, 864 insertions(+), 2 deletions(-) create mode 100644 html/pfappserver/root/src/views/Configuration/tenants/_api.js create mode 100644 html/pfappserver/root/src/views/Configuration/tenants/_components/TheForm.vue create mode 100644 html/pfappserver/root/src/views/Configuration/tenants/_components/TheList.vue create mode 100644 html/pfappserver/root/src/views/Configuration/tenants/_components/TheView.js create mode 100644 html/pfappserver/root/src/views/Configuration/tenants/_components/index.js create mode 100644 html/pfappserver/root/src/views/Configuration/tenants/_composables/useCollection.js create mode 100644 html/pfappserver/root/src/views/Configuration/tenants/_router.js create mode 100644 html/pfappserver/root/src/views/Configuration/tenants/_store.js create mode 100644 html/pfappserver/root/src/views/Configuration/tenants/config.js create mode 100644 html/pfappserver/root/src/views/Configuration/tenants/schema.js diff --git a/html/pfappserver/root/src/views/Configuration/_router/index.js b/html/pfappserver/root/src/views/Configuration/_router/index.js index 2222c8645149..88b7bd4918d5 100644 --- a/html/pfappserver/root/src/views/Configuration/_router/index.js +++ b/html/pfappserver/root/src/views/Configuration/_router/index.js @@ -59,6 +59,7 @@ import ActiveActiveRoutes from '../activeActive/_router' import RadiusRoutes from '../radius/_router' import DnsRoutes from '../dns/_router' import AdminRolesRoutes from '../adminRoles/_router' +import TenantsRoutes from '../tenants/_router' import store from '@/store' import BasesStoreModule from '../bases/_store' @@ -168,7 +169,8 @@ const route = { ...RadiusRoutes, ...DnsRoutes, ...AdminRolesRoutes, - ...SslCertificatesRoutes + ...SslCertificatesRoutes, + ...TenantsRoutes ] } diff --git a/html/pfappserver/root/src/views/Configuration/index.vue b/html/pfappserver/root/src/views/Configuration/index.vue index 69fc83b70120..e5f9137a2313 100644 --- a/html/pfappserver/root/src/views/Configuration/index.vue +++ b/html/pfappserver/root/src/views/Configuration/index.vue @@ -169,7 +169,8 @@ export default { }, { name: this.$i18n.t('DNS Configuration'), path: '/configuration/dns' }, { name: this.$i18n.t('Admin Access'), path: '/configuration/admin_roles' }, - { name: this.$i18n.t('SSL Certificates'), path: '/configuration/certificates' } + { name: this.$i18n.t('SSL Certificates'), path: '/configuration/certificates' }, + { name: this.$i18n.t('Tenants'), path: '/configuration/tenants' } ] } ] diff --git a/html/pfappserver/root/src/views/Configuration/tenants/_api.js b/html/pfappserver/root/src/views/Configuration/tenants/_api.js new file mode 100644 index 000000000000..6f4bfb569450 --- /dev/null +++ b/html/pfappserver/root/src/views/Configuration/tenants/_api.js @@ -0,0 +1,45 @@ +import apiCall from '@/utils/api' + +export default { + list: params => { + return apiCall.get('tenants', { params }).then(response => { + return response.data + }) + }, + listOptions: () => { + return apiCall.options('tenants').then(response => { + return response.data + }) + }, + item: id => { + return apiCall.get(['tenant', id]).then(response => { + return response.data.item + }) + }, + itemOptions: id => { + return apiCall.options(['tenant', id]).then(response => { + return response.data + }) + }, + create: data => { + return apiCall.post('tenants', data).then(response => { + return response.data + }) + }, + update: data => { + return apiCall.patch(['tenant', data.id], data).then(response => { + return response.data + }) + }, + delete: id => { + return apiCall.delete(['tenant', id]) + }, + reassign: data => { + return apiCall.patch(['tenant', data.from, 'reassign'], { id: data.to }) + }, + search: data => { + return apiCall.post('tenants/search', data).then(response => { + return response.data + }) + } +} diff --git a/html/pfappserver/root/src/views/Configuration/tenants/_components/TheForm.vue b/html/pfappserver/root/src/views/Configuration/tenants/_components/TheForm.vue new file mode 100644 index 000000000000..33ad2a4795bb --- /dev/null +++ b/html/pfappserver/root/src/views/Configuration/tenants/_components/TheForm.vue @@ -0,0 +1,89 @@ + + diff --git a/html/pfappserver/root/src/views/Configuration/tenants/_components/TheList.vue b/html/pfappserver/root/src/views/Configuration/tenants/_components/TheList.vue new file mode 100644 index 000000000000..58f2c0f34e39 --- /dev/null +++ b/html/pfappserver/root/src/views/Configuration/tenants/_components/TheList.vue @@ -0,0 +1,304 @@ + + + + \ No newline at end of file diff --git a/html/pfappserver/root/src/views/Configuration/tenants/_components/TheView.js b/html/pfappserver/root/src/views/Configuration/tenants/_components/TheView.js new file mode 100644 index 000000000000..83c6098d21fa --- /dev/null +++ b/html/pfappserver/root/src/views/Configuration/tenants/_components/TheView.js @@ -0,0 +1,26 @@ +import { + BaseView, + + FormButtonBar, + TheForm +} from './' + +const components = { + FormButtonBar, + TheForm +} + +import { useViewCollectionItem, useViewCollectionItemProps as props } from '../../_composables/useViewCollectionItem' + +import collection from '../_composables/useCollection' +const setup = (props, context) => useViewCollectionItem(collection, props, context) + +// @vue/component +export default { + name: 'the-view', + extends: BaseView, + inheritAttrs: false, + components, + props, + setup +} diff --git a/html/pfappserver/root/src/views/Configuration/tenants/_components/index.js b/html/pfappserver/root/src/views/Configuration/tenants/_components/index.js new file mode 100644 index 000000000000..25ec4b39fa02 --- /dev/null +++ b/html/pfappserver/root/src/views/Configuration/tenants/_components/index.js @@ -0,0 +1,20 @@ +import { BaseViewCollectionItem } from '../../_components/new/' +import { + BaseFormButtonBar, + BaseFormGroupInput +} from '@/components/new/' +import TheForm from './TheForm' +import TheView from './TheView' + +export { + BaseFormButtonBar as FormButtonBar, + + BaseFormGroupInput as FormGroupIdentifier, + BaseFormGroupInput as FormGroupName, + BaseFormGroupInput as FormGroupDomainName, + BaseFormGroupInput as FormGroupPortalDomainName, + + BaseViewCollectionItem as BaseView, + TheForm, + TheView +} diff --git a/html/pfappserver/root/src/views/Configuration/tenants/_composables/useCollection.js b/html/pfappserver/root/src/views/Configuration/tenants/_composables/useCollection.js new file mode 100644 index 000000000000..8d4575283e53 --- /dev/null +++ b/html/pfappserver/root/src/views/Configuration/tenants/_composables/useCollection.js @@ -0,0 +1,84 @@ +import { computed, toRefs } from '@vue/composition-api' +import i18n from '@/utils/locale' +import { + defaultsFromMeta as useItemDefaults +} from '../../_config/' + +const useItemTitle = (props) => { + const { + id, + isClone, + isNew + } = toRefs(props) + return computed(() => { + switch (true) { + case !isNew.value && !isClone.value: + return i18n.t('Tenant {id}', { id: id.value }) + case isClone.value: + return i18n.t('Clone Tenant {id}', { id: id.value }) + default: + return i18n.t('New Tenant') + } + }) +} + +export const useRouter = (props, context, form) => { + const { + id + } = toRefs(props) + const { root: { $router } = {} } = context + return { + goToCollection: () => $router.push({ name: 'tenants' }), + goToItem: (_id) => $router.push({ name: 'tenant', params: { id: _id || form.value.id || id.value } }) + .catch(e => { if (e.name !== "NavigationDuplicated") throw e }), + goToClone: () => $router.push({ name: 'cloneTenant', params: { id: id.value } }), + } +} + +const useStore = (props, context, form) => { + const { + id, + isClone + } = toRefs(props) + const { root: { $store } = {} } = context + return { + isLoading: computed(() => $store.getters['$_tenants/isLoading']), + getOptions: () => $store.dispatch('$_tenants/options'), + createItem: () => $store.dispatch('$_tenants/createTenant', form.value), + deleteItem: () => $store.dispatch('$_tenants/deleteTenant', id.value), + getItem: () => $store.dispatch('$_tenants/getTenant', id.value).then(item => { + if (isClone.value) { + item.name = `${item.name}-${i18n.t('copy')}` + item.not_deletable = false + } + return item + }), + updateItem: () => $store.dispatch('$_tenants/updateTenant', form.value), + } +} + +import { + useSearch as useConfigurationSearch +} from '@/views/Configuration/_composables/useSearch' +import api from '../_api' +import { + columns, + fields +} from '../config' +export const useSearch = (props, context, options) => { + return useConfigurationSearch(api, { + name: 'tenants', // localStorage prefix + columns, + fields, + sortBy: 'id', + ...options, + }) +} + +export default { + useItemDefaults, + useItemTitle, + useRouter, + useStore, + useSearch +} diff --git a/html/pfappserver/root/src/views/Configuration/tenants/_router.js b/html/pfappserver/root/src/views/Configuration/tenants/_router.js new file mode 100644 index 000000000000..89103aff0316 --- /dev/null +++ b/html/pfappserver/root/src/views/Configuration/tenants/_router.js @@ -0,0 +1,52 @@ +import store from '@/store' +import StoreModule from './_store' + +const TheList = () => import(/* webpackChunkName: "Configuration" */ './_components/TheList') +const TheView = () => import(/* webpackChunkName: "Configuration" */ './_components/TheView') + +export const beforeEnter = (to, from, next = () => {}) => { + if (!store.state.$_tenants) + store.registerModule('$_tenants', StoreModule) + next() +} + +export default [ + { + path: 'tenants', + name: 'tenants', + component: TheList, + props: (route) => ({ query: route.query.query }), + beforeEnter + }, + { + path: 'tenants/new', + name: 'newTenant', + component: TheView, + props: (route) => ({ isNew: true }), + beforeEnter + }, + { + path: 'tenant/:id', + name: 'tenant', + component: TheView, + props: (route) => ({ id: route.params.id }), + beforeEnter: (to, from, next) => { + beforeEnter() + store.dispatch('$_tenants/getTenant', to.params.id).then(() => { + next() + }) + } + }, + { + path: 'tenant/:id/clone', + name: 'cloneTenant', + component: TheView, + props: (route) => ({ id: route.params.id, isClone: true }), + beforeEnter: (to, from, next) => { + beforeEnter() + store.dispatch('$_tenants/getTenant', to.params.id).then(() => { + next() + }) + } + } +] diff --git a/html/pfappserver/root/src/views/Configuration/tenants/_store.js b/html/pfappserver/root/src/views/Configuration/tenants/_store.js new file mode 100644 index 000000000000..ed13462745a7 --- /dev/null +++ b/html/pfappserver/root/src/views/Configuration/tenants/_store.js @@ -0,0 +1,134 @@ +/** +* "$_tenants" store module +*/ +import Vue from 'vue' +import api from './_api' + +const types = { + LOADING: 'loading', + DELETING: 'deleting', + SUCCESS: 'success', + ERROR: 'error' +} + +// Default values +const state = () => { + return { + cache: {}, // items details + message: '', + itemStatus: '' + } +} + +const getters = { + isWaiting: state => [types.LOADING, types.DELETING].includes(state.itemStatus), + isLoading: state => state.itemStatus === types.LOADING +} + +const actions = { + all: () => { + const params = { + sort: 'id', + fields: ['id'].join(','), + limit: 1000 + } + return api.list(params).then(response => { + return response.items + }) + }, + options: ({ commit }, id) => { + commit('ITEM_REQUEST') + if (id) { + return api.itemOptions(id).then(response => { + commit('ITEM_SUCCESS') + return response + }).catch((err) => { + commit('ITEM_ERROR', err.response) + throw err + }) + } else { + return api.listOptions().then(response => { + commit('ITEM_SUCCESS') + return response + }).catch((err) => { + commit('ITEM_ERROR', err.response) + throw err + }) + } + }, + getTenant: ({ state, commit }, id) => { + if (state.cache[id]) + return Promise.resolve(state.cache[id]) + commit('ITEM_REQUEST') + return api.item(id).then(item => { + commit('ITEM_REPLACED', item) + return Promise.resolve(state.cache[id]) + }).catch((err) => { + commit('ITEM_ERROR', err.response) + throw err + }) + }, + createTenant: ({ state, commit }, data) => { + commit('ITEM_REQUEST') + return api.create(data).then(response => { + const { id } = response + commit('ITEM_REPLACED', { ...data, id }) + return Promise.resolve(state.cache[id]) + }).catch(err => { + commit('ITEM_ERROR', err.response) + throw err + }) + }, + updateTenant: ({ state, commit }, data) => { + commit('ITEM_REQUEST') + return api.update(data).then(() => { + commit('ITEM_REPLACED', data) + return Promise.resolve(state.cache[data.id]) + }).catch(err => { + commit('ITEM_ERROR', err.response) + throw err + }) + }, + deleteTenant: ({ commit }, data) => { + commit('ITEM_REQUEST', types.DELETING) + return api.delete(data).then(response => { + commit('ITEM_DESTROYED', data) + return response + }).catch(err => { + commit('ITEM_ERROR', err.response) + throw err + }) + } +} + +const mutations = { + ITEM_REQUEST: (state, type) => { + state.itemStatus = type || types.LOADING + state.message = '' + }, + ITEM_REPLACED: (state, data) => { + state.itemStatus = types.SUCCESS + Vue.set(state.cache, data.id, data) + }, + ITEM_DESTROYED: (state, id) => { + state.itemStatus = types.SUCCESS + Vue.set(state.cache, id, undefined) + }, + ITEM_ERROR: (state, response) => { + state.itemStatus = types.ERROR + if (response && response.data) { + state.message = response.data.message + } + }, + ITEM_SUCCESS: (state) => { + state.itemStatus = types.SUCCESS + } +} + +export default { + namespaced: true, + state, + getters, + actions, + mutations +} diff --git a/html/pfappserver/root/src/views/Configuration/tenants/config.js b/html/pfappserver/root/src/views/Configuration/tenants/config.js new file mode 100644 index 000000000000..50043fa81729 --- /dev/null +++ b/html/pfappserver/root/src/views/Configuration/tenants/config.js @@ -0,0 +1,62 @@ +import i18n from '@/utils/locale' +import { pfSearchConditionType as conditionType } from '@/globals/pfSearch' + +export const columns = [ + { + key: 'id', + class: 'text-nowrap', + label: i18n.t('Identifier'), + required: true, + sortable: true, + visible: true, + searchable: true + }, + { + key: 'name', + label: i18n.t('Name'), + sortable: true, + visible: true, + searchable: true + }, + { + key: 'domain_name', + label: i18n.t('Domain name'), + sortable: true, + visible: true, + searchable: true + }, + { + key: 'portal_domain_name', + label: i18n.t('Portal domain name'), + sortable: true, + visible: true, + searchable: true + }, + { + key: 'buttons', + locked: true + } +] + +export const fields = [ + { + value: 'id', + text: i18n.t('Identifier'), + types: [conditionType.SUBSTRING] + }, + { + value: 'name', + text: i18n.t('Name'), + types: [conditionType.SUBSTRING] + }, + { + value: 'domain_name', + text: i18n.t('Domain name'), + types: [conditionType.SUBSTRING] + }, + { + value: 'portal_domain_name', + text: i18n.t('Portal domain name'), + types: [conditionType.SUBSTRING] + } +] diff --git a/html/pfappserver/root/src/views/Configuration/tenants/schema.js b/html/pfappserver/root/src/views/Configuration/tenants/schema.js new file mode 100644 index 000000000000..7f4889be6888 --- /dev/null +++ b/html/pfappserver/root/src/views/Configuration/tenants/schema.js @@ -0,0 +1,43 @@ +import store from '@/store' +import i18n from '@/utils/locale' +import yup from '@/utils/yup' + +yup.addMethod(yup.string, 'tenantNameNotExistsExceptIdentifier', function (exceptId = '', message) { + return this.test({ + name: 'tenantNameNotExistsExceptIdentifier', + message: message || i18n.t('Name exists.'), + test: (value) => { + if (!value) return true + return store.dispatch('config/getTenants').then(response => { + return response.filter(tenant => + tenant.name.toLowerCase() === value.toLowerCase() + && tenant.id !== exceptId + ).length === 0 + }).catch(() => { + return true + }) + } + }) +}) + +export default (props) => { + const { + id, + isNew, + isClone + } = props + + return yup.object().shape({ + id: yup.string() + .nullable() + .required(i18n.t('Identifier required.')), + name: yup.string() + .nullable() + .required(i18n.t('Name required.')) + .tenantNameNotExistsExceptIdentifier((!isNew && !isClone) ? id : undefined, i18n.t('Name exists.')), + domain_name: yup.string().nullable(), + portal_domain_name: yup.string().nullable() + }) +} + +export { yup }