diff --git a/app/assets/app/cobalt/Form.component.js b/app/assets/app/cobalt/Form.component.js
new file mode 100644
index 0000000..1cee255
--- /dev/null
+++ b/app/assets/app/cobalt/Form.component.js
@@ -0,0 +1,198 @@
+import { Component } from '../../lib/vues6/vues6.js'
+import { utility } from '../service/Utility.service.js'
+import { location_service } from '../service/Location.service.js'
+import { resource_service } from '../service/Resource.service.js'
+import { action_service } from '../service/Action.service.js'
+
+const template = `
+
+
+ {{ title }}
+
+
+
+ {{ error_message }}
+ {{ other_message }}
+
+
+
+`
+
+const title_map = {
+ insert: 'Create',
+ update: 'Edit',
+ view: 'View',
+}
+
+export default class FormComponent extends Component {
+ static get selector() {
+ return 'cobalt-form'
+ }
+
+ static get template() {
+ return template
+ }
+
+ static get props() {
+ return ['resource', 'form_id', 'initial_mode']
+ }
+
+ definition = {}
+ data = {}
+ uuid = ''
+ title = ''
+ error_message = ''
+ other_message = ''
+
+ is_ready = false
+ mode = ''
+ id = ''
+
+ reset() {
+ this.definition = {}
+ this.data = {}
+ this.uuid = ''
+ this.title = ''
+ this.error_message = ''
+ this.other_message = ''
+ this.is_ready = false
+ }
+
+ async vue_on_create(internal = false) {
+ if ( !internal ) {
+ this.mode = this.initial_mode
+ this.id = this.form_id
+ this.resource_class = await resource_service.get(this.resource)
+ } else {
+ this.reset()
+ }
+
+ this.uuid = utility.uuid()
+ await this.load()
+ await this.init()
+ }
+
+ async init() {
+ for ( const field of this.definition.fields ) {
+ if ( field.type.startsWith('select.dynamic') ) {
+ field._options = field._options || field.options
+ const rsc = await resource_service.get(field._options.resource)
+
+ field.options = (await rsc.list()).map(item => {
+ return {
+ display: typeof field._options.display === 'function' ? field._options.display(item) : item[field._options.display || 'display'],
+ value: typeof field._options.value === 'function' ? field._options.value(item) : item[field._options.value || 'display'],
+ }
+ })
+ }
+
+ if ( field.type.endsWith('.multiple') ) {
+ this.data[field.field] = []
+ }
+ }
+
+ this.is_ready = true
+ this.$nextTick(() => {
+ if ( this.mode !== 'view' ) this.$refs.input[0].focus()
+ })
+ }
+
+ async load() {
+ this.definition = this.resource_class.form_definition
+ if (this.mode !== 'insert') {
+ this.data = await this.resource_class.get(this.id)
+ }
+
+ this.title = title_map[this.mode] + ' ' + this.resource_class.item
+ }
+
+ async on_create() {
+ this.id = this.data.id
+ this.mode = 'update'
+ await this.vue_on_create(true)
+ location_service.set_query(`mode=update&id=${this.id}`)
+ }
+
+ async save_click() {
+ if ( !this.validate() ) return
+ try {
+ if (this.mode === 'insert') {
+ this.data = await this.resource_class.create(this.data)
+ await this.on_create()
+ } else if (this.mode === 'update') {
+ await this.resource_class.update(this.id, this.data)
+ }
+
+ this.error_message = ''
+ this.other_message = `The ${this.resource_class.item} was saved.`
+ } catch (e) {
+ if ( e.response && e.response.data && e.response.data.message )
+ this.error_message = e.response.data.message
+ else this.error_message = 'An unknown error occurred while saving the form.'
+ }
+ }
+
+ validate() {
+ let valid = true
+ for ( const field of this.definition.fields ) {
+ if ( field.required && (!(field.field in this.data) || !this.data[field.field]) ) {
+ field.error = 'This field is required.'
+ valid = false
+ } else {
+ field.error = ''
+ }
+ }
+
+ this.$forceUpdate()
+
+ return valid
+ }
+
+ back() {
+ return action_service.perform({
+ text: '',
+ type: 'resource',
+ resource: this.resource,
+ action: 'list',
+ })
+ }
+
+ do_nothing() {}
+}
+
diff --git a/app/assets/app/cobalt/Listing.component.js b/app/assets/app/cobalt/Listing.component.js
index b897b50..460ebc9 100644
--- a/app/assets/app/cobalt/Listing.component.js
+++ b/app/assets/app/cobalt/Listing.component.js
@@ -1,21 +1,45 @@
import { Component } from '../../lib/vues6/vues6.js'
+import { action_service } from '../service/Action.service.js'
+import { message_service } from '../service/Message.service.js'
const template = `
-
{{ definition.title }}
+
+
{{ resource_class.plural }}
+
+
+
+
# |
{{ col.name }} |
+ |
-
+
{{ index + 1 }} |
- {{ col.field ? 'Yes' : 'No' }}
- {{ col.field in row ? row[col.field] : '-' }}
+ {{ col.renderer(row[col.field]) }}
+ {{ row[col.field] ? 'Yes' : 'No' }}
+ {{ col.field in row ? row[col.field] : '-' }}
+ |
+
+
|
@@ -26,5 +50,55 @@ const template = `
export default class ListingComponent extends Component {
static get selector() { return 'cobalt-listing' }
static get template() { return template }
- static get props() { return ['definition'] }
+ static get props() { return ['resource'] }
+
+ definition = {}
+ data = []
+ resource_class = {}
+
+ async vue_on_create() {
+ // Load the resource
+ const resource_mod = await import(`../resource/${this.resource}.resource.js`)
+ if ( !resource_mod )
+ throw new Error('Unable to load Cobalt listing resource.')
+
+ const rsc_name = this.resource.toLowerCase().replace(/\//g, '_')
+ if ( !resource_mod[rsc_name] )
+ throw new Error('Unable to extract resource object from module.')
+
+ this.resource_class = resource_mod[rsc_name]
+ await this.load()
+ }
+
+ async load() {
+ this.definition = this.resource_class.listing_definition
+ this.data = await this.resource_class.list()
+ }
+
+ async perform($event, action, row = undefined) {
+ if ( action.confirm ) {
+ message_service.modal({
+ title: 'Are you sure?',
+ message: `You are about to ${action.action}${row ? ' this '+this.resource_class.item : ''}. Do you want to continue?`,
+ buttons: [
+ {
+ text: 'Cancel',
+ type: 'close',
+ },
+ {
+ text: 'Continue',
+ class: ['btn', 'btn-primary'],
+ type: 'close',
+ on_click: async () => {
+ await action_service.perform({...action, resource: this.resource, ...(row ? {id: row.id} : {})})
+ await this.load()
+ },
+ },
+ ],
+ })
+ } else {
+ await action_service.perform({...action, resource: this.resource, ...(row ? {id: row.id} : {})})
+ await this.load()
+ }
+ }
}
diff --git a/app/assets/app/dash-components.js b/app/assets/app/dash-components.js
index e4cfa64..b670a57 100644
--- a/app/assets/app/dash-components.js
+++ b/app/assets/app/dash-components.js
@@ -5,6 +5,7 @@ import EditProfileComponent from './dash/profile/EditProfile.component.js'
import AppPasswordFormComponent from './dash/profile/form/AppPassword.component.js'
import ListingComponent from './cobalt/Listing.component.js'
+import FormComponent from './cobalt/Form.component.js'
const dash_components = {
SideBarComponent,
@@ -14,6 +15,7 @@ const dash_components = {
AppPasswordFormComponent,
ListingComponent,
+ FormComponent,
}
export { dash_components }
diff --git a/app/assets/app/dash/SideBar.component.js b/app/assets/app/dash/SideBar.component.js
index a50f479..374e876 100644
--- a/app/assets/app/dash/SideBar.component.js
+++ b/app/assets/app/dash/SideBar.component.js
@@ -37,17 +37,17 @@ export default class SideBarComponent extends Component {
{
text: 'Groups',
action: 'redirect',
- next: '/dash/groups',
+ next: '/dash/c/listing/ldap/Group',
},
{
text: 'LDAP Clients',
action: 'redirect',
- next: '/dash/ldap/clients',
+ next: '/dash/c/listing/ldap/Client',
},
{
text: 'SAML Service Providers',
action: 'redirect',
- next: '/dash/saml/service-providers',
+ next: '/dash/c/listing/saml/Provider',
},
{
text: 'Settings',
diff --git a/app/assets/app/dash/message/MessageContainer.component.js b/app/assets/app/dash/message/MessageContainer.component.js
index 5ddf1e2..78c548c 100644
--- a/app/assets/app/dash/message/MessageContainer.component.js
+++ b/app/assets/app/dash/message/MessageContainer.component.js
@@ -26,7 +26,7 @@ const template = `
aria-hidden="true"
ref="modal"
>
-