Improve Add Workbook Horizon UX

Show table with all created workbooks (currently stored in memory
stub), add 'Add Workbook', 'Edit Workbook' and 'Delete Workbook'
actions. Creating/ editing workbook is implemented as modal form (with
Workbook Builder inside).

Change-Id: I26139f674f7c7f3df2d45a0cd714e53b1d28538c
This commit is contained in:
Timur Sufiev 2014-10-16 21:35:22 +04:00
parent 0019829042
commit 4801a01b93
11 changed files with 300 additions and 32 deletions

69
extensions/mistral/api.py Normal file
View File

@ -0,0 +1,69 @@
# Copyright (c) 2014 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from horizon.test import utils as test_utils
_workbooks = []
def find_max_id():
max_id = 0
for workbook in _workbooks:
if max_id < int(workbook.id):
max_id = int(workbook.id)
return max_id
def create_workbook(request, json):
name = json['name']
for workbook in _workbooks:
if name == workbook['name']:
raise LookupError('Workbook with that name already exists!')
obj = test_utils.ObjDictWrapper(id=find_max_id()+1, **json)
_workbooks.append(obj)
return True
def modify_workbook(request, json):
id = json['id']
for i, workbook in enumerate(_workbooks[:]):
if unicode(id) == unicode(workbook.id):
_workbooks[i] = test_utils.ObjDictWrapper(**json)
return True
return False
def remove_workbook(request, id):
for i, workbook in enumerate(_workbooks[:]):
if unicode(id) == unicode(workbook.id):
del _workbooks[i]
return True
return False
def list_workbooks(request):
return _workbooks
def get_workbook(request, id):
for workbook in _workbooks:
if unicode(id) == unicode(workbook.id):
return workbook.__dict__
return None

View File

@ -0,0 +1,35 @@
# Copyright (c) 2014 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from horizon import forms
import yaml
from mistral import api
class BaseWorkbookForm(forms.SelfHandlingForm):
workbook = forms.CharField(widget=forms.HiddenInput,
required=False)
class CreateWorkbookForm(BaseWorkbookForm):
def handle(self, request, data):
json = yaml.load(data['workbook'])
return api.create_workbook(request, json)
class EditWorkbookForm(BaseWorkbookForm):
def handle(self, request, data):
json = yaml.load(data['workbook'])
return api.modify_workbook(request, json)

View File

