Skip to content

Commit

Permalink
improved job custom-execution prompts (#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
ansibleguy committed Jul 13, 2024
1 parent 63fc5fc commit bc2d716
Show file tree
Hide file tree
Showing 17 changed files with 436 additions and 80 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Version 0

### 0.0.22

* Improved [custom execution prompts](https://webui.ansibleguy.net/en/latest/usage/jobs.html#execute)

----

### 0.0.21

* Added [validation against XSS](https://github.com/ansibleguy/webui/issues/44)
Expand Down
Binary file removed docs/source/_static/img/job_exec.png
Binary file not shown.
Binary file added docs/source/_static/img/job_execution.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/_static/img/job_prompts_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/_static/img/job_prompts_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 13 additions & 4 deletions docs/source/usage/jobs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@

.. include:: ../_include/warn_develop.rst

.. |job_exec| image:: ../_static/img/job_exec.png
.. |job_exec| image:: ../_static/img/job_execution.png
:class: wiki-img

.. |job_prompts1| image:: ../_static/img/job_prompts_1.png
:class: wiki-img

.. |job_prompts2| image:: ../_static/img/job_prompts_2.png
:class: wiki-img


====
Jobs
====
Expand Down Expand Up @@ -41,8 +48,10 @@ You have two options to execute a job:

The fields available as overrides can be configured in the job settings!

You can define required and optional overrides.
|job_prompts1|

|job_exec|
|job_prompts2|

Extra-vars can also be prompted. These need to be supplied in the following format: :code:`var={VAR-NAME}#{DISPLAY-NAME}` per example: :code:`var=add_user#User to add`
These will be shown in the job overview:

|job_exec|
15 changes: 0 additions & 15 deletions src/ansibleguy-webui/aw/api_endpoints/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,6 @@ def validate(self, attrs: dict):
if field in attrs:
validate_no_xss(value=attrs[field], field=field)

for prompt_field in ['execution_prompts_required', 'execution_prompts_optional']:
if prompt_field in attrs and is_set(attrs[prompt_field]):
if regex_match(Job.execution_prompts_regex, attrs[prompt_field]) is None:
raise ValidationError('Invalid execution prompt pattern')

translated = []
for field in attrs[prompt_field].split(','):
if field in Job.execution_prompt_aliases:
translated.append(Job.execution_prompt_aliases[field])

else:
translated.append(field)

attrs[prompt_field] = ','.join(translated)

return attrs


Expand Down
21 changes: 7 additions & 14 deletions src/ansibleguy-webui/aw/model/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,14 @@ def validate_cronjob(value):


class Job(BaseJob):
CHANGE_FIELDS = [
form_fields = [
'name', 'playbook_file', 'inventory_file', 'repository', 'schedule', 'enabled', 'limit', 'verbosity',
'mode_diff', 'mode_check', 'tags', 'tags_skip', 'verbosity', 'comment', 'environment_vars', 'cmd_args',
'credentials_default', 'credentials_needed', 'credentials_category',
'execution_prompts_required', 'execution_prompts_optional',
]
CHANGE_FIELDS = form_fields.copy()
CHANGE_FIELDS.append('execution_prompts')
form_fields_primary = ['name', 'playbook_file', 'inventory_file', 'repository']
form_fields = CHANGE_FIELDS
api_fields_read = ['id']
api_fields_read.extend(CHANGE_FIELDS)
api_fields_write = api_fields_read.copy()
Expand All @@ -105,17 +105,10 @@ class Job(BaseJob):
credentials_category = models.CharField(max_length=100, **DEFAULT_NONE)
repository = models.ForeignKey(Repository, on_delete=models.SET_NULL, related_name='job_fk_repo', **DEFAULT_NONE)

execution_prompts_max_len = 2000
execution_prompts_regex = (r'^(limit|verbosity|comment|mode_diff|diff|mode_check|check|environment_vars|env_vars|'
r'tags|tags_skip|skip_tags|cmd_args|var=[^,#]*?|var=[^#]*?#[^,]*?|[,$])+$')
execution_prompt_aliases = {
'check': 'mode_check',
'diff': 'mode_diff',
'env_vars': 'environment_vars',
'skip_tags': 'tags_skip',
}
execution_prompts_required = models.CharField(max_length=execution_prompts_max_len, **DEFAULT_NONE)
execution_prompts_optional = models.CharField(max_length=execution_prompts_max_len, **DEFAULT_NONE)
execution_prompts_max_len = 5000
execution_prompt_separator = ';'
execution_prompt_arg_separator = '#'
execution_prompts = models.CharField(max_length=execution_prompts_max_len, **DEFAULT_NONE)

def __str__(self) -> str:
limit = '' if self.limit is None else f' [{self.limit}]'
Expand Down
12 changes: 12 additions & 0 deletions src/ansibleguy-webui/aw/static/css/aw.css
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,14 @@ form label {
padding-top: 1vh !important;
}

.form-label-sub {
font-weight: normal !important;
}

.col-sm-10 .btn {
margin-top: 0.5vh;
}

form .form-control {
color: var(--tc1) !important;
background-color: var(--bg) !important;
Expand Down Expand Up @@ -648,3 +656,7 @@ td div hr {
.aw-log-change {
color: #F07201;
}

.aw-log-debug {
color: #326fa7;
}
153 changes: 153 additions & 0 deletions src/ansibleguy-webui/aw/static/js/jobs/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ const CHOICE_SELECTED_FLAG = 'selected';
const CHOICES_CLASS_DIR = 'aw-fs-choice-dir';
const CHOICES_CLASS_FILE = 'aw-fs-choice-file';
const CHOICES_CLASS_VALUE = 'aw-fs-value';
const ELEM_ID_TMPL_PROMPT = 'aw-job-form-prompt-tmpl';
const ELEM_ID_PROMPTS = 'aw-job-form-prompts';
const ELEM_ID_PROMPT_PREFIX = 'aw-job-form-prompt-'
const API_FIELD_PROMPTS = 'execution_prompts';
const PROMPT_SIMPLE_TYPES = ['tags', 'skip_tags', 'mode_check', 'mode_diff', 'limit', 'env_vars', 'cmd_args', 'verbosity'];
const PROMPT_SEPARATOR = ';';
const PROMPT_ARG_SEPARATOR = '#';
var PROMPT_ID = 0;

function apiBrowseDirFilteredChoices(choices, userInputCurrent, allowEmpty = false) {
let choicesFiltered = [];
Expand Down Expand Up @@ -272,10 +280,155 @@ function handleKeyTab($this) {
}
}

function addPromptInputsWithDefaults(name, varName, kind, required, choices, regex) {
PROMPT_ID += 1;
let tmpl = document.getElementById(ELEM_ID_TMPL_PROMPT).innerHTML;
tmpl = tmpl.replaceAll('${ID}', PROMPT_ID)
let promptElement = document.createElement('div');
promptElement.id = ELEM_ID_PROMPT_PREFIX + PROMPT_ID;

if (!is_set(name)) {
tmpl = tmpl.replaceAll('${NAME}', '');
} else {
tmpl = tmpl.replaceAll('${NAME}', name);
}

if (!is_set(varName)) {
tmpl = tmpl.replaceAll('${VAR_NAME}', '');
} else {
tmpl = tmpl.replaceAll('${VAR_NAME}', varName);
}

if (kind == 'dropdown') {
tmpl = tmpl.replaceAll('${kind_text}', '');
tmpl = tmpl.replaceAll('${kind_dd}', 'selected');
} else {
tmpl = tmpl.replaceAll('${kind_text}', 'selected');
tmpl = tmpl.replaceAll('${kind_dd}', '');
}

if (required == '1') {
tmpl = tmpl.replaceAll('${req_req}', 'selected');
tmpl = tmpl.replaceAll('${req_opt}', '');
} else {
tmpl = tmpl.replaceAll('${req_req}', '');
tmpl = tmpl.replaceAll('${req_opt}', 'selected');
}

if (!is_set(regex)) {
tmpl = tmpl.replaceAll('${REGEX}', '');
} else {
tmpl = tmpl.replaceAll('${REGEX}', regex);
}

if (!is_set(choices)) {
tmpl = tmpl.replaceAll('${CHOICES}', '');
} else {
tmpl = tmpl.replaceAll('${CHOICES}', choices);
}

promptElement.innerHTML = tmpl;
document.getElementById(ELEM_ID_PROMPTS).append(promptElement);
}

function addPromptInputs() {
addPromptInputsWithDefaults('', '', '', '', '', '');
}

function initPromptInputs() {
if (PROMPT_CNF == 'None') {
return;
}
for (let prompt of PROMPT_CNF.split(PROMPT_SEPARATOR)) {
if (PROMPT_SIMPLE_TYPES.includes(prompt)) {
continue;
}
let fields = prompt.split(PROMPT_ARG_SEPARATOR);
let regex = fields[5];
if (is_set(regex)) {
regex = atob(regex);
}

addPromptInputsWithDefaults(fields[0], fields[1], fields[2], fields[3], fields[4], regex);
}
}

function removePromptInputs(promptId) {
document.getElementById(ELEM_ID_PROMPT_PREFIX + promptId).remove();
}

$( document ).ready(function() {
getRepositoryToBrowse();
setInterval('getRepositoryToBrowse()', (DATA_REFRESH_SEC * 500));
initPromptInputs();

$(".aw-main").on("submit", "#aw-job-form", function(event) {
event.preventDefault();

var form = $(this);
var actionUrl = form.attr('action');
var method = form.attr('method');
var refresh = false;

var job = [];
var prompts = [];
var promptFields = [];
for (let field of form[0]) {
if (['input', 'select'].includes(field.localName)) {
if (field.name.startsWith('prompt_')) {
let promptNameParts = field.name.split('_');
if (promptNameParts.length == 2) {
prompts.push(promptNameParts[1]);
continue;
}

let promptId = promptNameParts[1];
let fieldName = promptNameParts[2];
if (promptFields[promptId] === undefined) {
promptFields[promptId] = [];
}
promptFields[promptId][fieldName] = field.value;

} else {
job.push(field.name + '=' + field.value);
}
}
}

for (let i = 1; i <= promptFields.length; i++) {
if (promptFields[i] === undefined) {
continue;
}
let prompt = [];
prompt.push(promptFields[i].name);
prompt.push(promptFields[i].varName);
prompt.push(promptFields[i].kind);
prompt.push(promptFields[i].required);
prompt.push(promptFields[i].choices);
prompt.push(btoa(promptFields[i].regex));
prompts.push(prompt.join(PROMPT_ARG_SEPARATOR));
}

job.push(API_FIELD_PROMPTS + '=' + prompts.join(PROMPT_SEPARATOR));

var jobSerialized = job.join('&');

$.ajax({
type: method,
url: actionUrl,
data: jobSerialized,
success: function (result) { apiActionSuccess(result); },
error: function (result, exception) { apiActionError(result, exception); },
});

return false;
});
$(".aw-main").on("click", "#aw-job-form-prompt-add", function(){
addPromptInputs();
});
$(".aw-main").on("click", ".aw-job-form-prompt-del", function(){
removePromptInputs($(this).attr("name"));
});
$(".aw-main").on("input", ".aw-fs-browse", function(){
updateChoices(jQuery(this));
});
Expand Down
Loading

0 comments on commit bc2d716

Please sign in to comment.