241 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			241 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| import jQuery from 'jquery'
 | |
| import riot from 'riot'
 | |
| 
 | |
| import 'riot-hot-reload'
 | |
| 
 | |
| import './edit.tag.pug'
 | |
| import './postgresql.tag.pug'
 | |
| import './help-edit.tag.pug'
 | |
| import './help-general.tag.pug'
 | |
| import './postgresqls.tag.pug'
 | |
| import './logs.tag.pug'
 | |
| import './new.tag.pug'
 | |
| import './status.tag.pug'
 | |
| import './app.tag.pug'
 | |
| import './restore.tag.pug'
 | |
| 
 | |
| Object.fromEntries = entries => entries.length === 0 ? {} : Object.assign(...entries.map(([k, v]) => ({[k]: v})))
 | |
| Object.mapValues = (o, f) => Object.fromEntries(Object.entries(o).map(([k, v]) => [k, f(v, k)]))
 | |
| Object.mapEntries = (o, f) => Object.fromEntries(Object.entries(o).map(f).filter(x => x))
 | |
| Object.filterEntries = (o, f) => Object.mapEntries(o, entry => f(entry) && entry)
 | |
| Object.filterValues = (o, f) => Object.filterEntries(o, ([key, value]) => f(value) && [key, value])
 | |
| 
 | |
| 
 | |
| const getDefaulting = (object, key, def) => (
 | |
|     object.hasOwnProperty(key) ? object[key] : def
 | |
| )
 | |
| 
 | |
| 
 | |
| const Dynamic = (options={}) => {
 | |
|     const instance = {
 | |
|         init: getDefaulting(options, 'init', () => ''),
 | |
|         refresh: getDefaulting(options, 'refresh', () => true),
 | |
|         update: getDefaulting(options, 'update', value => (instance.state = value, true)),
 | |
|         validState: getDefaulting(options, 'validState', state => (
 | |
|             state !== undefined &&
 | |
|       state !== null &&
 | |
|       typeof state === 'string' &&
 | |
|       state.length > 0
 | |
|         )),
 | |
| 
 | |
|         edit: event => (instance.update(event.target.value, instance, event), true),
 | |
|         valid: () => instance.validState(instance.state),
 | |
|     }
 | |
| 
 | |
|     instance.state = instance.init()
 | |
|     return instance
 | |
| }
 | |
| 
 | |
| 
 | |
| /*
 | |
| Dynamics manages a dynamic array whose elements are themselves Dynamic objects.
 | |
| 
 | |
| The default initializer builds an empty array as the initial state.
 | |
| 
 | |
| The "add" DOM event callback is provided to add a newly initialized Dynamic
 | |
| object to the end of the state array.  The Dynamic array item is initialized
 | |
| with the "itemInit" callback, which can be specified with a constructor option
 | |
| and defaults to creating a Dynamic with all default options.
 | |
| 
 | |
| The "remove" DOM event callback is provided to handle DOM events that should
 | |
| or remove a specific item.  For events on elements generated by iterating the
 | |
| state with an each= attribute, the event.item will be set to the correct value.
 | |
| 
 | |
| The refresh callback is forwarded to all constituent Dynamic objects.
 | |
| */
 | |
| const Dynamics = (options={}) => {
 | |
|     const instance = Object.assign(
 | |
|         Dynamic(
 | |
|             Object.assign(
 | |
|                 { init: () => [] },
 | |
|                 'refresh' in options
 | |
|                     ? { refresh: options.refresh }
 | |
|                     : undefined
 | |
|             )
 | |
|         ),
 | |
| 
 | |
|         {
 | |
|             itemInit: getDefaulting(options, 'itemInit', () =>
 | |
|                 Dynamic(
 | |
|                     'refresh' in options
 | |
|                         ? { refresh: options.refresh }
 | |
|                         : {}
 | |
|                 )
 | |
|             ),
 | |
|             itemValid: getDefaulting(options, 'itemValid', item => item.valid()),
 | |
|             validState: state => state.every(instance.itemValid),
 | |
|             update: () => true,
 | |
|             edit: () => true,
 | |
| 
 | |
|             add: _event => {
 | |
|                 instance.state.push(instance.itemInit())
 | |
|                 instance.refresh()
 | |
|                 return true
 | |
|             },
 | |
| 
 | |
|             remove: event => {
 | |
|                 instance.state.splice(instance.state.indexOf(event.item), 1)
 | |
|                 instance.refresh()
 | |
|                 return true
 | |
|             },
 | |
|         }
 | |
|     )
 | |
| 
 | |
|     Object.defineProperty(instance, 'valids', { get: () =>
 | |
|         instance.state.filter(instance.itemValid)
 | |
|     })
 | |
| 
 | |
|     return instance
 | |
| }
 | |
| 
 | |
| 
 | |
