This closes #197
diff --git a/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html b/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html
index 67c0ce1..b105215 100644
--- a/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html
+++ b/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html
@@ -21,5 +21,5 @@
<div class="empty-tree text-muted text-center" ng-if="vm.applications.length === 0">
<hr />
<h4>No applications deployed yet.</h4>
- <p>You can create one via the <a href="/brooklyn-ui-blueprint-composer" class="btn btn-primary btn-xs">blueprint composer</a> or deploy an existing <a href="/" class="btn btn-primary btn-xs">catalog item</a>.</p>
+ <p>You can create one via the Composer by clicking "+" above or deploy an existing <a href="/" class="btn btn-primary btn-xs">catalog item</a>.</p>
</div>
diff --git a/ui-modules/app-inspector/app/views/main/main.controller.js b/ui-modules/app-inspector/app/views/main/main.controller.js
index c691a10..31502f8 100644
--- a/ui-modules/app-inspector/app/views/main/main.controller.js
+++ b/ui-modules/app-inspector/app/views/main/main.controller.js
@@ -23,17 +23,19 @@
name: 'main',
url: '/',
template: template,
- controller: ['$scope', '$q', 'brWebNotifications', mainController],
+ controller: ['$scope', '$q', 'brWebNotifications', 'brBrandInfo', mainController],
controllerAs: 'ctrl'
};
const savedSortReverse = 'app-inspector-sort-reverse';
-export function mainController($scope, $q, brWebNotifications) {
+export function mainController($scope, $q, brWebNotifications, brBrandInfo) {
$scope.$emit(HIDE_INTERSTITIAL_SPINNER_EVENT);
let ctrl = this;
+ ctrl.composerUrl = brBrandInfo.blueprintComposerBaseUrl;
+
ctrl.sortReverse = localStorage && localStorage.getItem(savedSortReverse) !== null ?
JSON.parse(localStorage.getItem(savedSortReverse)) :
true;
diff --git a/ui-modules/app-inspector/app/views/main/main.template.html b/ui-modules/app-inspector/app/views/main/main.template.html
index c7a90cd..f3bec97 100644
--- a/ui-modules/app-inspector/app/views/main/main.template.html
+++ b/ui-modules/app-inspector/app/views/main/main.template.html
@@ -45,7 +45,7 @@
<i class="fa fa-ban fa-stack-2x text-danger"></i>
</span>
</button>
- <a href="/brooklyn-ui-blueprint-composer" class="btn btn-sm btn-default entity-tree-action">
+ <a href="{{ ctrl.composerUrl }}" class="btn btn-sm btn-default entity-tree-action" ng-if="ctrl.composerUrl">
<i class="fa fa-plus"></i>
</a>
</div>
diff --git a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector-palette-footer.html b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector-palette-footer.html
index fd13c2a..2cb7ddf 100644
--- a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector-palette-footer.html
+++ b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector-palette-footer.html
@@ -23,43 +23,43 @@
<small class="help-block text-sm palette-footer-message" ng-class="{ 'no-match': searchedItems.length === 0 }">
<span ng-if="search">
<span ng-if="searchedItems.length === 0">
- <span ng-if="!skippingFilters && itemsBeforeActiveFilters.length > itemsAfterActiveFilters.length">
+ <div class="palette-footer-explanation" ng-if="!skippingFilters && itemsBeforeActiveFilters.length > itemsAfterActiveFilters.length"><span>
<strong>{{ itemsBeforeActiveFilters.length - itemsAfterActiveFilters.length }}
<ng-pluralize count="itemsBeforeActiveFilters.length" when="{ '1': 'item', 'other': 'items' }"></<ng-pluralize></strong>
matching search but excluded by filters.
<button class="btn btn-outline btn-info" ng-click="disableFilters()">Clear filters</button>
- </span>
- <span ng-if="skippingFilters || itemsBeforeActiveFilters.length === 0">
+ </span></div>
+ <div class="palette-footer-explanation" ng-if="skippingFilters || itemsBeforeActiveFilters.length === 0"><span>
No matches for <code>{{ search }}</code>.
- </span>
+ </span></div>
</span>
<span ng-if="searchedItems.length > 0">
- <span ng-if="skippingFilters">
+ <div ng-if="skippingFilters"><span>
<strong>No matches with selected filters.</strong><br/>
Showing matches ignoring filters.
- </span>
- <span ng-if="!skippingFilters && itemsBeforeActiveFilters.length > itemsAfterActiveFilters.length">
+ </span></div>
+ <div class="palette-footer-explanation" ng-if="!skippingFilters && itemsBeforeActiveFilters.length > itemsAfterActiveFilters.length"><span>
<strong>{{ itemsBeforeActiveFilters.length - itemsAfterActiveFilters.length }} more
<ng-pluralize count="itemsBeforeActiveFilters.length" when="{ '1': 'item', 'other': 'items' }"></<ng-pluralize></strong>
matching search but excluded by filters.
<button class="btn btn-outline btn-info" ng-click="disableFilters()">Clear filters</button>
- </span>
+ </span></div>
</span>
</span>
<span ng-if="!search">
- <span ng-if="itemsBeforeActiveFilters.length == 0">
+ <div class="palette-footer-explanation" ng-if="itemsBeforeActiveFilters.length == 0"><span>
<strong>Nothing available.</strong>
- </span>
- <span ng-if="skippingFilters && searchedItems.length > 0">
+ </span></div>
+ <div class="palette-footer-explanation" ng-if="skippingFilters && searchedItems.length > 0"><span>
<strong>Nothing available in selected filters.</strong><br/>
Ignoring filters.
- </span>
- <span ng-if="!skippingFilters && itemsBeforeActiveFilters.length > itemsAfterActiveFilters.length">
+ </span></div>
+ <div class="palette-footer-explanation" ng-if="!skippingFilters && itemsBeforeActiveFilters.length > itemsAfterActiveFilters.length"><span>
<strong>{{ itemsBeforeActiveFilters.length - itemsAfterActiveFilters.length }} more
<ng-pluralize count="itemsBeforeActiveFilters.length" when="{ '1': 'item', 'other': 'items' }"></<ng-pluralize></strong>
excluded by filters.
<button class="btn btn-outline btn-info" ng-click="disableFilters()">Clear filters</button>
- </span>
+ </span></div>
</span>
</small>
diff --git a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js
index 1b2fbdc..799c71b 100644
--- a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js
+++ b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js
@@ -346,9 +346,10 @@
if (!rowsPerPage) {
let palette = angular.element(document.querySelector(".page-main-area"));
let toolbar = angular.element(document.querySelector(".navbar-mode"));
+ let title = 56;
let header = angular.element($element[0].querySelector(".catalog-palette-header"));
let footer = angular.element($element[0].querySelector(".catalog-palette-footer"));
- rowsPerPage = Math.max(MIN_ROWS_PER_PAGE, Math.floor((palette[0].offsetHeight - (toolbar[0].offsetHeight + header[0].offsetHeight + footer[0].offsetHeight + 16)) / ($scope.state.viewMode.rowHeightPx || 96)));
+ rowsPerPage = Math.max(MIN_ROWS_PER_PAGE, Math.floor((palette[0].offsetHeight - (toolbar[0].offsetHeight + title + header[0].offsetHeight + footer[0].offsetHeight + 32)) / ($scope.state.viewMode.rowHeightPx || 96)));
}
$scope.$apply(() => $scope.pagination.itemsPerPage = rowsPerPage * $scope.state.viewMode.itemsPerRow);
}
diff --git a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.less b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.less
index 25f6cda..1a2ac11 100644
--- a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.less
+++ b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.less
@@ -233,6 +233,12 @@
// useful if it wraps, e.g. because pagination buttons show:
line-height: 2;
margin-top: -2px;
+ .palette-footer-explanation {
+ line-height: 1.5em;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ }
}
div[uib-pagination] {
margin-left: 1em;
@@ -344,4 +350,5 @@
p:last-child {
margin-bottom: 0;
}
+
}
diff --git a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.template.html b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.template.html
index 3cf0cc9..a78c9fd 100644
--- a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.template.html
+++ b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.template.html
@@ -162,7 +162,9 @@
<p><i class="mini-icon fa fa-fw fa-bookmark"></i> <samp class="type-symbolic-name">{{popover.symbolicName}}</samp></p>
<p ng-if="popover.version"><i class="mini-icon fa fa-fw fa-code-fork"></i> {{popover.version}}</p>
</div>
- <p class="quick-info-description" ng-if="popover.description">{{popover.description}}</p>
+ <p class="quick-info-description">
+ <md-first-line raw-data="::popover.description" raw-item="::popover"></md-first-line>
+ </p>
<p class="quick-info-description" ng-if="popover == freeFormTile">This is an ad hoc tile for an item entered by the user not known in the catalog.</p>
<div class="quick-info-metadata bundle">
<p ng-if="lastUsedText(popover)"><i class="mini-icon fa fa-clock-o"></i> {{ lastUsedText(popover) }}
diff --git a/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js b/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js
index a798d9c..19794ab 100644
--- a/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js
+++ b/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js
@@ -584,6 +584,9 @@
version: data.containingBundle.split(':')[1]
});
entity.miscData.set('typeName', data.displayName || data.symbolicName);
+ entity.miscData.set('displayName', data.displayName);
+ entity.miscData.set('symbolicName', data.symbolicName);
+ entity.miscData.set('description', data.description);
entity.miscData.set('config', data.config || []);
entity.miscData.set('parameters', data.parameters || []);
entity.miscData.set('sensors', data.sensors || []);
diff --git a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
index 9106aa8..9da1581 100644
--- a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
+++ b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
@@ -35,7 +35,7 @@
const REPLACED_DSL_ENTITYSPEC = '___brooklyn:entitySpec';
angular.module(MODULE_NAME, [onEnter, autoGrow, blurOnEnter, brooklynDslEditor, brooklynDslViewer])
- .directive('specEditor', ['$rootScope', '$templateCache', '$injector', '$sanitize', '$filter', '$log', '$sce', '$timeout', '$document', '$state', '$compile', 'blueprintService', 'composerOverrides', specEditorDirective])
+ .directive('specEditor', ['$rootScope', '$templateCache', '$injector', '$sanitize', '$filter', '$log', '$sce', '$timeout', '$document', '$state', '$compile', 'blueprintService', 'composerOverrides', 'mdHelper', specEditorDirective])
.filter('specEditorConfig', specEditorConfigFilter)
.filter('specEditorType', specEditorTypeFilter)
.run(['$templateCache', templateCache]);
@@ -89,7 +89,7 @@
'string', 'boolean', 'integer', 'double', 'duration', ' port'
];
-export function specEditorDirective($rootScope, $templateCache, $injector, $sanitize, $filter, $log, $sce, $timeout, $document, $state, $compile, blueprintService, composerOverrides) {
+export function specEditorDirective($rootScope, $templateCache, $injector, $sanitize, $filter, $log, $sce, $timeout, $document, $state, $compile, blueprintService, composerOverrides, mdHelper) {
return {
restrict: 'E',
scope: {
@@ -118,6 +118,7 @@
scope.REPLACED_DSL_ENTITYSPEC = REPLACED_DSL_ENTITYSPEC;
scope.parameters = [];
scope.config = {};
+ specEditor.descriptionVisible = false;
specEditor.paramTypes = PARAM_TYPES;
specEditor.getParameter = getParameter;
@@ -214,6 +215,11 @@
// Model
scope.$watch('model', (newVal, oldVal) => {
if (newVal && !newVal.equals(oldVal)) {
+ scope.modelDescription = mdHelper.analyzeDescription({
+ description: newVal.miscData.get('description'),
+ symbolicName: newVal.miscData.get('symbolicName'),
+ displayName: newVal.miscData.get('displayName'),
+ });
loadLocalConfigFromModel();
loadLocalParametersFromModel();
}
@@ -470,16 +476,6 @@
return issues.some(issue => issue.level === ISSUE_LEVEL.ERROR) ? 'badge-danger' : 'badge-warning';
};
- specEditor.descriptionHtml = (text) => {
- let out = [];
- for (let item of text.split(/\n\n+/)) {
- out.push('<div class="paragraph-spacing"></div>');
- out.push($sanitize(item));
- }
- out.splice(0, 1);
- return $sce.trustAsHtml(out.join("\n"));
- };
-
function getConfigWidgetModeInternal(item, val) {
if (angular.element($document[0].activeElement).hasClass("form-control") && item.widgetMode) {
// don't switch mode in mid-edit, e.g. if you are manually typing $brooklyn:component("x").config("y")
diff --git a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less
index 3a6e045..68fd6e3 100644
--- a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less
+++ b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less
@@ -32,7 +32,7 @@
cursor: pointer;
transition: 0.1s ease all;
&:hover {
- color: @brand-primary;
+ color: @brand-primary !important;
}
}
.remove-affordance() {
@@ -903,6 +903,15 @@
order: 99;
}
}
+
+ .type-description {
+ .toolbar-button-affordance();
+ padding-top: 1ex;
+ float: right;
+ &.active {
+ color: @gray;
+ }
+ }
}
.popover.spec-editor-popover {
diff --git a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html
index 6c46db0..19b0371 100644
--- a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html
+++ b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html
@@ -41,16 +41,32 @@
<img ng-src="{{model.icon}}" alt="{{model | entityName}} logo" class="media-object" />
</div>
<div class="media-body panel-header-body">
- <div title="{{ model.miscData.get('typeName') }}"><i class="fa fa-bookmark panel-header-icon"></i><samp class="entity-type-header">{{model.type}}</samp> <span class="label label-primary version">{{model | entityVersion}}</span></div>
+ <div title="{{ model.miscData.get('typeName') }}">
+ <i class="fa fa-question-circle type-description"
+ ng-class="{ 'active': specEditor.descriptionVisible }"
+ title="{{ specEditor.descriptionVisible ? 'Hide description' : 'Show description' }}"
+ ng-if="modelDescription.isPresent"
+ ng-click="specEditor.descriptionVisible = !specEditor.descriptionVisible"></i>
+ <i class="fa fa-bookmark panel-header-icon"></i><samp class="entity-type-header">{{model.type}}</samp>
+ <span class="label label-primary version">{{model | entityVersion}}</span>
+ </div>
<div ng-if="['POLICY', 'ENRICHER'].indexOf(model.family.id) === -1" class="identifier">
<i class="fa fa-id-card-o panel-header-icon"></i>
<input class="form-control editable" ng-model="model.id" placeholder="(no reference ID)" blur-on-enter />
</div>
+ <md-if-oneline ng-show="specEditor.descriptionVisible && modelDescription.isNonMultiline" data="modelDescription"></md-if-oneline>
</div>
</div>
</div>
</section>
+<br-collapsible state="true" ng-show="specEditor.descriptionVisible && modelDescription.isMultiline">
+ <heading>Description</heading>
+
+ <div style="margin-left: 3px;">
+ <md-if-multiline ng-show="specEditor.descriptionVisible" data="modelDescription"></md-if-multiline>
+ </div>
+</br-collapsible>
<!-- ENTITY PARAMETERS -->
<br-collapsible state="state.parameters.open" ng-if="!model.parent"> <!-- the ng-if is needed to make state update?! -->
@@ -672,7 +688,9 @@
<p><i class="mini-icon fa fa-fw fa-cog"></i> <samp class="type-symbolic-name">{{item.name}}</samp>
<span class="config-type label-color column-for-type oneline label label-success">{{item.type}}</span></p>
</div>
- <p class="quick-info-description" ng-if="item.description" ng-bind-html="specEditor.descriptionHtml(item.description)"></p>
+ <p class="quick-info-description">
+ <md-field raw-item="item"/>
+ </p>
<div class="quick-info-metadata config-default" ng-if="item.defaultValue"></i>Default value: <samp>{{item.defaultValue}}</samp></div>
</div>
@@ -686,7 +704,9 @@
<p><i class="mini-icon fa fa-fw fa-cog"></i> <samp class="type-symbolic-name">{{item.name}}</samp>
<span class="config-type label-color column-for-type oneline label label-success">{{item.type}}</span></p>
</div>
- <p class="quick-info-description" ng-if="item.description" ng-bind-html="specEditor.descriptionHtml(item.description)"></p>
+ <p class="quick-info-description" ng-if="item.description">
+ <md-field raw-item="item"/>
+ </p>
<div class="quick-info-metadata config-default" ng-if="item.defaultValue"></i>Default value: <samp>{{item.defaultValue}}</samp></div>
<div class="quick-info-metadata config-required" ng-if="item.constraints.required"><i>This field is required.</div>
</div>
diff --git a/ui-modules/blueprint-composer/app/index.js b/ui-modules/blueprint-composer/app/index.js
index e45c07f..7504627 100755
--- a/ui-modules/blueprint-composer/app/index.js
+++ b/ui-modules/blueprint-composer/app/index.js
@@ -35,6 +35,7 @@
import brooklynUserManagement from 'brooklyn-ui-utils/user-management/user-management';
import brYamlEditor from 'brooklyn-ui-utils/yaml-editor/yaml-editor';
import brUtils from 'brooklyn-ui-utils/utils/general';
+import mdHelper from 'brooklyn-ui-utils/md-helper';
import brSpecEditor from './components/spec-editor/spec-editor.directive';
import brooklynCatalogSaver from './components/catalog-saver/catalog-saver.directive';
@@ -79,7 +80,7 @@
angular.module('brooklynBlueprintComposer', [ngAnimate, ngResource, ngCookies, ngClipboard, uiRouter, 'ui.router.state.events', brCore,
brServerStatus, brAutoFocus, brIconGenerator, brInterstitialSpinner, brooklynModuleLinks, brooklynUserManagement,
- brYamlEditor, brUtils, brSpecEditor, brooklynCatalogSaver, brooklynApi, bottomSheet, stackViewer, brDragndrop,
+ brYamlEditor, brUtils, brSpecEditor, brooklynCatalogSaver, brooklynApi, bottomSheet, stackViewer, brDragndrop, mdHelper,
customActionDirective, customConfigSuggestionDropdown, paletteApiProvider, paletteServiceProvider, blueprintLoaderApiProvider,
breadcrumbs, catalogSelector, designer, objectCache, entityFilters, locationFilter, actionService, tabService, composerOverrides, blueprintService,
dslService, paletteDragAndDropService, recentlyUsedService, scriptTagDecorator, brandAngularJs])
diff --git a/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.less b/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.less
index 3e5c5b8..dfb6a30 100644
--- a/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.less
+++ b/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.less
@@ -147,6 +147,8 @@
&, .palette-full-height-wrapper {
display: flex;
flex-direction: column;
+ justify-content: space-between;
+
flex: 1 1 auto;
margin-left: 0;
margin-right: 0;
diff --git a/ui-modules/blueprint-importer/app/views/main/main.controller.js b/ui-modules/blueprint-importer/app/views/main/main.controller.js
index 959ca2c..ee269b6 100644
--- a/ui-modules/blueprint-importer/app/views/main/main.controller.js
+++ b/ui-modules/blueprint-importer/app/views/main/main.controller.js
@@ -123,7 +123,11 @@
type: item.symbolicName + ':' + item.version
}]
};
- return '/brooklyn-ui-blueprint-composer/#!/graphical?format=brooklyn-camp&yaml=' + JSON.stringify(yaml);
+ if (brBrandInfo.blueprintComposerBaseUrl) {
+ return brBrandInfo.blueprintComposerBaseUrl + '#!/graphical?format=brooklyn-camp&yaml=' + JSON.stringify(yaml);
+ } else {
+ return;
+ }
default:
return;
}
diff --git a/ui-modules/branding/brand.js b/ui-modules/branding/brand.js
index b38a221..c296f72 100644
--- a/ui-modules/branding/brand.js
+++ b/ui-modules/branding/brand.js
@@ -55,6 +55,7 @@
getAppDeployedUrl: function (appId, entityId) {
return '/brooklyn-ui-app-inspector/#!/application/' + appId + '/entity/' + entityId + '/summary';
},
+ blueprintComposerBaseUrl: '/brooklyn-ui-blueprint-composer/',
};
}
diff --git a/ui-modules/catalog/app/components/type-item/index.html b/ui-modules/catalog/app/components/type-item/index.html
index 93b57fb..2cffc7b 100644
--- a/ui-modules/catalog/app/components/type-item/index.html
+++ b/ui-modules/catalog/app/components/type-item/index.html
@@ -19,7 +19,7 @@
<div class="media">
<a ui-sref="bundle.type({bundleId: bundle.symbolicName, bundleVersion: bundle.version, typeId: type.symbolicName, typeVersion: type.version})"></a>
<div class="media-left media-middle icon">
- <img ng-src="{{type | iconGeneratorPipe:'symbolicName'}}" class="media-object" alt="{{::type.symbolicName}}'s logo" />
+ <img ng-src="{{ ::type | iconGeneratorPipe:'symbolicName' }}" class="media-object" alt="{{::type.symbolicName}}'s logo" />
</div>
<div class="media-body catalog-item-body">
<div class="btn-group pull-right" ng-if="showBundle || showType">
@@ -36,8 +36,8 @@
<span class="label label-default">{{::type.version}}</span>
</h4>
<span class="media-description">
- <span ng-if="type.displayName"><i class="fa fa-fw fa-bookmark sym-name"></i> <samp class="sym-name">{{::type.symbolicName}}</samp>: </span>
- {{::type.description}}
+ <span ng-if="type.displayName"><i class="fa fa-fw fa-bookmark sym-name"></i> <samp class="sym-name">{{::type.symbolicName}}</samp>{{::descriptionMarkdown.oneline ? ':' : ''}} </span>
+ <md-first-line data="descriptionMarkdown"></md-first-line>
</span>
</div>
</div>
diff --git a/ui-modules/catalog/app/components/type-item/index.js b/ui-modules/catalog/app/components/type-item/index.js
index c916458..145ada4 100644
--- a/ui-modules/catalog/app/components/type-item/index.js
+++ b/ui-modules/catalog/app/components/type-item/index.js
@@ -18,17 +18,18 @@
*/
import angular from 'angular';
import template from './index.html';
+import mdHelper from 'brooklyn-ui-utils/md-helper';
import brIconGenerator from 'brooklyn-ui-utils/icon-generator/icon-generator';
const MODULE_NAME = 'brooklyn.components.type-item';
-angular.module(MODULE_NAME, [brIconGenerator])
- .directive('typeItem', typeListDirective);
+angular.module(MODULE_NAME, [brIconGenerator, mdHelper])
+ .directive('typeItem', ['mdHelper', typeListDirective]);
export default MODULE_NAME;
-export function typeListDirective() {
+export function typeListDirective(mdHelper) {
return {
restrict: 'EA',
scope: {
@@ -37,6 +38,9 @@
showType: '<?',
showBundle: '<?'
},
- template: template
+ template: template,
+ controller: ["$scope", ($scope) => {
+ $scope.descriptionMarkdown = mdHelper.analyzeDescription($scope.type);
+ }],
};
}
diff --git a/ui-modules/catalog/app/index.js b/ui-modules/catalog/app/index.js
index b294236..9a78a70 100644
--- a/ui-modules/catalog/app/index.js
+++ b/ui-modules/catalog/app/index.js
@@ -28,6 +28,7 @@
import brooklynModuleLinks from 'brooklyn-ui-utils/module-links/module-links';
import brooklynUserManagement from "brooklyn-ui-utils/user-management/user-management";
import brooklynCatalogUpdater from 'brooklyn-ui-utils/catalog-uploader/catalog-uploader';
+import mdHelper from 'brooklyn-ui-utils/md-helper';
import uiRouter from 'angular-ui-router';
@@ -39,7 +40,7 @@
const IS_PRODUCTION = process.env.NODE_ENV === 'production' || false;
-angular.module('brooklynCatalog', [ngAnimate, ngCookies, ngResource, brCore, brServerStatus, brInterstitialSpinner, brooklynModuleLinks, brooklynUserManagement, brooklynCatalogUpdater, uiRouter, catalogState, catalogBundleState, catalogBundleTypeState, brandAngularJs])
+angular.module('brooklynCatalog', [ngAnimate, ngCookies, ngResource, brCore, brServerStatus, brInterstitialSpinner, brooklynModuleLinks, brooklynUserManagement, brooklynCatalogUpdater, uiRouter, catalogState, catalogBundleState, catalogBundleTypeState, brandAngularJs, mdHelper])
.config(['$logProvider', '$compileProvider', applicationConfig])
.config(['$urlRouterProvider', routerConfig])
.run(['$rootScope', '$state', 'brSnackbar', errorHandler])
diff --git a/ui-modules/catalog/app/views/bundle/bundle.state.js b/ui-modules/catalog/app/views/bundle/bundle.state.js
index 22ccaf0..fb7832e 100644
--- a/ui-modules/catalog/app/views/bundle/bundle.state.js
+++ b/ui-modules/catalog/app/views/bundle/bundle.state.js
@@ -67,6 +67,10 @@
search: {}
};
+ $scope.pagination = {
+ maxItemsToShow: 50
+ };
+
$scope.clearSearchFilters = () => {
$scope.state.search = {};
$scope.state.orderBy = orderBys[0];
@@ -84,8 +88,9 @@
});
};
- $scope.downloadBundle = () => {
- return catalogApi.downloadBundle($scope.bundle.symbolicName, $scope.bundle.version, {urlOnly: true});
+ $scope.downloadBundleUrl = () => {
+ return !$scope.bundle ? /* loading */ "" :
+ /* normal */ catalogApi.downloadBundle($scope.bundle.symbolicName, $scope.bundle.version, {urlOnly: true});
}
$scope.isNonEmpty = (o) => {
diff --git a/ui-modules/catalog/app/views/bundle/bundle.template.html b/ui-modules/catalog/app/views/bundle/bundle.template.html
index 47c7683..53ce2b9 100644
--- a/ui-modules/catalog/app/views/bundle/bundle.template.html
+++ b/ui-modules/catalog/app/views/bundle/bundle.template.html
@@ -21,8 +21,8 @@
<header class="row">
<div class="col-md-12">
<div class="media">
- <div class="media-left">
- <img ng-src="{{bundle | iconGeneratorPipe:'symbolicName'}}" class="media-object" alt="{{::bundle.symbolicName}}'s logo" />
+ <div class="media-left" ng-if="bundle.symbolicName">
+ <img ng-src="{{::bundle | iconGeneratorPipe:'symbolicName'}}" class="media-object" alt="{{::bundle.symbolicName}}'s logo" />
</div>
<div class="media-body">
<div class="pull-right highlights" ng-bind-html="bundle | bundleHighlights"></div>
@@ -37,7 +37,7 @@
</li>
</ul>
</div>
- <a class="btn btn-sm btn-default" ng-href="{{downloadBundle()}}">
+ <a class="btn btn-sm btn-default" ng-href="{{downloadBundleUrl()}}">
<i class="fa fa-fw fa-download"></i> Download
</a>
</h4>
@@ -104,7 +104,8 @@
<div class="col-md-12">
<ul class="list-group list-group-types">
- <li ng-repeat="type in bundle.types | filter:state.search | orderBy:state.orderBy.id as filteredTypes track by (type.containingBundle + type.symbolicName + type.version)"
+ <li ng-repeat="type in (bundle.types | filter:state.search | orderBy:state.orderBy.id) as filteredTypes track by (type.containingBundle + type.symbolicName + type.version)"
+ ng-if="$index < pagination.maxItemsToShow"
ng-class="{'deprecated': type.deprecated, 'disabled': type.disabled}"
class="list-group-item">
<type-item bundle="bundle" type="type" show-type="true"></type-item>
@@ -114,11 +115,21 @@
<h4>Loading types</h4>
</li>
<li ng-show="bundle.types.length === 0" class="list-group-item empty">
- <h4>There is no types associated to this bundle</h4>
+ <h4>There are no types provided by this bundle</h4>
</li>
<li ng-show="bundle.types.length > 0 && filteredTypes.length === 0" class="list-group-item no-results">
<h4>No results matching current filters</h4>
</li>
+
+ <!-- quick and dirty pagination; should improve / refactor / apply to others; but for now we are only dealing with bundles
+ that might have 1000 entities so only focused on that -->
+ <div ng-if="filteredTypes.length > pagination.maxItemsToShow" style="text-align: right;">
+ <i>Only showing {{ pagination.maxItemsToShow }} of {{ filteredTypes.length }}</i>
+
+ <a ng-click="pagination.maxItemsToShow = pagination.maxItemsToShow + 50">Show more</a>
+
+ <a ng-click="pagination.maxItemsToShow = filteredTypes.length">Show all</a>
+ </div>
</ul>
</div>
</div>
diff --git a/ui-modules/catalog/app/views/bundle/type/type.state.js b/ui-modules/catalog/app/views/bundle/type/type.state.js
index 478e439..99052e6 100644
--- a/ui-modules/catalog/app/views/bundle/type/type.state.js
+++ b/ui-modules/catalog/app/views/bundle/type/type.state.js
@@ -17,6 +17,7 @@
* under the License.
*/
import angular from 'angular';
+import ngSanitize from "angular-sanitize";
import template from './type.template.html';
import modalTemplate from './modal.template.html';
import brooklynTypeItem from '../../../components/type-item/index';
@@ -26,11 +27,12 @@
import brooklynQuickLaunch from 'brooklyn-ui-utils/quick-launch/quick-launch';
import brTable from 'brooklyn-ui-utils/table/index';
import brUtils from 'brooklyn-ui-utils/utils/general';
+import mdHelper from 'brooklyn-ui-utils/md-helper';
import {HIDE_INTERSTITIAL_SPINNER_EVENT} from 'brooklyn-ui-utils/interstitial-spinner/interstitial-spinner';
const MODULE_NAME = 'type.state';
-angular.module(MODULE_NAME, [brooklynCatalogApi, brooklynQuickLaunch, brooklynTypeItem, brUtils, brTable])
+angular.module(MODULE_NAME, [ngSanitize, brooklynCatalogApi, brooklynQuickLaunch, brooklynTypeItem, brUtils, brTable, mdHelper])
.provider('locationApi', locationApiProvider)
.config(['$stateProvider', typeStateConfig]);
@@ -40,7 +42,7 @@
name: 'bundle.type',
url: '/types/:typeId/:typeVersion',
template: template,
- controller: ['$scope', '$state', '$stateParams', '$q', '$uibModal', 'brBrandInfo', 'brUtilsGeneral', 'brSnackbar', 'catalogApi', typeController],
+ controller: ['$scope', '$state', '$stateParams', '$q', '$uibModal', 'brBrandInfo', 'brUtilsGeneral', 'brSnackbar', 'catalogApi', 'mdHelper', typeController],
controllerAs: 'ctrl'
};
@@ -48,7 +50,7 @@
$stateProvider.state(bundleState);
}
-export function typeController($scope, $state, $stateParams, $q, $uibModal, brBrandInfo, brUtilsGeneral, brSnackbar, catalogApi) {
+export function typeController($scope, $state, $stateParams, $q, $uibModal, brBrandInfo, brUtilsGeneral, brSnackbar, catalogApi, mdHelper) {
$scope.state = {
default: 2,
limit: 2
@@ -71,6 +73,8 @@
return brUtilsGeneral.isNonEmpty(o);
};
+ $scope.composerUrl = brBrandInfo.blueprintComposerBaseUrl;
+
$scope.deploy = (event)=> {
let instance = $uibModal.open({
template: modalTemplate,
@@ -125,6 +129,7 @@
typeVersion: typeVersion.version
};
});
+ $scope.typeDescription = mdHelper.analyze( ($scope.type || {}).description );
$scope.$emit(HIDE_INTERSTITIAL_SPINNER_EVENT);
}).catch(error => {
@@ -133,6 +138,7 @@
});
$scope.tables = {};
+ $scope.markdown = mdHelper.analyze;
['config', 'sensors', 'effectors'].forEach((t) => $scope.tables[t] = { columns: [] });
function addColumn(cols, base) {
@@ -167,6 +173,7 @@
field: 'description',
width: 150,
colspan: 6,
+ template: '<md-field raw-data="::item[column.field]"></md-field>',
tdClass: 'column-for-description',
} );
diff --git a/ui-modules/catalog/app/views/bundle/type/type.template.html b/ui-modules/catalog/app/views/bundle/type/type.template.html
index 028151e..f15a273 100644
--- a/ui-modules/catalog/app/views/bundle/type/type.template.html
+++ b/ui-modules/catalog/app/views/bundle/type/type.template.html
@@ -20,15 +20,15 @@
<header class="row">
<div class="col-md-12">
<div class="media">
- <div class="media-left">
- <img ng-src="{{type | iconGeneratorPipe:'symbolicName'}}" class="media-object" alt="{{::type.symbolicName}}'s logo" />
+ <div class="media-left" ng-if="type.symbolicName">
+ <img ng-src="{{::type | iconGeneratorPipe:'symbolicName'}}" class="media-object" alt="{{::type.symbolicName}}'s logo" />
</div>
<div class="media-body">
<h4 class="media-heading">
<i class="fa fa-star" ng-if="type.template" aria-label="This item is a template"></i>
{{::(type.displayName || type.symbolicName)}}
<div class="btn-group version-dropdown" uib-dropdown dropdown-append-to-body="true">
- <button uib-dropdown-toggle id="versions-dropdown" type="button" class="btn btn-sm btn-default"><span><i class="fa fa-code-fork"></i> {{::type.version}}</span>
+ <button uib-dropdown-toggle id="versions-dropdown" type="button" class="btn btn-sm btn-default"><span><i class="fa fa-code-fork"></i> {{::type.version}}</span>
<span class="caret" ng-if="versions && versions.length > 1"></span></button>
<ul class="dropdown-menu dropdown-menu-right versions-dropdown-list" uib-dropdown-menu role="menu" aria-labelledby="versions-dropdown">
<li role="menuitem" ng-repeat="version in versions track by (version.bundleSymbolicName + version.bundleVersion + version.typeVersion)" ng-class="{'active': version.bundleSymbolicName + ':' + version.bundleVersion === type.containingBundle && version.typeVersion === type.version}">
@@ -54,7 +54,7 @@
<i class="fa fa-fw fa-exclamation-circle"></i> This type has the following aliases:
<code ng-repeat="alias in type.aliases track by alias">{{::alias}}</code>
</small></p>
- <p class="media-description type-description">{{::type.description}}</p>
+ <md-if-oneline data="typeDescription"></md-if-oneline>
<p class="media-types">
<span ng-repeat="supertype in type.supertypes | limitTo:state.limit track by supertype" class="label label-supertype pull-left">
<i class="fa fa-fw fa-puzzle-piece "></i>
@@ -76,11 +76,11 @@
<div class="btn-group" uib-dropdown ng-if="isEditable()">
<button uib-dropdown-toggle type="button" class="btn btn-primary"><i class="fa fa-fw fa-edit"></i> Edit <span class="caret"></span></button>
<ul class="dropdown-menu dropdown-menu-right" uib-dropdown-menu role="menu" aria-labelledby="edit-dropdown">
- <li role="menuitem" ng-if="type.supertypes.includes('org.apache.brooklyn.api.entity.Entity')">
- <a ng-href="/brooklyn-ui-blueprint-composer/#!/graphical?bundleSymbolicName={{type.containingBundle.split(':')[0]}}&bundleVersion={{type.containingBundle.split(':')[1]}}&typeSymbolicName={{type.symbolicName}}&typeVersion={{type.version}}">Using graphical designer</a>
+ <li role="menuitem" ng-if="type.supertypes.includes('org.apache.brooklyn.api.entity.Entity') && composerUrl">
+ <a ng-href="{{ composerUrl }}#!/graphical?bundleSymbolicName={{type.containingBundle.split(':')[0]}}&bundleVersion={{type.containingBundle.split(':')[1]}}&typeSymbolicName={{type.symbolicName}}&typeVersion={{type.version}}">Using graphical designer</a>
</li>
- <li role="menuitem" ng-if="type.supertypes.includes('org.apache.brooklyn.api.entity.Entity')">
- <a ng-href="/brooklyn-ui-blueprint-composer/#!/yaml?bundleSymbolicName={{type.containingBundle.split(':')[0]}}&bundleVersion={{type.containingBundle.split(':')[1]}}&typeSymbolicName={{type.symbolicName}}&typeVersion={{type.version}}">Using YAML editor</a>
+ <li role="menuitem" ng-if="type.supertypes.includes('org.apache.brooklyn.api.entity.Entity') && composerUrl">
+ <a ng-href="{{ composerUrl }}#!/yaml?bundleSymbolicName={{type.containingBundle.split(':')[0]}}&bundleVersion={{type.containingBundle.split(':')[1]}}&typeSymbolicName={{type.symbolicName}}&typeVersion={{type.version}}">Using YAML editor</a>
</li>
<li role="menuitem" ng-if="type.supertypes.includes('org.apache.brooklyn.api.location.Location')">
<a ng-href="brooklyn-ui-location-manager/#!/location?symbolicName={{type.symbolicName}}&version={{type.version}}">Using location manager</a>
@@ -92,6 +92,11 @@
</button>
</div>
<uib-tabset active="tab">
+ <uib-tab heading="Description" ng-if="typeDescription.isMultiline">
+ <md-if-multiline data="::typeDescription"></md-if-multiline>
+ <div style="padding-bottom: 48px;"></div>
+ </uib-tab>
+
<uib-tab heading="Config" ng-if="isNonEmpty(type.config)">
<br-table ng-model="type.config" columns="tables.config.columns" col-width="20"></br-table>
</uib-tab>
diff --git a/ui-modules/catalog/app/views/catalog/catalog.state.js b/ui-modules/catalog/app/views/catalog/catalog.state.js
index f7a6072..9edd821 100644
--- a/ui-modules/catalog/app/views/catalog/catalog.state.js
+++ b/ui-modules/catalog/app/views/catalog/catalog.state.js
@@ -22,6 +22,7 @@
import brooklynTypeItem from '../../components/type-item/index';
import brUtils from 'brooklyn-ui-utils/utils/general';
import template from './catalog.template.html';
+import {analyzeDescription} from 'brooklyn-ui-utils/md-helper';
import {HIDE_INTERSTITIAL_SPINNER_EVENT} from 'brooklyn-ui-utils/interstitial-spinner/interstitial-spinner';
const MODULE_NAME = 'catalog.state';
@@ -88,8 +89,8 @@
}
$scope.pagination = {
- page: 1,
- itemsPerPage: 20
+ page: 1, // not used
+ itemsPerPage: 20 // used as an absolute limit
};
$scope.config = {
orderBy: savedOrderBy.orderBy === 'bundles' ? orderBysBundles : orderBysTypes
@@ -201,8 +202,10 @@
return;
}
if (input.description) {
- return input.description;
+ // bundles don't have description yet so this is moot, but when they do this will be nice - or better use the md-if-multiline widget from mdHelper
+ return analyzeDescription(input).oneline;
}
+
let alwaysGenerateDefaultDescription = true;
if (alwaysGenerateDefaultDescription || (input.symbolicName && input.symbolicName.startsWith('brooklyn-catalog-bom'))) {
// useful in anonymous case because the name gives no clue as to the title;
diff --git a/ui-modules/catalog/app/views/catalog/catalog.template.html b/ui-modules/catalog/app/views/catalog/catalog.template.html
index 5a726fa..cee272b 100644
--- a/ui-modules/catalog/app/views/catalog/catalog.template.html
+++ b/ui-modules/catalog/app/views/catalog/catalog.template.html
@@ -69,7 +69,7 @@
</div>
<div class="col-md-12">
- <ul class="list-group list-group-bundles" ng-show="state.view === 'bundles'">
+ <ul class="list-group list-group-bundles" ng-if="state.view === 'bundles'">
<li ng-repeat="bundle in bundles | filter:state.search | orderBy:state.orderBy.id as filteredBundles track by (bundle.symbolicName + bundle.version)"
ng-class="{'expanded': !isCollapsed, 'is-expandable': bundle.types.length > 0}"
class="list-group-item bundles-list">
@@ -98,6 +98,7 @@
</div>
<div class="extra" uib-collapse="isCollapsed">
+ <!-- note entities are not filtered as search changes; but they will take an initial search; not intuitive -->
<div ng-init="filteredEntities = (bundle.types | bundleTypeFilter:'org.apache.brooklyn.api.entity.Entity' | filter:state.search)" ng-show="::filteredEntities.length > 0">
<ul class="list-group list-group-types">
<li class="list-group-item typed-group-table-header">
@@ -105,11 +106,21 @@
when="{'one': 'Entity', 'other': 'Entities'}">
</ng-pluralize>
</li>
- <li ng-repeat="entity in filteredEntities track by (entity.containingBundle + entity.symbolicName + entity.version)"
+ <li ng-repeat="entity in ::filteredEntities track by (entity.containingBundle + entity.symbolicName + entity.version)"
+ ng-if="!isCollapsed && $index < pagination.itemsPerPage"
ng-class="{'deprecated': entity.deprecated, 'disabled': entity.disabled}"
class="list-group-item">
- <type-item bundle="bundle" type="entity"></type-item>
+ <type-item bundle="::bundle" type="::entity"></type-item>
</li>
+ <!-- quick and dirty pagination; should improve / refactor / apply to others; but for now we are only dealing with bundles
+ that might have 1000 entities so only focused on that -->
+ <div ng-if="::filteredEntities.length > pagination.itemsPerPage" style="text-align: right;">
+ <i>Only showing {{ pagination.itemsPerPage }} of {{ ::filteredEntities.length }}</i>
+
+ <a ng-click="pagination.itemsPerPage = pagination.itemsPerPage + 20">Show more</a>
+
+ <a ng-click="pagination.itemsPerPage = filteredEntities.length">Show all</a>
+ </div>
</ul>
</div>
<div ng-init="filteredPolicies = (bundle.types | bundleTypeFilter:'org.apache.brooklyn.api.policy.Policy' | filter:state.search)" ng-show="::filteredPolicies.length > 0">
@@ -165,7 +176,7 @@
</li>
</ul>
- <ul class="list-group list-group-types" ng-show="state.view === 'types'">
+ <ul class="list-group list-group-types" ng-if="state.view === 'types'">
<li ng-repeat="type in types | filter:state.search | orderBy:state.orderBy.id as filteredTypes track by (type.containingBundle + type.symbolicName + type.version)"
ng-class="{'deprecated': type.deprecated, 'disabled': type.disabled}"
class="list-group-item">
@@ -176,7 +187,7 @@
<h4>Loading types</h4>
</li>
<li ng-show="types.length === 0" class="list-group-item empty">
- <h4>There is no items in the catalog</h4>
+ <h4>There are no items in the catalog</h4>
<button class="btn btn-primary" ng-click="launchCatalogUploader()">Upload some</button>
</li>
<li ng-show="filteredTypes.length === 0" class="list-group-item no-results">
diff --git a/ui-modules/utils/icon-generator/icon-generator.js b/ui-modules/utils/icon-generator/icon-generator.js
index 0a20e94..5775da9 100644
--- a/ui-modules/utils/icon-generator/icon-generator.js
+++ b/ui-modules/utils/icon-generator/icon-generator.js
@@ -31,7 +31,10 @@
export default MODULE_NAME;
export function iconGeneratorProvider() {
- let useSessionStorage = true;
+ // session storage can fill up very quickly with icons - best not to use it.
+ // generation isn't that big a problem anyway, across tabs.
+ // actual icons are probably more expensive, but the browser will cache those for us.
+ let useSessionStorage = false;
let background = [0, 0, 0, 0];
let margin = 0.2;
let size = 128;
@@ -52,6 +55,10 @@
useSessionStorage = false;
return this;
},
+ enableSessionStorage: function () {
+ useSessionStorage = true;
+ return this;
+ },
$get: ['$cacheFactory', function ($cacheFactory) {
if (useSessionStorage && typeof(Storage) !== "undefined") {
return new IconGenerator(new SessionsStorageWrapper(CACHE_NAME), background, margin, size);
@@ -191,16 +198,38 @@
this.get = getValue;
this.put = putValue;
+ var disabled = null;
+ var count = 0;
+
+ // session storage can fill up very quickly with icons - so have some good error checking
+
function getValue(key, defaultValue) {
+ //if (count++ % 1000 < 10) console.log("SessionStorage access for "+key+": "+( sessionStorage.getItem(cacheName + '.' + key) ? "hit" : "miss" )+" (count "+count+")");
return sessionStorage.getItem(cacheName + '.' + key) || defaultValue || undefined;
}
function putValue(key, value) {
+ //if (count++ % 1000 < 10) console.log("SessionStorage write for "+key+": "+( sessionStorage.getItem(cacheName + '.' + key) ? "already present" : "not present" )+" (count "+count+")");
+ if (disabled) {
+ if (disabled + 60*1000 < Date.now()) {
+ console.log("Attempting to re-enable session storage");
+ disabled = null;
+ } else {
+ return;
+ }
+ }
try {
sessionStorage.setItem(cacheName + '.' + key, value);
} catch (ex) {
+ console.warn("Error setting cache value in session storage; will try clearing and retrying", cacheName, key, ex);
sessionStorage.clear();
- this.putValue(key, value);
+ try {
+ sessionStorage.setItem(cacheName + '.' + key, value);
+ console.log("Succeeded after clearing cache");
+ } catch (ex2) {
+ console.warn("Failed even after clearing cache; will disable session storage for a period", ex2);
+ disabled = Date.now();
+ }
}
}
}
diff --git a/ui-modules/utils/md-helper/index.js b/ui-modules/utils/md-helper/index.js
new file mode 100644
index 0000000..4890749
--- /dev/null
+++ b/ui-modules/utils/md-helper/index.js
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 angular from 'angular';
+import marked from 'marked';
+
+const MODULE_NAME = 'br.utils.md-helper';
+
+angular.module(MODULE_NAME, [])
+ .factory("mdHelper", mdHelperProvider)
+ .directive('mdField', [ mdFieldDirective])
+ .directive('mdFirstLine', [ mdFirstLineDirective])
+ .directive('mdIfOneline', [ mdIfOnelineDirective])
+ .directive('mdIfMultiline', [mdIfMultilineDirective]);
+
+export default MODULE_NAME;
+
+// displays markdown, as one-line or multi-line depending on the data
+export function mdFieldDirective() {
+ return {
+ restrict: 'E',
+ scope: {
+ data: '<',
+ rawData: '<',
+ rawItem: '<',
+ },
+ controller: ['$scope', populateData],
+ template: `
+ <div>
+ <md-if-oneline data="data"></md-if-oneline>
+ <md-if-multiline data="data"></md-if-multiline>
+ </div>
+ `,
+ };
+}
+
+// prints out one line of data -- in future could use markdown formatting, but currently does not
+export function mdFirstLineDirective() {
+ return {
+ restrict: 'E',
+ scope: {
+ data: '<',
+ rawData: '<',
+ rawItem: '<',
+ },
+ controller: ['$scope', populateData],
+ template: `
+ <span>{{::data.oneline}}</span>
+ `,
+ };
+}
+
+// prints out full, formatted, _if_ it is one line of data
+export function mdIfOnelineDirective() {
+ return {
+ restrict: 'E',
+ scope: {
+ data: '<',
+ rawData: '<',
+ rawItem: '<',
+ },
+ controller: ['$scope', populateData],
+ template: `
+ <div ng-if="data.isNonMultiline">
+ <p ng-if="data.unformatted">{{data.unformatted}}</p>
+ <div ng-if="data.markdownFormatted" ng-bind-html="data.markdownFormatted"></div>
+ </div>
+ `,
+ };
+}
+
+// prints out all the data, formatted, if multiline
+export function mdIfMultilineDirective() {
+ return {
+ restrict: 'E',
+ scope: {
+ data: '<',
+ rawData: '<',
+ rawItem: '<',
+ },
+ controller: ['$scope', populateData],
+ // for multiline, collapse margin from children eg an <h1> first element, inserting then removing a 24px margin
+ template: `
+ <div ng-if="data.isMultiline">
+ <pre ng-if="data.unformatted">{{data.unformatted}}</pre>
+ <div ng-if="data.markdownFormatted" style="margin-top: -24px;"><div style="margin-top: 24px;" ng-bind-html="data.markdownFormatted"></div></div>
+ </div>
+ `,
+ };
+}
+
+function nameFieldValues(src) {
+ src = src || {};
+ return ['symbolicName', 'displayName', 'typeName', 'name'].map(x => src[x]);
+}
+
+function populateData($scope) {
+ if (!$scope.data) {
+ if (!$scope.rawData && $scope.rawItem) {
+ $scope.rawData = $scope.rawItem.description;
+ }
+ $scope.data = analyze($scope.rawData, nameFieldValues($scope.rawItem));
+ }
+}
+
+export function analyzeDescription(input) {
+ input = input || {};
+ return analyze(input.description, nameFieldValues(input));
+}
+
+export function analyze(field, names) {
+ let result = {
+ isPresent: !!field,
+ };
+ result.isMultiline = !!(result.isPresent && (field.trim().match(/^#+($| )/) || field.split('\n', 5)>5));
+ result.isNonMultiline = result.isPresent && !result.isMultiline;
+ if (result.isPresent) {
+ try {
+ result.markdownFormatted = marked(field);
+ } catch (e) {
+ console.log("could not convert markdown; treading as unformatted", e);
+ // not markdown
+ result.unformatted = field;
+ }
+ }
+ result.oneline = oneline(field, names);
+ return result;
+}
+
+function containsAny(line, words) {
+ if (!words) return false;
+ return words.filter(w => w && line.indexOf(w)>=0);
+}
+
+export function oneline(field, names) {
+ if (!field) {
+ return null;
+ }
+ if (field.trim().match(/^#+($| )/)) {
+ // looks like markdown; skip line if it's a title
+ let inputStripped = field.trim().substring(1);
+ let line1 = inputStripped.split('\n', 1)[0].trim();
+
+ if (!line1 || (containsAny(line1, names) && line1.length<100)) {
+ // probably a summary eg "About FooEntity" -- ignore
+ inputStripped = /\n((.*)(\n.*)*)/.exec(inputStripped)[1]
+ if (!inputStripped) return null;
+ return oneline(inputStripped, names);
+ }
+
+ // otherwise use default behaviour
+
+ field = line1;
+ }
+
+ let dStrippedP = field.trim().split('\n', 2);
+ let d = dStrippedP[0];
+
+ if (d.length > 200) {
+ // if very long then truncate and return...
+ return d.substring(0,197)+"...";
+ } else {
+ if (dStrippedP[1] !== undefined) {
+ d += " ...";
+ }
+ return d;
+ }
+}
+
+function mdHelperProvider() {
+ return {
+ analyze,
+ analyzeDescription,
+ }
+}
\ No newline at end of file
diff --git a/ui-modules/utils/package.json b/ui-modules/utils/package.json
index a895f62..408731f 100644
--- a/ui-modules/utils/package.json
+++ b/ui-modules/utils/package.json
@@ -36,6 +36,7 @@
"angular": "^1.6.1",
"angular-animate": "^1.6.1",
"angular-multiple-transclusion": "^1.0.0",
+ "angular-sanitize": "^1.6.1",
"angular-ui-bootstrap": "^2.5.0",
"bootstrap": "^3.3.7",
"codemirror": "^5.27.2",
@@ -44,6 +45,7 @@
"jsonschema": "^1.1.1",
"jssha": "^2.2.0",
"lodash": "^4.15.0",
+ "marked": "^2.0.1",
"rxjs": "^5.0.0-beta.11"
},
"devDependencies": {
diff --git a/ui-modules/utils/quick-launch/quick-launch.js b/ui-modules/utils/quick-launch/quick-launch.js
index 89ea39f..907cffe 100644
--- a/ui-modules/utils/quick-launch/quick-launch.js
+++ b/ui-modules/utils/quick-launch/quick-launch.js
@@ -41,10 +41,10 @@
args: '=?', // default behaviour of code is: { noEditButton: false, noComposerButton: false, noCreateLocationLink: false, location: null }
callback: '=?',
},
- controller: ['$scope', '$http', '$location', 'brSnackbar', controller]
+ controller: ['$scope', '$http', '$location', 'brSnackbar', 'brBrandInfo', controller]
};
- function controller($scope, $http, $location, brSnackbar) {
+ function controller($scope, $http, $location, brSnackbar, brBrandInfo) {
$scope.deploying = false;
$scope.model = {
newConfigFormOpen: false,
@@ -287,13 +287,17 @@
}
function openComposer() {
+ if (!brBrandInfo.blueprintComposerBaseUrl) {
+ console.warn("Composer unavailable in this build");
+ return;
+ }
try {
- window.location.href = '/brooklyn-ui-blueprint-composer/#!/graphical?'+
+ window.location.href = brBrandInfo.blueprintComposerBaseUrl + '#!/graphical?'+
($scope.app.plan.format ? 'format='+encodeURIComponent($scope.app.plan.format)+'&' : '')+
'yaml='+encodeURIComponent(buildComposerYaml(true));
} catch (error) {
console.warn("Opening composer in YAML text editor mode because we cannot generate a model for this configuration:", error);
- window.location.href = '/brooklyn-ui-blueprint-composer/#!/yaml?'+
+ window.location.href = brBrandInfo.blueprintComposerBaseUrl + '#!/yaml?'+
($scope.app.plan.format ? 'format='+encodeURIComponent($scope.app.plan.format)+'&' : '')+
'yaml='+encodeURIComponent(
"# This plan may have items which require attention so is being opened in YAML text editor mode.\n"+