diff --git a/extensions/enabled/_51_add_hotbuilder_panel.py b/extensions/enabled/_51_add_hotbuilder_panel.py new file mode 100644 index 0000000..20054e3 --- /dev/null +++ b/extensions/enabled/_51_add_hotbuilder_panel.py @@ -0,0 +1,17 @@ +# The name of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'hotbuilder' +# The name of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'project' +# The name of the panel group the PANEL is associated with. +PANEL_GROUP = 'orchestration' + +ADD_INSTALLED_APPS = ['merlin', 'hotbuilder'] + +# Python panel class of the PANEL to be added. +ADD_PANEL = 'hotbuilder.panel.HotBuilderPanel' + +ADD_ANGULAR_MODULES = ['merlin', 'hotbuilder'] +ADD_JS_FILES = ['merlin/js/custom-libs/ui-bootstrap-tpls-0.12.1.js', + 'merlin/js/merlin.init.js', + 'merlin/js/merlin.templates.js', + 'hotbuilder/js/hotbuilder.init.js'] diff --git a/extensions/hotbuilder/__init__.py b/extensions/hotbuilder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/extensions/hotbuilder/api.py b/extensions/hotbuilder/api.py new file mode 100644 index 0000000..c24ec6d --- /dev/null +++ b/extensions/hotbuilder/api.py @@ -0,0 +1,30 @@ +# Copyright (c) 2015 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. + +import logging +from openstack_dashboard.api import heat + +logger = logging.getLogger(__name__) + + +def resource_type_list(request): + client = heat.heatclient(request) + kwargs = {} + return client.resource_types.list(**kwargs) + + +def resource_type_show(request, resource_type): + client = heat.heatclient(request) + return client.resource_types.get(resource_type) + diff --git a/extensions/hotbuilder/panel.py b/extensions/hotbuilder/panel.py new file mode 100644 index 0000000..32f5b5d --- /dev/null +++ b/extensions/hotbuilder/panel.py @@ -0,0 +1,20 @@ +# 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. + +import horizon + + +class HotBuilderPanel(horizon.Panel): + name = 'HOT Templates' + slug = 'hotbuilder' diff --git a/extensions/hotbuilder/static/hotbuilder/js/hotbuilder.controllers.js b/extensions/hotbuilder/static/hotbuilder/js/hotbuilder.controllers.js new file mode 100644 index 0000000..d8216ab --- /dev/null +++ b/extensions/hotbuilder/static/hotbuilder/js/hotbuilder.controllers.js @@ -0,0 +1,46 @@ +(function() { + 'use strict'; + + angular + .module('hotbuilder') + .controller('HotbuilderController', HotbuilderController); + + HotbuilderController.$inject = ['$q', '$http']; + + function HotbuilderController($q, $http) { + var deferred = $q.defer(); + var vm = this; + + deferred.promise.then(function(url) { + $http.get(url).success(function(data) { + data.forEach(function(resourceType) { + $http.get(url + '/' + resourceType).success(function(data) { + vm.allEntries.push(data); + }); + }); + }); + }); + + vm.setUrl = setUrl; + + vm.allEntries = []; + + vm.entries = [ + + ]; + + vm.panel = { + title: 'Add Resource' + }; + + vm.tabs = [ + {title: 'Parameters', content: 'There'}, + {title: 'Outputs', content: 'Where'} + ]; + + function setUrl(url) { + deferred.resolve(url); + } + } + +})(); diff --git a/extensions/hotbuilder/static/hotbuilder/js/hotbuilder.init.js b/extensions/hotbuilder/static/hotbuilder/js/hotbuilder.init.js new file mode 100644 index 0000000..6edc4e6 --- /dev/null +++ b/extensions/hotbuilder/static/hotbuilder/js/hotbuilder.init.js @@ -0,0 +1,14 @@ +(function() { + 'use strict'; + + angular + .module('hotbuilder', ['merlin']) + .run(initModule); + + initModule.$inject = ['merlin.templates']; + + function initModule(templates) { + templates.prefetch('/static/hotbuilder/templates/fields/', []); + } + +})(); diff --git a/extensions/hotbuilder/static/hotbuilder/js/hotbuilder.models.js b/extensions/hotbuilder/static/hotbuilder/js/hotbuilder.models.js new file mode 100644 index 0000000..249ea83 --- /dev/null +++ b/extensions/hotbuilder/static/hotbuilder/js/hotbuilder.models.js @@ -0,0 +1,10 @@ +/** + * Created by tsufiev on 2/24/15. + */ +(function() { + 'use strict'; + + angular + .module('hotbuilder'); + +})(); diff --git a/extensions/hotbuilder/templates/hotbuilder/index.html b/extensions/hotbuilder/templates/hotbuilder/index.html new file mode 100644 index 0000000..e1c7c1b --- /dev/null +++ b/extensions/hotbuilder/templates/hotbuilder/index.html @@ -0,0 +1,88 @@ +{% extends 'merlin/base.html' %} +{% load i18n %} +{% block title %}{% trans "Template Builder" %}{% endblock %} + +{% block merlin-css %} +{% endblock %} + +{% block merlin-js %} + <script type="text/javascript" src="{{ STATIC_URL }}hotbuilder/js/hotbuilder.init.js"></script> + <script type="text/javascript" src="{{ STATIC_URL }}hotbuilder/js/hotbuilder.controllers.js"></script> + <script type="text/javascript" src="{{ STATIC_URL }}hotbuilder/js/hotbuilder.models.js"></script> +{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Template Builder") %} +{% endblock page_header %} + +{% block main %} + <h3>Compose Template</h3> + <div ng-controller="HotbuilderController as hotbuilder" + ng-init="hotbuilder.setUrl('{% url "horizon:project:hotbuilder:resource_types" %}')"> + <div class="well"> + <div class="row"> + <div class="col-xs"> + <tabset> + <tab heading="Resources"> + <panel content="hotbuilder.panel"> + + <collapsible-group class="col-xs-12" content="{}" + title="'Recent resources'"> + <div class="col-xs-6"> + <draggable-entry + ng-repeat="entry in hotbuilder.entries" + entry="entry" entry-icon="server"> + </draggable-entry> + </div> + </collapsible-group> + <collapsible-group class="col-xs-12" content="{}" + title="'More resources'"> + <div class="col-xs-6"> + <div class="has-feedback"> + <input type="text" ng-model="filterValue" placeholder="Filter"> + <span class="form-control-feedback fa fa-filter"></span> + </div> + <draggable-entry + ng-repeat="entry in hotbuilder.allEntries | filter:filterValue" + entry="entry" entry-icon="server"> + </draggable-entry> + </div> + </collapsible-group> + </panel> + </tab> + <tab ng-repeat="tab in hotbuilder.tabs" heading="{$ tab.title $}" active="tab.active"> + <panel> + {$ tab.content $} + </panel> + </tab> + </tabset> + </div> + <div class="col-xs end-xs"> + <div class="btn-group btn-toggle"> + <button ng-click="isGraphMode = true" class="btn btn-sm" + ng-class="isGraphMode ? 'active btn-primary' : 'btn-default'">Graph</button> + <button ng-click="isGraphMode = false" class="btn btn-sm" + ng-class="!isGraphMode ? 'active btn-primary' : 'btn-default'">YAML</button> + </div> + <div class="panel panel-default"> + <div class="panel-body" ng-show="!isGraphMode"> + <pre>{$ workbook.toYAML() $}</pre> + </div> + <div class="panel-body" ng-show="isGraphMode"> + Here will be a fancy Graph View as soon as we implement it! + </div> + </div> + </div> + </div> + <!-- page footer --> + <div class="row"> + <div class="col-xs end-xs"> + <button ng-click="discardWorkbook()" class="btn btn-default cancel">Cancel</button> + <button ng-click="commitWorkbook()" class="btn btn-primary"> + {$ workbookID ? 'Modify' : 'Create' $} + </button> + </div> + </div> + </div> + </div> +{% endblock %} diff --git a/extensions/hotbuilder/urls.py b/extensions/hotbuilder/urls.py new file mode 100644 index 0000000..8e0fae2 --- /dev/null +++ b/extensions/hotbuilder/urls.py @@ -0,0 +1,26 @@ +# 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.conf.urls import patterns +from django.conf.urls import url + +from hotbuilder import views + +urlpatterns = patterns('', + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^heat_resources$', views.ResourceTypesView.as_view(), + name='resource_types'), + url(r'^heat_resources/(?P<resource_type>[^/]+)$', + views.ShowResourceView.as_view(), name='show_resource') +) diff --git a/extensions/hotbuilder/views.py b/extensions/hotbuilder/views.py new file mode 100644 index 0000000..f35067f --- /dev/null +++ b/extensions/hotbuilder/views.py @@ -0,0 +1,45 @@ +# Copyright 2014 Rackspace +# +# 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. + +import json + +from django.views.generic import TemplateView, View +from django.http import HttpResponse + +from hotbuilder import api + + +class JSONView(View): + def get_data(self, request, *args, **kwargs): + pass + + def get(self, request, *args, **kwargs): + return HttpResponse( + json.dumps(self.get_data(request, *args, **kwargs)), + content_type='application/json') + + +class ResourceTypesView(JSONView): + def get_data(self, request, *args, **kwargs): + return [resource.resource_type for resource in + api.resource_type_list(request)] + + +class ShowResourceView(JSONView): + def get_data(self, request, *args, **kwargs): + return api.resource_type_show(request, kwargs['resource_type']) + + +class IndexView(TemplateView): + template_name = 'hotbuilder/index.html' diff --git a/extensions/mistral/templates/mistral/create.html b/extensions/mistral/templates/mistral/create.html index 931f969..69a671e 100644 --- a/extensions/mistral/templates/mistral/create.html +++ b/extensions/mistral/templates/mistral/create.html @@ -1,41 +1,18 @@ -{% extends "base.html" %} +{% extends "merlin/base.html" %} {% load i18n %} -{% load url from future %} -{% load static %} -{% load compress %} {% block title %}{% trans "Create Workbook" %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Workbooks") %} {% endblock page_header %} -{% block js %} -{% include "horizon/_scripts.html" %} -<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/custom-libs/barricade.js"></script> -<script type="text/javascript" src="{{ STATIC_URL }}merlin/libs/js-yaml/dist/js-yaml.min.js"></script> -<script type="text/javascript" src="{{ STATIC_URL }}merlin/libs/underscore/underscore-min.js"></script> -<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.init.js"></script> -<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.templates.js"></script> -<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.filters.js"></script> -<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.directives.js"></script> -<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.field.models.js"></script> -<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.panel.models.js"></script> -<script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.utils.js"></script> - -<script type="text/javascript" src="{{ STATIC_URL }}mistral/js/mistral.init.js"></script> -<script type="text/javascript" src="{{ STATIC_URL }}mistral/js/mistral.workbook.controllers.js"></script> -<script type="text/javascript" src="{{ STATIC_URL }}mistral/js/mistral.workbook.models.js"></script> - +{% block merlin-js %} + <script type="text/javascript" src="{{ STATIC_URL }}mistral/js/mistral.init.js"></script> + <script type="text/javascript" src="{{ STATIC_URL }}mistral/js/mistral.workbook.controllers.js"></script> + <script type="text/javascript" src="{{ STATIC_URL }}mistral/js/mistral.workbook.models.js"></script> {% endblock %} -{% block css %} - {% include "_stylesheets.html" %} - {% compress css %} - <link href='{{ STATIC_URL }}merlin/scss/merlin.scss' type='text/scss' media='screen' rel='stylesheet' /> - {% endcompress %} -<link href='{{ STATIC_URL }}merlin/libs/flexboxgrid/dist/flexboxgrid.css' type='text/css' media='screen' rel='stylesheet' /> - {% block merlin-css %}{% endblock %} -{% endblock %} +{% block merlin-css %}{% endblock %} {% block main %} <h3>Create Workbook</h3> diff --git a/merlin/static/merlin/js/merlin.directives.js b/merlin/static/merlin/js/merlin.directives.js index fa0e69d..57030b8 100644 --- a/merlin/static/merlin/js/merlin.directives.js +++ b/merlin/static/merlin/js/merlin.directives.js @@ -42,6 +42,7 @@ .directive('typedField', typedField) .directive('editableTitle', editableTitle) +.directive('draggableEntry', draggableEntry) .directive('labeled', labeled); function labeled() { @@ -230,4 +231,21 @@ } }; } + + function draggableEntry() { + return { + restrict: 'E', + scope: { + entry: '=' + }, + link: link, + templateUrl: '/static/merlin/templates/draggable-entry.html' + }; + + function link(scope, element, attrs) { + if (angular.isDefined(attrs.entryIcon)) { + scope.iconCls = 'fa-' + attrs.entryIcon; + } + } + } })(); diff --git a/merlin/static/merlin/scss/merlin.scss b/merlin/static/merlin/scss/merlin.scss index fa84b67..8db93e1 100644 --- a/merlin/static/merlin/scss/merlin.scss +++ b/merlin/static/merlin/scss/merlin.scss @@ -61,4 +61,15 @@ i.fa-times-circle { .section .section .section-heading & { margin-top: 7px; } -}; +} + +.has-feedback { + input { + width: 100%; + } + .form-control-feedback { + top: -5px; + right: 7px; + } +} + diff --git a/merlin/static/merlin/templates/draggable-entry.html b/merlin/static/merlin/templates/draggable-entry.html new file mode 100644 index 0000000..5ad7238 --- /dev/null +++ b/merlin/static/merlin/templates/draggable-entry.html @@ -0,0 +1,13 @@ +<div class="col-xs-12"> + <div class="row middle-xs"> + <div class="col-xs-10"> + <span ng-show="iconCls" class="fa" ng-class="iconCls"></span> + {$ entry.resource_type $} + </div> + <div class="add-btn add-entry col-xs"> + <button class="btn btn-default btn-sm" ng-click="onAdd()"> + <i class="fa fa-plus"></i></button> + </div> + </div> + +</div> diff --git a/merlin/templates/merlin/base.html b/merlin/templates/merlin/base.html new file mode 100644 index 0000000..785b0ad --- /dev/null +++ b/merlin/templates/merlin/base.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} +{% load compress %} + +{% block js %} + {% include "horizon/_scripts.html" %} + <script type="text/javascript" src="{{ STATIC_URL }}merlin/js/custom-libs/barricade.js"></script> + <script type="text/javascript" src="{{ STATIC_URL }}merlin/libs/js-yaml/dist/js-yaml.min.js"></script> + <script type="text/javascript" src="{{ STATIC_URL }}merlin/libs/underscore/underscore-min.js"></script> + <script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.init.js"></script> + <script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.templates.js"></script> + <script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.filters.js"></script> + <script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.directives.js"></script> + <script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.field.models.js"></script> + <script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.panel.models.js"></script> + <script type="text/javascript" src="{{ STATIC_URL }}merlin/js/merlin.utils.js"></script> + {% block merlin-js %}{% endblock %} +{% endblock %} + +{% block css %} + {% include "_stylesheets.html" %} + {% compress css %} + <link href='{{ STATIC_URL }}merlin/libs/flexboxgrid/dist/flexboxgrid.css' type='text/css' media='screen' rel='stylesheet' /> + <link href='{{ STATIC_URL }}merlin/scss/merlin.scss' type='text/scss' media='screen' rel='stylesheet' /> + {% endcompress %} + {% block merlin-css %}{% endblock %} +{% endblock %} diff --git a/package.json b/package.json index ad05b7e..9f95eeb 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,6 @@ "postinstall": "bower install", "test-unit": "grunt test:unit", "test": "karma start ./karma-unit.conf.js", - "lint": "eslint --no-color ./merlin/static ./extensions/mistral/static" + "lint": "eslint --no-color ./merlin/static ./extensions/mistral/static ./extensions/hotbuilder/static" } }