open-nomad/ui/app/adapters/variable.js
Phil Renaud 3db9f11c37
[feat] Nomad Job Templates (#15746)
* Extend variables under the nomad path prefix to allow for job-templates (#15570)

* Extend variables under the nomad path prefix to allow for job-templates

* Add job-templates to error message hinting

* RadioCard component for Job Templates (#15582)

* chore: add

* test: component API

* ui: component template

* refact: remove  bc naming collission

* styles: remove SASS var causing conflicts

* Disallow specific variable at nomad/job-templates (#15681)

* Disallows variables at exactly nomad/job-templates

* idiomatic refactor

* Expanding nomad job init to accept a template flag (#15571)

* Adding a string flag for templates on job init

* data-down actions-up version of a custom template editor within variable

* Dont force grid on job template editor

* list-templates flag started

* Correctly slice from end of path name

* Pre-review cleanup

* Variable form acceptance test for job template editing

* Some review cleanup

* List Job templates test

* Example from template test

* Using must.assertions instead of require etc

* ui: add choose template button (#15596)

* ui: add new routes

* chore: update file directory

* ui: add choose template button

* test: button and page navigation

* refact: update var name

* ui: use `Button` component from `HDS` (#15607)

* ui: integrate  buttons

* refact: remove  helper

* ui: remove icons on non-tertiary buttons

* refact: update normalize method for key/value pairs (#15612)

* `revert`: `onCancel` for `JobDefinition`

The `onCancel` method isn't included in the component API for `JobEditor` and the primary cancel behavior exists outside of the component. With the exception of the `JobDefinition` page where we include this button in the top right of the component instead of next to the `Plan` button.

* style: increase button size

* style: keep lime green

* ui: select template (#15613)

* ui: deprecate unused component

* ui: deprecate tests

* ui: jobs.run.templates.index

* ui: update logic to handle templates

* refact: revert key/value changes

* style: padding for cards + buttons

* temp: fixtures for mirage testing

* Revert "refact: revert key/value changes"

This reverts commit 124e95d12140be38fc921f7e15243034092c4063.

* ui: guard template for unsaved job

* ui: handle reading template variable

* Revert "refact: update normalize method for key/value pairs (#15612)"

This reverts commit 6f5ffc9b610702aee7c47fbff742cc81f819ab74.

* revert: remove test fixtures

* revert: prettier problems

* refact: test doesnt need filter expression

* styling: button sizes and responsive cards

* refact: remove route guarding

* ui: update variable adapter

* refact: remove model editing behavior

* refact: model should query variables to populate editor

* ui: clear qp on exit

* refact: cleanup deprecated API

* refact: query all namespaces

* refact: deprecate action

* ui: rely on  collection

* refact: patch deprecate transition API

* refact: patch test to expect namespace qp

* styling: padding, conditionals

* ui: flashMessage on 404

* test: update for o(n+1) query

* ui: create new job template (#15744)

* refact: remove unused code

* refact: add type safety

* test: select template flow

* test: add data-test attrs

* chore: remove dead code

* test: create new job flow

* ui: add create button

* ui: create job template

* refact: no need for wildcard

* refact:  record instead of delete

* styling: spacing

* ui: add error handling and form validation to job create template (#15767)

* ui: handle server side errors

* ui: show error to prevent duplicate

* refact: conditional namespace

* ui: save as template flow (#15787)

* bug:  patches failing tests associated with `pretender` (#15812)

* refact: update assertion

* refact: test set-up

* ui: job templates manager view (#15815)

* ui: manager list view

* test: edit flow

* refact: deprecate column-helper

* ui: template edit and delete flow (#15823)

* ui: manager list view

* refact: update title

* refact: update permissions

* ui: template edit page

* bug: typo

* refact: update toast messages

* bug:  clear selections on exit (#15827)

* bug:  clear controllers on exit

* test: mirage config changes (#15828)

* refact: deprecate column-helper

* style: update z-index for HDS

* Revert "style: update z-index for HDS"

This reverts commit d3d87ceab6d083f7164941587448607838944fc1.

* refact: update delete button

* refact: edit redirect

* refact: patch reactivity issues

* styling: fixed width

* refact: override defaults

* styling: edit text causing overflow

* styling:  add inline text

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

* bug: edit `text` to `template`

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

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

* test:  delete flow job templates (#15896)

* refact: edit names

* bug:  set correct ref to store

* chore: trim whitespace:

* test: delete flow

* bug: reactively update view (#15904)

* Initialized default jobs (#15856)

* Initialized default jobs

* More jobs scaffolded

* Better commenting on a couple example job specs

* Adapter doing the work

* fall back to epic config

* Label format helper and custom serialization logic

* Test updates to account for a never-empty state

* Test suite uses settled and maintain RecordArray in adapter return

* Updates to hello-world and variables example jobspecs

* Parameterized job gets optional payload output

* Formatting changes for param and service discovery job templates

* Multi-group service discovery job

* Basic test for default templates (#15965)

* Basic test for default templates

* Percy snapshot for manage page

* Some late-breaking design changes

* Some copy edits to the header paragraphs for job templates (#15967)

* Added some init options for job templates (#15994)

* Async method for populating default job templates from the variable adapter

---------

Co-authored-by: Jai <41024828+ChaiWithJai@users.noreply.github.com>
2023-02-02 10:37:40 -05:00

158 lines
4.8 KiB
JavaScript

// @ts-check
import ApplicationAdapter from './application';
import AdapterError from '@ember-data/adapter/error';
import { pluralize } from 'ember-inflector';
import classic from 'ember-classic-decorator';
import { ConflictError } from '@ember-data/adapter/error';
import DEFAULT_JOB_TEMPLATES from 'nomad-ui/utils/default-job-templates';
import { inject as service } from '@ember/service';
@classic
export default class VariableAdapter extends ApplicationAdapter {
@service store;
pathForType = () => 'var';
// PUT instead of POST on create;
// /v1/var instead of /v1/vars on create (urlForFindRecord)
createRecord(_store, type, snapshot) {
let data = this.serialize(snapshot);
let baseUrl = this.buildURL(type.modelName, data.ID);
const checkAndSetValue = snapshot?.attr('modifyIndex') || 0;
return this.ajax(`${baseUrl}?cas=${checkAndSetValue}`, 'PUT', { data });
}
/**
* Query for job templates, both defaults and variables at the nomad/job-templates path.
* @returns {Promise<{variables: Variable[], default: Variable[]}>}
*/
async getJobTemplates() {
await this.populateDefaultJobTemplates();
const jobTemplateVariables = await this.store.query('variable', {
prefix: 'nomad/job-templates',
namespace: '*',
});
// Ensure we run a findRecord on each to get its keyValues
await Promise.all(
jobTemplateVariables.map((t) => this.store.findRecord('variable', t.id))
);
const defaultTemplates = this.store
.peekAll('variable')
.filter((t) => t.isDefaultJobTemplate);
return { variables: jobTemplateVariables, default: defaultTemplates };
}
async populateDefaultJobTemplates() {
await Promise.all(
DEFAULT_JOB_TEMPLATES.map((template) => {
if (!this.store.peekRecord('variable', template.id)) {
let variableSerializer = this.store.serializerFor('variable');
let normalized =
variableSerializer.normalizeDefaultJobTemplate(template);
return this.store.createRecord('variable', normalized);
}
return null;
})
);
}
/**
* @typedef Variable
* @type {object}
*/
/**
* Lookup a job template variable by ID/path.
* @param {string} templateID
* @returns {Promise<Variable>}
*/
async getJobTemplate(templateID) {
await this.populateDefaultJobTemplates();
const defaultJobs = this.store
.peekAll('variable')
.filter((template) => template.isDefaultJobTemplate);
if (defaultJobs.find((job) => job.id === templateID)) {
return defaultJobs.find((job) => job.id === templateID);
} else {
return this.store.findRecord('variable', templateID);
}
}
urlForFindAll(modelName) {
let baseUrl = this.buildURL(modelName);
return pluralize(baseUrl);
}
urlForQuery(_query, modelName) {
let baseUrl = this.buildURL(modelName);
return pluralize(baseUrl);
}
urlForFindRecord(identifier, modelName) {
let path,
namespace = null;
// TODO: Variables are namespaced. This Adapter should extend the WatchableNamespaceId Adapter.
// When that happens, we will need to refactor this to accept JSON tuple like we do for jobs.
const delimiter = identifier.lastIndexOf('@');
if (delimiter !== -1) {
path = identifier.slice(0, delimiter);
namespace = identifier.slice(delimiter + 1);
} else {
path = identifier;
namespace = 'default';
}
let baseUrl = this.buildURL(modelName, path);
return `${baseUrl}?namespace=${namespace}`;
}
urlForUpdateRecord(identifier, modelName, snapshot) {
const { id } = _extractIDAndNamespace(identifier, snapshot);
let baseUrl = this.buildURL(modelName, id);
if (snapshot?.adapterOptions?.overwrite) {
return `${baseUrl}`;
} else {
const checkAndSetValue = snapshot?.attr('modifyIndex') || 0;
return `${baseUrl}?cas=${checkAndSetValue}`;
}
}
urlForDeleteRecord(identifier, modelName, snapshot) {
const { namespace, id } = _extractIDAndNamespace(identifier, snapshot);
const baseUrl = this.buildURL(modelName, id);
return `${baseUrl}?namespace=${namespace}`;
}
handleResponse(status, _, payload) {
if (status === 404) {
return new AdapterError([{ detail: payload, status: 404 }]);
}
if (status === 409) {
return new ConflictError([
{ detail: _normalizeConflictErrorObject(payload), status: 409 },
]);
}
return super.handleResponse(...arguments);
}
}
function _extractIDAndNamespace(identifier, snapshot) {
const namespace = snapshot?.attr('namespace') || 'default';
const id = snapshot?.attr('path') || identifier;
return {
namespace,
id,
};
}
function _normalizeConflictErrorObject(conflictingVariable) {
return {
modifyTime: Math.floor(conflictingVariable.ModifyTime / 1000000),
items: conflictingVariable.Items,
};
}