@ -14,10 +14,7 @@
*/
.left, .right {
width: 50%;
float: left;
padding: 6px;
box-sizing: border-box;
}
.left {
border: 1px solid black;

View File

@ -301,6 +301,9 @@
'@type': Number,
'@default': 2
},
'name': {
'@type': String
},
'description': {
'@type': String,
'@required': false

View File

@ -17,6 +17,7 @@ var workbook;
$(function() {
var __counter = 0;
var current_workbook_id;
function getNextCounter() {
__counter++;
@ -278,16 +279,53 @@ $(function() {
});
}
$('button#create-workbook').click(function() {
var $controls = $('div#controls'),
$label = createNewLabel('Mistral Workbook');
$controls.empty();
workbook = types.Mistral.Workbook.create();
function initWorkbook(recreate) {
var $controls = $('div#controls'),
$label = createNewLabel('Mistral Workbook'),
json = jsyaml.load($('#json-output').val());
if ( json === undefined ) {
current_workbook_id = null
} else {
current_workbook_id = json.id;
}
$controls.empty();
if (recreate) {
workbook = types.Mistral.Workbook.create();
} else {
workbook = types.Mistral.Workbook.create(json);
}
drawTypedNode($controls, $label, workbook).find('label').click();
drawTypedNode($controls, $label, workbook).find('label').click();
}
$(function() {
initWorkbook()
});
$('button#create-workbook').click(function(evt) {
initWorkbook(true);
evt.preventDefault();
});
$('button#save-workbook').click(function() {
$('.right').text(jsyaml.dump(workbook.toJSON()));
})
function saveWorkbook() {
var json = workbook.toJSON(),
text;
if ( current_workbook_id !== null ) {
json.id = current_workbook_id;
}
text = jsyaml.dump(json);
$('.right pre').text(text);
$('#json-output').val(text);
}
$('form').submit(saveWorkbook);
$('button#save-workbook').click(function(evt) {
saveWorkbook();
evt.preventDefault();
});
// to prevent modal form submit
$('div#controls').click(function(evt) {
evt.preventDefault();
});
});

View File

@ -0,0 +1,53 @@
# Copyright (c) 2014 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
from django.template import defaultfilters
from horizon import tables
from mistral import api
class CreateWorkbook(tables.LinkAction):
name = 'create'
verbose_name = _('Create Workbook')
url = 'horizon:project:mistral:create'
classes = ('ajax-modal',)
icon = 'plus'
class EditWorkbook(tables.LinkAction):
name = 'edit'
verbose_name = _('Edit Workbook')
url = 'horizon:project:mistral:edit'
classes = ('ajax-modal',)
class RemoveWorkbook(tables.DeleteAction):
name = 'remove'
verbose_name = _('Remove Workbook')
data_type_singular = _('Workbook')
def delete(self, request, obj_id):
return api.remove_workbook(request, obj_id)
class WorkbooksTable(tables.DataTable):
name = tables.Column('name', verbose_name=_('Workbook Name'))
running = tables.Column('running', verbose_name=_('Running'),
filters=(defaultfilters.yesno,))
class Meta:
table_actions = (CreateWorkbook, RemoveWorkbook)
name = 'workbooks'
row_actions = (EditWorkbook, RemoveWorkbook)

View File

@ -0,0 +1,46 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}create_workbook{% endblock %}
{% block form_action %}
{% if form.initial.workbook_id %}
{% url 'horizon:project:mistral:edit' form.initial.workbook_id %}
{% else %}
{% url 'horizon:project:mistral:create' %}
{% endif %}
{% endblock %}
{% block modal-header %}
{% if form.initial.workbook_id %}
{% trans "Edit Workbook" %}
{% else %}
{% trans "Create Workbook" %}
{% endif %}
{% endblock %}
{% block modal_id %}create_workbook_modal{% endblock %}
{% block modal-body %}
<div class="left">
<div id="toolbar">
<button id="create-workbook">Reset Workbook</button>
<button id="save-workbook">Update YAML presentation</button>
<div id="controls"></div>
</div>
</div>
<div class="right">
<pre></pre>
<input name="workbook" id="json-output" type="hidden" value="{{ form.initial.workbook }}"/>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit"
value="{% if form.initial.workbook_id %}{% trans "Edit" %}{% else %}{% trans "Create" %}{% endif %}" />
<a href="{% url 'horizon:project:mistral:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}
{% block modal-js %}
<script src="{{ STATIC_URL }}mistral/js/schema.js"></script>
<script src="{{ STATIC_URL }}mistral/js/workbook.js"></script>
{% endblock %}

View File

@ -1,24 +1,15 @@
{% extends "merlin/base.html" %}
{% block title %}Merlin Project{% endblock %}
{% block main %}
<div>
<div class="left">
<div id="toolbar">
<button id="create-workbook">New Workbook</button>
<button id="save-workbook">Save Workbook</button>
</div>
<div id="controls"></div>
</div>
<pre class="right"></pre>
</div>
{% endblock %}
{% extends 'merlin/base.html' %}
{% load i18n %}
{% block title %}{% trans "Workbooks" %}{% endblock %}
{% block merlin-css %}
<link rel="stylesheet" href="{{ STATIC_URL }}mistral/css/mistral.css">
{% endblock %}
{% block merlin-js-scripts %}
<script src="{{ STATIC_URL }}mistral/js/schema.js"></script>
<script src="{{ STATIC_URL }}mistral/js/workbook.js"></script>
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Workbooks") %}
{% endblock page_header %}
{% block main %}
{{ table.render }}
{% endblock %}

View File

@ -19,4 +19,7 @@ from mistral import views
urlpatterns = patterns('',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create$', views.CreateWorkbookView.as_view(), name='create'),
url(r'^edit/(?P<workbook_id>[^/]+)$', views.EditWorkbookView.as_view(),
name='edit')
)

View File

@ -12,8 +12,40 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.views.generic import TemplateView # noqa
from django.core.urlresolvers import reverse_lazy
from horizon import tables
from horizon.forms import views
import yaml
from mistral import api
from mistral import forms as mistral_forms
from mistral import tables as mistral_tables
class IndexView(TemplateView):
class CreateWorkbookView(views.ModalFormView):
form_class = mistral_forms.CreateWorkbookForm
template_name = 'project/mistral/create.html'
success_url = reverse_lazy('horizon:project:mistral:index')
class EditWorkbookView(views.ModalFormView):
form_class = mistral_forms.EditWorkbookForm
template_name = 'project/mistral/create.html'
success_url = reverse_lazy('horizon:project:mistral:index')
def get_initial(self):
workbook_id = self.kwargs['workbook_id']
workbook = api.get_workbook(self.request, workbook_id)
if workbook:
return {'workbook': yaml.dump(workbook),
'workbook_id': workbook_id}
else:
return {}
class IndexView(tables.DataTableView):
template_name = 'project/mistral/index.html'
table_class = mistral_tables.WorkbooksTable
def get_data(self):
return api.list_workbooks(self.request)

View File

@ -0,0 +1 @@
{% extends "horizon/common/_modal_form.html" %}