/** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: BUSL-1.1 */ // @ts-check import { action } from '@ember/object'; import { inject as service } from '@ember/service'; import { alias } from '@ember/object/computed'; import { tracked } from '@glimmer/tracking'; import Component from '@glimmer/component'; import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error'; export default class NamespaceEditorComponent extends Component { @service notifications; @service router; @service store; @service can; @alias('args.namespace') namespace; @tracked JSONError = null; @tracked definitionString = this.definitionStringFromNamespace( this.args.namespace ); @action updateNamespaceName({ target: { value } }) { this.namespace.set('name', value); } @action updateNamespaceDefinition(value) { this.JSONError = null; this.definitionString = value; try { JSON.parse(this.definitionString); } catch (error) { this.JSONError = 'Invalid JSON'; } } @action async save(e) { if (e instanceof Event) { e.preventDefault(); // code-mirror "command+enter" submits the form, but doesnt have a preventDefault() } try { this.deserializeDefinitionJson(JSON.parse(this.definitionString)); const nameRegex = '^[a-zA-Z0-9-]{1,128}$'; if (!this.namespace.name?.match(nameRegex)) { throw new Error( `Namespace name must be 1-128 characters long and can only contain letters, numbers, and dashes.` ); } const shouldRedirectAfterSave = this.namespace.isNew; if ( this.namespace.isNew && this.store .peekAll('namespace') .filter((namespace) => namespace !== this.namespace) .findBy('name', this.namespace.name) ) { throw new Error( `A namespace with name ${this.namespace.name} already exists.` ); } this.namespace.set('id', this.namespace.name); await this.namespace.save(); this.notifications.add({ title: 'Namespace Saved', color: 'success', }); if (shouldRedirectAfterSave) { this.router.transitionTo( 'administration.namespaces.acl-namespace', this.namespace.name ); } } catch (err) { let title = `Error ${ this.namespace.isNew ? 'creating' : 'updating' } Namespace ${this.namespace.name}`; let message = err.errors?.length ? messageFromAdapterError(err) : err.message || 'Unknown Error'; this.notifications.add({ title, message, color: 'critical', sticky: true, }); } } definitionStringFromNamespace(namespace) { let definitionHash = {}; definitionHash['Description'] = namespace.description; definitionHash['Capabilities'] = namespace.capabilities; definitionHash['Meta'] = namespace.meta; if (this.can.can('configure-in-namespace node-pool')) { definitionHash['NodePoolConfiguration'] = namespace.nodePoolConfiguration; } if (this.can.can('configure-in-namespace quota')) { definitionHash['Quota'] = namespace.quota; } return JSON.stringify(definitionHash, null, 4); } deserializeDefinitionJson(definitionHash) { this.namespace.set('description', definitionHash['Description']); this.namespace.set('meta', definitionHash['Meta']); let capabilities = this.store.createFragment( 'ns-capabilities', definitionHash['Capabilities'] ); this.namespace.set('capabilities', capabilities); if (this.can.can('configure-in-namespace node-pool')) { let npConfig = definitionHash['NodePoolConfiguration'] || {}; this.store.create; // If we don't manually set this to null, removing // the keys wont update the data framgment, which we want if (!('Allowed' in npConfig)) { npConfig['Allowed'] = null; } if (!('Disallowed' in npConfig)) { npConfig['Disallowed'] = null; } // Create node pool config fragment let nodePoolConfiguration = this.store.createFragment( 'ns-node-pool-configuration', npConfig ); this.namespace.set('nodePoolConfiguration', nodePoolConfiguration); } if (this.can.can('configure-in-namespace quota')) { this.namespace.set('quota', definitionHash['Quota']); } } }