open-nomad/ui/app/components/job-editor.js
Jai 08d97a19ca
feat: visualize HCL Job Specification in the Nomad UI jobs.job.definition view (#16669)
* ui:  Toggle for `read-only` view (#16279)

* ui: model update for specification

* style: add styling for select

* style: add styling for select

* refact: add spec to view

* refact: update component API

* test: refactor for new UI state

* refact: clean conditional

* refact: update component API for prop

* chore: correct naming

* chore:  remove `fn` helper

Co-authored-by: Phil Renaud <phil.renaud@hashicorp.com>

* update `default` Mirage scenario (#16496)

* chore: update mirage scenario:

* ui: conditionally render toggle button (#16497)

* chore: update css variable name (#16498)

---------

Co-authored-by: Phil Renaud <phil.renaud@hashicorp.com>

* ui:  Display JSON view of variables associated to job specification (#16570)

* chore: move fixture to util

* chore: update tests:

* ui: display variables table

* chore: add mirage fixture (#16572)

* ui: regex for job spec parse (#16668)

* ui: remove variable table (#16670)

* ui:  notify user if specification has variables (#16671)

* ui: regex for job spec parse

* chore: deprecate variable references

* chore: update mirage

* ui: add notification

* test: add test coverage for parse method (#16590)

* refact: `JobEditor` reactive query parameters (#16710)

* refact: add query parameter

* refact: move toggle action to controller

* ui:  remove toggle behavior in `JobEditor` (#16711)

* refact: rename logic for select

* chore: instantiate qp in route

* refact:  uniform alerts (#16715)

* style: buffer between alert and header

* refact: extract alerts into a component

* chore: update tests for qp

* chore: defensive logic for app controller

* refact:  move `edit` state to controller (#16725)

* refact: move edit state to controller

* refact: handle edit state (#16731)

* refact: handle edit state

* ui:  warning message (#16732)

* ui: warning message

* ui:  enable editing of HCL vars in the UI (#16734)

* enable editing of HCL vars

* refact: default qp logic

* refact: alert condition

* refact: Pass `variables` as string (#16849)

* ui:  Toggle for `read-only` view (#16279)

* ui: model update for specification

* style: add styling for select

* style: add styling for select

* refact: add spec to view

* refact: update component API

* test: refactor for new UI state

* refact: clean conditional

* refact: update component API for prop

* chore: correct naming

* chore:  remove `fn` helper

Co-authored-by: Phil Renaud <phil.renaud@hashicorp.com>

* update `default` Mirage scenario (#16496)

* chore: update mirage scenario:

* ui: conditionally render toggle button (#16497)

* chore: update css variable name (#16498)

---------

Co-authored-by: Phil Renaud <phil.renaud@hashicorp.com>

* refact: `JobEditor` reactive query parameters (#16710)

* refact: add query parameter

* refact: move toggle action to controller

* ui:  remove toggle behavior in `JobEditor` (#16711)

* refact: rename logic for select

* chore: instantiate qp in route

* refact:  uniform alerts (#16715)

* style: buffer between alert and header

* refact: extract alerts into a component

* chore: update tests for qp

* chore: defensive logic for app controller

* refact:  move `edit` state to controller (#16725)

* refact: move edit state to controller

* refact: handle edit state (#16731)

* refact: handle edit state

* ui:  warning message (#16732)

* ui: warning message

* ui:  enable editing of HCL vars in the UI (#16734)

* enable editing of HCL vars

* refact: default qp logic

* refact: alert condition

* refact: variables as string

* style: revert styling change

---------

Co-authored-by: Phil Renaud <phil.renaud@hashicorp.com>

* bug: correctly edit variables (#16989)

* ui: visualize variables (#16987)

* ui: fetchRawSpecification

* refact: integrate new model method

* test: fetchRaw unit

* styling: enable height on cm

* chore: update copy

* feat: visual variables

* chore: conditional render info txt

* refact: add mirage endpoint

* refact: update test for new schema

* refact: job submit flow (#17015)

* refact: job update logic

* chore: remove dead code

* bug:  update `job.run` and `job.update` adapter methods (#17055)

* refact:  update adapter

* chore: update api usage

* styling:  UX requests (#17064)

* refact:  update adapter

* chore: update api usage

* styling: disable toggle w text

* styling: stick button

* style: space out alerts

* chore:  autofocus on first editor

* bug: dismiss alert

* chore:  add jsdoc and assertion check

* chore:  update mirage for Vercel (#17054)

* chore: mirage logic for vercel deploy

* chore:  update test for mirage change

* refact:  API refactoring (#17083)

* refact: udpate for req schema

* refact: update for variable flags and literal

* bug: visualize job model not derived state

* chore:  update copy

* chore: fix incorrect copy

* chore: deprecate variables derived state

* chore: update copy

* feat: enable toggle on edit

* chore: prettify

* refact: move conditional

---------

Co-authored-by: Phil Renaud <phil.renaud@hashicorp.com>
2023-05-09 11:03:52 -04:00

202 lines
4.5 KiB
JavaScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { task } from 'ember-concurrency';
import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';
import localStorageProperty from 'nomad-ui/utils/properties/local-storage';
import { tracked } from '@glimmer/tracking';
export default class JobEditor extends Component {
@service config;
@service store;
@tracked error = null;
@tracked planOutput = null;
constructor() {
super(...arguments);
if (this.definition) {
this.setDefinitionOnModel();
}
if (this.args.variables) {
this.args.job.set(
'_newDefinitionVariables',
this.jsonToHcl(this.args.variables.flags).concat(
this.args.variables.literal
)
);
}
}
get isEditing() {
return ['new', 'edit'].includes(this.args.context);
}
@action
setDefinitionOnModel() {
this.args.job.set('_newDefinition', this.definition);
}
@action
edit() {
this.setDefinitionOnModel();
this.args.onToggleEdit(true);
}
@action
onCancel() {
this.args.onToggleEdit(false);
}
get stage() {
if (this.planOutput) return 'review';
if (this.isEditing) return 'edit';
else return 'read';
}
@localStorageProperty('nomadMessageJobPlan', true) shouldShowPlanMessage;
@action
dismissPlanMessage() {
this.shouldShowPlanMessage = false;
}
@(task(function* () {
this.reset();
try {
yield this.args.job.parse();
} catch (err) {
this.onError(err, 'parse', 'parse jobs');
return;
}
try {
const plan = yield this.args.job.plan();
this.planOutput = plan;
} catch (err) {
this.onError(err, 'plan', 'plan jobs');
}
}).drop())
plan;
@task(function* () {
try {
if (this.args.context === 'new') {
yield this.args.job.run();
} else {
yield this.args.job.update(this.args.format);
}
const id = this.args.job.plainId;
const namespace = this.args.job.belongsTo('namespace').id() || 'default';
this.reset();
// Treat the job as ephemeral and only provide ID parts.
this.args.onSubmit(id, namespace);
} catch (err) {
this.onError(err, 'run', 'submit jobs');
this.planOutput = null;
}
})
submit;
onError(err, type, actionMsg) {
const error = messageFromAdapterError(err, actionMsg);
this.error = { message: error, type };
this.scrollToError();
}
@action
reset() {
this.planOutput = null;
this.error = null;
}
scrollToError() {
if (!this.config.get('isTest')) {
window.scrollTo(0, 0);
}
}
@action
updateCode(value, _codemirror, type = 'job') {
if (!this.args.job.isDestroying && !this.args.job.isDestroyed) {
if (type === 'hclVariables') {
this.args.job.set('_newDefinitionVariables', value);
} else {
this.args.job.set('_newDefinition', value);
}
}
}
@action
uploadJobSpec(event) {
const reader = new FileReader();
reader.onload = () => {
this.updateCode(reader.result);
};
const [file] = event.target.files;
reader.readAsText(file);
}
get definition() {
if (this.args.view === 'full-definition') {
return JSON.stringify(this.args.definition, null, 2);
} else {
return this.args.specification;
}
}
jsonToHcl(obj) {
const hclLines = [];
for (const key in obj) {
const value = obj[key];
const hclValue = typeof value === 'string' ? `"${value}"` : value;
hclLines.push(`${key}=${hclValue}\n`);
}
return hclLines.join('\n');
}
get data() {
return {
cancelable: this.args.cancelable,
definition: this.definition,
format: this.args.format,
hasSpecification: !!this.args.specification,
hasVariables:
!!this.args.variables?.flags || !!this.args.variables?.literal,
job: this.args.job,
planOutput: this.planOutput,
shouldShowPlanMessage: this.shouldShowPlanMessage,
view: this.args.view,
};
}
get fns() {
return {
onCancel: this.onCancel,
onDismissPlanMessage: this.dismissPlanMessage,
onEdit: this.edit,
onPlan: this.plan,
onReset: this.reset,
onSaveAs: this.args.handleSaveAsTemplate,
onSubmit: this.submit,
onSelect: this.args.onSelect,
onUpdate: this.updateCode,
onUpload: this.uploadJobSpec,
};
}
}