| /*
 | |
| DynamicSet manages a keyed collection of Dynamic objects.  The constructor
 | |
| receives an object mapping keys to initialization functions, and its state is a
 | |
| mapping from the same keys to Dynamics initialized using the corresponding
 | |
| key's initialization function.  A DynamicSet is valid when its constituent
 | |
| Dynamics are all simultaneously valid.  The refresh callback is forwarded to
 | |
| all constituent Dynamic objects.
 | |
| 
 | |
| Example:
 | |
| 
 | |
|   DynamicSet({
 | |
|     foo: undefined,
 | |
|     bar: () => 'baz',
 | |
|   })
 | |
| 
 | |
| This call would create a DynamicSet with two constituent dynamics in its state:
 | |
| one of them under the 'foo' key of the state object, built with the default
 | |
| Dynamic initializer, and another under the 'bar' key of the state object, whose
 | |
| state, in turn, would initially hold the value 'baz'.
 | |
| */
 | |
| const DynamicSet = (items, options={}) => Object.assign(
 | |
|     Dynamic(
 | |
|         Object.assign(
 | |
|             {
 | |
|                 init: () => Object.mapValues(items, init =>
 | |
|                     Dynamic(
 | |
|                         Object.assign(
 | |
|                             init ? { init: init } : undefined,
 | |
|                             'refresh' in options ? { refresh: options.refresh } : undefined
 | |
|                         )
 | |
|                     )
 | |
|                 ),
 | |
|             },
 | |
|             'refresh' in options
 | |
|                 ? { refresh: options.refresh }
 | |
|                 : undefined
 | |
|         )
 | |
|     ),
 | |
| 
 | |
|     {
 | |
|         items: items,
 | |
|         validState: state => Object.values(state).every(item => item.valid()),
 | |
|         edit: () => true,
 | |
|         update: () => true,
 | |
|     }
 | |
| )
 | |
| 
 | |
| 
 | |
| const delete_cluster = (namespace, clustername) => {
 | |
|     jQuery.confirm({
 | |
|         backgroundDismiss: true,
 | |
|         content: `
 | |
|       <p>
 | |
|         Are you sure you want to remove this PostgreSQL cluster?  If so,
 | |
|         please <strong>type the cluster name here
 | |
|         (<code>${namespace}/${clustername}</code>)</strong> and click the
 | |
|         confirm button:
 | |
|       </p>
 | |
|       <input
 | |
|         type="text"
 | |
|         class="confirm-delete"
 | |
|         placeholder="cluster name"
 | |
|         style="width: 100%"
 | |
|       >
 | |
|       <hr>
 | |
|       <p><small>
 | |
|         <strong>Note</strong>: if you create a cluster with the same name as
 | |
|         this one after deleting it, the new cluster will restore the data
 | |
|         from this cluster's current backups stored in AWS S3.  This behavior
 | |
|         will change soon and you will be able to reuse a cluster name and
 | |
|         get a completely new cluster.
 | |
|       </small></p>
 | |
|     `,
 | |
|         escapeKey: true,
 | |
|         icon: 'glyphicon glyphicon-warning-sign',
 | |
|         title: 'Confirm cluster deletion?',
 | |
|         typeAnimated: true,
 | |
|         type: 'red',
 | |
|         onOpen: function () {
 | |
|             const dialog = this
 | |
|             const confirm = dialog.buttons.confirm
 | |
|             const confirmSelector = jQuery(confirm.el)
 | |
|             const input = dialog.$content.find('input')
 | |
|             input.on('input', () => {
 | |
|                 if (input.val() === namespace + '/' + clustername) {
 | |
|                     confirmSelector.removeClass('btn-default').addClass('btn-danger')
 | |
|                     confirm.enable()
 | |
|                 } else {
 | |
|                     confirm.disable()
 | |
|                     confirmSelector.removeClass('btn-danger').addClass('btn-default')
 | |
|                 }
 | |
|             })
 | |
|         },
 | |
|         buttons: {
 | |
|             cancel: {
 | |
|                 text: 'Cancel',
 | |
|             },
 | |
|             confirm: {
 | |
|                 btnClass: 'btn-default',
 | |
|                 isDisabled: true,
 | |
|                 text: 'Delete cluster',
 | |
|                 action: () => {
 | |
|                     jQuery.ajax({
 | |
|                         type: 'DELETE',
 | |
|                         url: (
 | |
|                             './postgresqls/'
 | |
|               + encodeURI(namespace)
 | |
|               + '/' + encodeURI(clustername)
 | |
|                         ),
 | |
|                         dataType: 'text',
 | |
|                         success: () => location.assign('./#/list'),
 | |
|                         error: (r, status, error) => location.assign('./#/list'), // TODO: show error
 | |
|                     })
 | |
|                 },
 | |
|             },
 | |
|         }
 | |
|     })
 | |
| }
 | |
| 
 | |
| 
 | |
| /* Unfortunately, there does not appear to be a good way to import local modules
 | |
| inside a Riot tag, so we define/import things here and pass them manually in the
 | |
| opts variable.  Remember to propagate opts manually when instantiating tags.  */
 | |
| riot.mount('app', {
 | |
|     Dynamic: Dynamic,
 | |
|     Dynamics: Dynamics,
 | |
|     DynamicSet: DynamicSet,
 | |
|     delete_cluster: delete_cluster,
 | |
| })
 |