namespace_browser: Bookmarks and Top Recommendation UI polish.
This CL refactor the UI and the code around Bookmarks/Recommendations
and Items views.
Main UI Refactors:
-View switcher for Grid View, Tree View, Visualize View, Bookmarks,
Recommendations
https://screenshot.googleplex.com/4BagmtKoVG.png
https://screenshot.googleplex.com/bsoME6UJ6G.png
https://screenshot.googleplex.com/f2a8Rf9qJV.png
https://screenshot.googleplex.com/9bT6TMfe3X.png
https://screenshot.googleplex.com/exJOkG7vLV.png
https://screenshot.googleplex.com/rj1tpydVmu.png
-Bookmark action moved to the side panel with UNDO-able toast.
https://screenshot.googleplex.com/hyZoMWN32N.png
Main code refactors:
-Splitting browse component into several sub components
-Moving bookmark and recommendation business logic to a service layer
Also includes random bug fixes (UI and logic) as I noticed them during testing
Change-Id: Ic9dfd3267bbd3e71733d06e53ea40ef67ccd8f61
diff --git a/package.json b/package.json
index 2ff5b19..4dc04e3 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,7 @@
"mercury": "^12.0.0",
"routes": "^1.2.0",
"lodash": "~2.4.1",
- "xtend": "~4.0.0",
- "vis": "~3.7.1"
+ "vis": "~3.8.0",
+ "extend": "~2.0.0"
}
}
diff --git a/public/index.html b/public/index.html
index 77becb8..b202d1d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -7,10 +7,9 @@
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="description" content="">
<link href='//fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'>
- <title>Veyron Browser</title>
+ <title>Viz - Vanadium Viewer</title>
<link rel="import" href="bundle.html">
<head>
<body fullbleed>
-<div id="mynetwork"></div>
<script src="bundle.js"></script>
</body>
\ No newline at end of file
diff --git a/src/components/browse/bookmarks/index.js b/src/components/browse/bookmarks/index.js
new file mode 100644
index 0000000..f131ec2
--- /dev/null
+++ b/src/components/browse/bookmarks/index.js
@@ -0,0 +1,66 @@
+var mercury = require('mercury');
+
+var ItemCardList = require('../item-card-list/index');
+
+var bookmarksService = require('../../../services/bookmarks/service');
+
+var log = require('../../../lib/log')('components:browse:bookmarks');
+
+module.exports = create;
+module.exports.render = render;
+module.exports.load = load;
+
+/*
+ * Bookmark view
+ */
+function create() {
+
+ var state = mercury.varhash({
+ /*
+ * List of user-specified bookmark items to display
+ * @see services/namespace/item
+ * @type {Array<namespaceitem>}
+ */
+ bookmarkItems: mercury.array([])
+ });
+
+ return {
+ state: state
+ };
+}
+
+/*
+ * Renders the bookmark view
+ */
+function render(state, browseState, browseEvents, navEvents) {
+ return ItemCardList.render(
+ state.bookmarkItems,
+ browseState,
+ browseEvents,
+ navEvents, {
+ title: 'Bookmarks',
+ emptyText: 'No bookmarks.'
+ }
+ );
+}
+
+/*
+ * Does the initialization and loading of the data necessary to display the
+ * bookmarks.
+ * Called and used by the parent browse view to initialize the view on
+ * request.
+ * Returns a promise that will be resolved when loading is finished. Promise
+ * is used by the parent browse view to display a loading progressbar.
+ */
+function load(state) {
+ return new Promise(function(resolve, reject) {
+ bookmarksService.getAll()
+ .then(function bookmarksReceived(items) {
+ state.put('bookmarkItems', items);
+ items.events.on('end', resolve);
+ }).catch(function(err) {
+ log.error(err);
+ reject();
+ });
+ });
+}
\ No newline at end of file
diff --git a/src/components/browse/browse-namespace.js b/src/components/browse/browse-namespace.js
index eede491..c172a54 100644
--- a/src/components/browse/browse-namespace.js
+++ b/src/components/browse/browse-namespace.js
@@ -1,13 +1,17 @@
-var mercury = require('mercury');
-var guid = require('guid');
-var handleShortcuts = require('./handle-shortcuts');
-var recommendShortcuts = require('./recommend-shortcuts');
-var exists = require('../../lib/exists');
+var extend = require('extend');
+
+var Bookmarks = require('./bookmarks/index.js');
+var Recommendations = require('./recommendations/index.js');
+var Items = require('./items/index.js');
+
var log = require('../../lib/log')('components:browse:browse-namespace');
-var namespaceService = require('../../services/namespace/service');
module.exports = browseNamespace;
+// We keep track of previous namespace that was browsed to so we can
+// know when navigating to a different namespace happens.
+var previousNamespace;
+
/*
* Default event handler for the browseNamespace event.
* Updates the necessary states when browseNamespace is triggered
@@ -18,75 +22,81 @@
* }
*/
function browseNamespace(browseState, browseEvents, data) {
- if (exists(data.namespace)) {
- browseState.namespace.set(data.namespace);
+
+ var defaults = {
+ namespace: '',
+ globQuery: '',
+ subPage: 'items',
+ viewType: 'grid'
+ };
+
+ data = extend(defaults, data);
+
+ if (!Items.trySetViewType(browseState.items, data.viewType)) {
+ error404('Invalid view type: ' + data.viewType);
+ return;
}
- if (exists(data.globQuery)) {
- browseState.globQuery.set(data.globQuery);
- }
+ browseState.namespace.set(data.namespace);
+ browseState.globQuery.set(data.globQuery);
+ browseState.subPage.set(data.subPage);
var namespace = browseState.namespace();
-
- // Search the namespace and update the browseState's items.
- var requestId = guid.create().value;
- browseState.isFinishedLoadingItems.set(false);
- browseState.currentRequestId.set(requestId);
- browseState.put('items', mercury.array([]));
-
var globQuery = browseState.globQuery() || '*';
- namespaceService.search(namespace, globQuery).
- then(function globResultsReceived(items) {
- if (!isCurrentRequest()) {
- return;
- }
- browseState.put('items', items);
- items.events.on('end', searchFinished);
- items.events.on('streamError', searchFinished);
- }).catch(function(err) {
- searchFinished();
- browseEvents.error(err);
- log.error(err);
- });
+ var subPage = browseState.subPage();
- // Reload the user's shortcuts.
- handleShortcuts.load(browseState).catch(function(err) {
+ // When navigating to a different namespace, reset the currently selected item
+ if (previousNamespace !== namespace) {
+ browseState.selectedItemName.set(namespace);
+ }
+ previousNamespace = namespace;
+
+ browseState.isFinishedLoadingItems.set(false);
+
+ switch (subPage) {
+ case 'items':
+ Items.load(browseState.items, namespace, globQuery)
+ .then(loadingFinished)
+ .catch(onError.bind(null, 'items'));
+ break;
+ case 'bookmarks':
+ Bookmarks.load(browseState.bookmarks)
+ .then(loadingFinished)
+ .catch(onError.bind(null, 'bookmarks'));
+ break;
+ case 'recommendations':
+ Recommendations.load(browseState.recommendations)
+ .then(loadingFinished)
+ .catch(onError.bind(null, 'recommendations'));
+ break;
+ default:
+ browseState.subPage.set(defaults.subPage);
+ error404('Invalid page: ' + browseState.subPage());
+ return;
+ }
+
+ function onError(subject, err) {
+ var message = 'Could not load ' + subject;
browseEvents.toast({
- text: 'Could not load shortcuts',
+ text: message,
type: 'error'
});
- // TODO(alexfandrianto): I'd like to toast here, but our toasting mechanism
- // would only allow for 1 toast. The toast below would override this one.
- // Perhaps we should allow an array of toasts to be set?
- log.error('Could not load user shortcuts', err);
- });
+ log.error(message, err);
+ loadingFinished();
+ }
- // Update our shortcuts, as they may have changed.
- recommendShortcuts(browseState);
+ function error404(errMessage) {
+ log.error(errMessage);
+ //TODO(aghassemi) Needs to be 404 error when we have support for 404
+ browseEvents.error(new Error(errMessage));
+ }
- // Trigger display items event
- browseEvents.selectedItemDetails.displayItemDetails({
- name: data.namespace
- });
-
- // TODO(alexfandrianto): Example toast. Consider removing.
- browseEvents.toast({
- text: 'Browsing ' + data.namespace,
- action: browseNamespace.bind(null, browseState, browseEvents, data),
- actionText: 'REFRESH'
- });
-
- function searchFinished() {
- if (!isCurrentRequest()) {
- return;
- }
+ function loadingFinished() {
browseState.isFinishedLoadingItems.set(true);
}
- // Whether were are still the current request. This is used to ignore out of
- // order return of async calls where user has moved on to another item
- // by the time previous requests result comes back.
- function isCurrentRequest() {
- return browseState.currentRequestId() === requestId;
- }
+ // Update the right side
+ browseEvents.selectedItemDetails.displayItemDetails({
+ name: browseState.selectedItemName()
+ });
}
\ No newline at end of file
diff --git a/src/components/browse/get-service-icon.js b/src/components/browse/get-service-icon.js
deleted file mode 100644
index e58b392..0000000
--- a/src/components/browse/get-service-icon.js
+++ /dev/null
@@ -1,15 +0,0 @@
-module.exports = getServiceIcon;
-
-var serviceIconMap = Object.freeze({
- 'veyron-mounttable': ['social:circles-extended', 'social:circles-extended'],
- 'veyron-unknown': ['cloud-queue', 'cloud'],
- '': ['folder-open', 'folder']
-});
-
-/*
- * Given the type of a service and whether the element should be filled or not,
- * return the name of the corresponding core-icon to use for rendering.
- */
-function getServiceIcon(type, fill) {
- return serviceIconMap[type][fill ? 1 : 0];
-}
\ No newline at end of file
diff --git a/src/components/browse/handle-shortcuts.js b/src/components/browse/handle-shortcuts.js
deleted file mode 100644
index 9bc4709..0000000
--- a/src/components/browse/handle-shortcuts.js
+++ /dev/null
@@ -1,102 +0,0 @@
-var mercury = require('mercury');
-var _ = require('lodash');
-var arraySet = require('../../lib/arraySet');
-var recommendShortcuts = require('./recommend-shortcuts');
-var log = require('../../lib/log')('components:browse:handle-shortcuts');
-var store = require('../../lib/store');
-var namespaceService = require('../../services/namespace/service');
-
-module.exports = {
- load: loadShortcuts,
- set: setShortcut,
- find: findShortcut
-};
-
-// Data is loaded from and saved to this key in the store.
-var userShortcutsID = 'user-shortcuts';
-
-/*
- * Returns a promise that loads the user's shortcuts into the browse state.
- */
-function loadShortcuts(browseState) {
- return loadShortcutKeys().then(function getShortcuts(rawShortcuts) {
- rawShortcuts = rawShortcuts || [];
-
- // Clear out the old shortcuts and fill them with new ones.
- browseState.put('userShortcuts', mercury.array([]));
-
- rawShortcuts.forEach(function(rawShortcut, i) {
- namespaceService.getNamespaceItem(rawShortcut).then(
- function(shortcut) {
- browseState.userShortcuts.put(i, shortcut);
- }
- ).catch(function(err) {
- // TODO(alexfandrianto): We should find a way to indicate that the
- // service is not accessible at the moment. A toast is not enough.
- log.error('Could not load shortcut', rawShortcut, err);
- });
- });
- }).catch(function(err) {
- log.error('Unable to load user shortcuts', err);
- return Promise.reject(err);
- });
-}
-
-/*
- * Returns a promise that resolves to an array of user-defined shortcut keys.
- */
-function loadShortcutKeys() {
- return store.getValue(userShortcutsID);
-}
-
-/*
- * Returns a promise that saves the new status of the shortcut key to the store.
- */
-function saveShortcutKey(key, shouldSet) {
- return loadShortcutKeys().then(function(keys) {
- keys = keys || []; // Initialize the shortcuts, if none were loaded.
- arraySet.set(keys, key, shouldSet);
- return store.setValue(userShortcutsID, keys);
- });
-}
-
-/*
- * Update the given browseState with the shortcut information in the given data.
- * data should have 'save' (boolean) and 'item' (@see services/namespace/item).
- *
- * This update is done asynchronously; to maintain consistency, shortcuts are
- * refreshed before they are persisted. Additionally, what is rendered in the
- * browseState may not perfectly match the data present in the store.
- */
-function setShortcut(browseState, browseEvents, data) {
- // Update the user shortcuts, as the data specifies.
- arraySet.set(
- browseState.userShortcuts,
- data.item,
- data.save,
- findShortcut.bind(null, browseState())
- );
-
- // The recommended shortcuts may have changed too.
- recommendShortcuts(browseState);
-
- // Persist the updated user shortcut.
- return saveShortcutKey(data.item.objectName, data.save).catch(function(err) {
- browseEvents.toast({
- text: 'Error while modifying shortcut',
- type: 'error'
- });
- log.error('Error while modifying shortcut', err);
- });
-}
-
-/*
- * Check the browseState for the index of the given item. -1 if not present.
- * Note: browseState should be observed.
- */
-function findShortcut(browseState, item) {
- return _.findIndex(browseState.userShortcuts, function(shortcut) {
- // Since shortcuts can be assigned out of order, check for undefined.
- return shortcut !== undefined && item.objectName === shortcut.objectName;
- });
-}
\ No newline at end of file
diff --git a/src/components/browse/index.css b/src/components/browse/index.css
index bc1afd6..fb7337b 100644
--- a/src/components/browse/index.css
+++ b/src/components/browse/index.css
@@ -32,86 +32,27 @@
background-color: var(--color-grey-very-light);
border-left: solid 1px var(--color-divider);
}
-.items-container {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- border-bottom: var(--border);
- padding-bottom: 0.5em;
+
+.browse-main-wrapper paper-progress {
+ position: absolute;
+ z-index: 500;
+ opacity: 0.6;
}
-.items-container:last-child {
- border-bottom: none;
-}
-.items-container paper-progress{
+
+.browse-main-wrapper core-tooltip {
position: absolute;
}
-.items-container h2 {
+
+.browse-main-wrapper h2 {
width: 100%;
font-size: var(--size-font-large);
color: var(--color-text-heading);
padding: 0.5em 0em 0em 0.75em;
text-decoration: none;
}
-.item.card {
- box-sizing: border-box;
- background-color: var(--color-white);
- display: flex;
- flex-shrink: 0;
- flex-grow: 0;
- height: 2.5em;
- min-width: 8em;
- margin: 0.75em;
- border-radius: 3px;
- overflow: hidden;
- position: relative;
- box-shadow: var(--shadow-all-around);
- border: var(--border);
-}
-.item .label, .item .drill {
- display: flex;
- flex-direction: row;
- align-items: center;
- padding: 0.5em;
-}
-.item .label {
- text-decoration: none;
- flex: 1;
- overflow: hidden;
- white-space: nowrap;
-}
-.item .drill {
- width: 1.5em;
- background-color: var(--color-grey-light);
- border-left: var(--border);
-}
-.item.selected .drill {
- background-color: var(--color-bright);
-}
-
-.item a:hover, .item a:focus{
- opacity: 0.7;
-}
-
-.item.card .icon {
- align-self: center;
- padding-right: 0.5em;
-}
-
-.item.inaccessible {
- opacity: 0.5;
-}
-
-.item.selected.card {
- background-color: var(--color-bright);
- color: var(--color-text-primary-invert);
-}
-
-.tooltip::shadow .core-tooltip {
- width: 36em;
- line-height: 0.9em; /* overrides Polymer's 6px line-height */
- white-space: pre-wrap;
- font-size: var(--size-font-xsmall);
+.progress-tooltip {
+ width: 100%;
}
.breadcrumbs {
@@ -124,6 +65,7 @@
}
.breadcrumb-item {
+ font-size: var(--size-font-small);
overflow: hidden;
flex-shrink: 1;
flex-grow: 0;
@@ -144,27 +86,21 @@
.breadcrumb-item:before
{
content: '/';
- padding:0 0.5em;
+ padding:0 var(--size-space-xxsmall);
display: inline-block;
color: var(--color-text-secondary);
}
+
.breadcrumb-item:first-child:before
{
content: ' ';
}
-.empty {
- padding: 1em;
- text-align: center;
- color: var(--color-text-secondary);
-}
-
.namespace-box {
background: var(--color-white-transparent);
font-size: var(--size-font-xsmall);
- height: 2.7em;
- padding: 0 0.8em;
- margin-left: 3em !important;
+ padding: var(--size-space-xxsmall) var(--size-space-small);
+ margin-left: var(--size-space-xxlarge) !important;
border-radius: 1px;
}
@@ -172,20 +108,43 @@
color: inherit
}
+.icon-group {
+ white-space: nowrap;
+}
+
+.vertical-ruler {
+ display: inline-block;
+ width: 1px;
+ background-color: var(--color-divider);
+ margin:0 var(--size-space-xxsmall);
+}
+
+.icon-group paper-icon-button {
+ vertical-align: middle;
+ color: var(--color-text-secondary);
+ padding: var(--size-space-xxsmall);
+ margin:0 var(--size-space-xxsmall);
+}
+
+.icon-group paper-icon-button:hover, .icon-group paper-icon-button:focus {
+ background: #eee;
+ border-radius: 50%;
+}
+
.search-box {
white-space: nowrap;
- width: var(--size-input-width-normal);
+ width: var(--size-input-width-small);
+ margin-left: var(--size-space-xxsmall);
}
.search-box core-tooltip {
width: 100%;
- font-size: var(--size-font-xsmall);
}
.search-box .icon, .namespace-box .icon {
align-self: flex-end;
- margin-right: 0.5em;
color: var(--color-text-secondary);
+ margin-right: var(--size-space-xxsmall);
}
.search-box .input {
@@ -204,21 +163,6 @@
transform: scale(0.7);
padding-right: 0.75em;
}
-/* The service icon is colored brightly if active. */
-.service-type-icon:hover,
-.service-type-icon.shortcut {
- color: var(--color-bright);
-}
-/* If the service item is selected and is also active, make the icon's colors deeper. */
-.item.selected.card .service-type-icon:hover,
-.item.selected.card .service-type-icon.shortcut {
- color: var(--color-bright-deep);
-}
-/* If hovering over a shortcut, use the parent's color. */
-.service-type-icon.shortcut:hover,
-.item.selected.card .service-type-icon.shortcut:hover {
- color: inherit;
-}
paper-autocomplete {
width: var(--size-input-width-normal);
diff --git a/src/components/browse/index.js b/src/components/browse/index.js
index c68f78d..9bfc059 100644
--- a/src/components/browse/index.js
+++ b/src/components/browse/index.js
@@ -1,19 +1,29 @@
var mercury = require('mercury');
var insertCss = require('insert-css');
+
var AttributeHook = require('../../lib/mercury/attribute-hook');
var PropertyValueEvent = require('../../lib/mercury/property-value-event');
+
var exists = require('../../lib/exists');
-var log = require('../../lib/log')('components:browse');
-var browseRoute = require('../../routes/browse');
-var browseNamespace = require('./browse-namespace');
-var getNamespaceSuggestions = require('./get-namespace-suggestions');
-var getServiceIcon = require('./get-service-icon');
-var handleShortcuts = require('./handle-shortcuts');
-var itemDetailsComponent = require('./item-details/index');
+
var namespaceService = require('../../services/namespace/service');
var smartService = require('../../services/smart/service');
-var css = require('./index.css');
+var browseRoute = require('../../routes/browse');
+var bookmarksRoute = require('../../routes/bookmarks');
+var recommendationsRoute = require('../../routes/recommendations');
+
+var ItemDetails = require('./item-details/index');
+var Items = require('./items/index');
+var Bookmarks = require('./bookmarks/index');
+var Recommendations = require('./recommendations/index');
+
+var browseNamespace = require('./browse-namespace');
+var getNamespaceSuggestions = require('./get-namespace-suggestions');
+
+var log = require('../../lib/log')('components:browse');
+
+var css = require('./index.css');
var h = mercury.h;
module.exports = create;
@@ -26,14 +36,17 @@
function create() {
loadLearners();
- var selectedItemDetails = itemDetailsComponent();
+ var selectedItemDetails = new ItemDetails();
+ var bookmarks = new Bookmarks();
+ var recommendations = new Recommendations();
+ var items = new Items();
var state = mercury.varhash({
/*
* Veyron namespace being displayed and queried
* @type {string}
*/
- namespace: mercury.value(''), //TODO(aghassemi) temp
+ namespace: mercury.value(''),
/*
* Glob query applied to the Veyron namespace
@@ -60,38 +73,19 @@
namespacePrefix: mercury.value(''),
/*
- * List of namespace items to display
- * @see services/namespace/item
- * @type {Array<namespaceitem>}
+ * State of the bookmarks component
*/
- items: mercury.array([]),
+ bookmarks: bookmarks.state,
/*
- * Whether loading items has finished.
- * @type {Boolean}
+ * State of the recommendation component
*/
- isFinishedLoadingItems: mercury.value(false),
+ recommendations: recommendations.state,
/*
- * uuid for the current browse-namespace request.
- * Needed to handle out-of-order return of async calls.
- * @type {String}
+ * State of the items component
*/
- currentRequestId: mercury.value(''),
-
- /*
- * List of user-specified shortcuts to display
- * @see services/namespace/item
- * @type {Array<namespaceitem>}
- */
- userShortcuts: mercury.array([]),
-
- /*
- * List of recommended shortcuts to display
- * @see services/namespace/item
- * @type {Array<namespaceitem>}
- */
- recShortcuts: mercury.array([]),
+ items: items.state,
/*
* State of the selected item-details component
@@ -101,7 +95,19 @@
/*
* Name of currently selected item
*/
- selectedItemName: mercury.value(''),
+ selectedItemName: mercury.value(''),
+
+ /*
+ * Whether loading items has finished.
+ * @type {Boolean}
+ */
+ isFinishedLoadingItems: mercury.value(false),
+
+ /*
+ * Specifies what sub page is currently displayed.
+ * One of: items, bookmarks, recommendations
+ */
+ subPage: mercury.value('items')
});
@@ -110,7 +116,7 @@
* Indicates a request to browse the Veyron namespace
* Data of form:
* {
- * namespace: '/veyron/name/space',
+ * namespace: '/namespace-root:8881/name/space',
* globQuery: '*',
* }
* is expected as data for the event
@@ -123,16 +129,35 @@
'getNamespaceSuggestions',
/*
- * Indicates that the user is setting/removing a shortcut.
+ * Selects an items.
+ * Data of form:
+ * {
+ * name: 'object/name'
+ * }
*/
- 'setShortcut',
-
- 'selectedItemDetails',
-
'selectItem',
+ /*
+ * Events for the ItemDetails component
+ */
+ 'selectedItemDetails',
+
+ /*
+ * Displays an error
+ * Data of should be an Error object.
+ */
'error',
+ /*
+ * Displays a toast
+ * Data of form:
+ * {
+ text: 'Saved',
+ type: 'error',
+ action: function undo(){ // },
+ actionText: 'UNDO'
+ * }
+ */
'toast'
]);
@@ -148,17 +173,11 @@
/*
* Loads the learners into the smart service upon creation of this component.
+ * TODO(aghassemi), TODO(alexfandrianto) Move this into service layers, similar
+ * to how `learner-shortcut` is now loaded in the recommendations service.
*/
function loadLearners() {
smartService.loadOrCreate(
- 'learner-shortcut',
- smartService.constants.LEARNER_SHORTCUT, {
- k: 3
- }
- ).catch(function(err) {
- log.error(err);
- });
- smartService.loadOrCreate(
'learner-method-input',
smartService.constants.LEARNER_METHOD_INPUT, {
minThreshold: 0.2,
@@ -183,6 +202,102 @@
* namespace root.
*/
function renderHeader(browseState, browseEvents, navEvents) {
+ return h('div', [
+ renderNamespaceBox(browseState, browseEvents, navEvents)
+ ]);
+}
+
+/*
+ * Renders the main body of the namespace browser.
+ * A toolbar is rendered on top of the mainView and sideView showing the current
+ * position in the namespace as well as a globquery searchbox.
+ * The mainView contains the shortcuts and names at this point in the namespace.
+ * The sideView displays the detail information of the selected name.
+ */
+function render(browseState, browseEvents, navEvents) {
+ insertCss(css);
+
+ var sideView = [
+ ItemDetails.render(
+ browseState.selectedItemDetails,
+ browseEvents.selectedItemDetails
+ )
+ ];
+
+ var mainView;
+ switch (browseState.subPage) {
+ case 'items':
+ mainView = Items.render(browseState.items, browseState,
+ browseEvents, navEvents);
+ break;
+ case 'bookmarks':
+ mainView = Bookmarks.render(browseState.bookmarks,
+ browseState, browseEvents, navEvents);
+ break;
+ case 'recommendations':
+ mainView = Recommendations.render(browseState.recommendations,
+ browseState, browseEvents, navEvents);
+ break;
+ default:
+ log.error('Unsupported subPage ' + browseState.subPage);
+ }
+
+ // add progressbar and wrap in a container
+ var progressbar;
+ if (!browseState.isFinishedLoadingItems) {
+ progressbar = h('core-tooltip.progress-tooltip', {
+ 'label': new AttributeHook('Loading items...'),
+ 'position': new AttributeHook('bottom')
+ }, h('paper-progress.delayed', {
+ 'indeterminate': new AttributeHook(true),
+ 'aria-label': new AttributeHook('Loading items')
+ }));
+ }
+
+ mainView = h('div.browse-main-wrapper', [
+ progressbar,
+ mainView
+ ]);
+
+ var sideViewWidth = '50%';
+ var view = [
+ h('core-toolbar.browse-toolbar', [
+ renderBreadcrumbs(browseState, navEvents),
+ renderViewActions(browseState, navEvents)
+ ]),
+ h('core-drawer-panel', {
+ 'rightDrawer': new AttributeHook(true),
+ 'drawerWidth': new AttributeHook(sideViewWidth),
+ 'responsiveWidth': new AttributeHook('0px')
+ }, [
+ h('core-header-panel.browse-main-panel', {
+ 'main': new AttributeHook(true)
+ }, [
+ mainView
+ ]),
+ h('core-header-panel.browse-details-sidebar', {
+ 'drawer': new AttributeHook(true)
+ }, [
+ sideView
+ ])
+ ])
+ ];
+
+ return h('core-drawer-panel', {
+ 'drawerWidth': new AttributeHook('0px')
+ }, [
+ h('core-header-panel', {
+ 'main': new AttributeHook(true)
+ }, [
+ view
+ ])
+ ]);
+}
+
+/*
+ * Renders the addressbar for entering namespace
+ */
+function renderNamespaceBox(browseState, browseEvents, navEvents) {
// Trigger an actual navigation event when value of the inputs change
var changeEvent = new PropertyValueEvent(function(val) {
var namespace = browseState.namespace;
@@ -190,7 +305,9 @@
namespace = val;
}
navEvents.navigate({
- path: browseRoute.createUrl(namespace)
+ path: browseRoute.createUrl(browseState, {
+ namespace: namespace
+ })
});
}, 'value', true);
@@ -231,86 +348,65 @@
);
}
+function createActionIcon(tooltip, icon, href) {
+ var view = h('core-tooltip', {
+ 'label': tooltip,
+ 'position': 'bottom'
+ },
+ h('a', {
+ 'href': new AttributeHook(href)
+ }, h('paper-icon-button.icon', {
+ 'icon': new AttributeHook(icon)
+ }))
+ );
+
+ return view;
+}
+
/*
- * Renders the main body of the namespace browser.
- * A toolbar is rendered on top of the mainView and sideView showing the current
- * position in the namespace as well as a globquery searchbox.
- * The mainView contains the shortcuts and names at this point in the namespace.
- * The sideView displays the detail information of the selected name.
+ * Renders the view switchers for different views and bookmarks, recommendations
*/
-function render(browseState, browseEvents, navEvents) {
- insertCss(css);
+function renderViewActions(browseState, navEvents) {
- var sideView = [
- itemDetailsComponent.render(
- browseState.selectedItemDetails,
- browseEvents.selectedItemDetails
+ var switchGroup = h('div.icon-group', [
+ createActionIcon('Grid view', 'apps',
+ browseRoute.createUrl(browseState, {
+ viewType: 'grid'
+ })
+ ),
+ createActionIcon('Tree view', 'list',
+ browseRoute.createUrl(browseState, {
+ viewType: 'tree'
+ })
+ ),
+ createActionIcon('Visualize view', 'social:circles-extended',
+ browseRoute.createUrl(browseState, {
+ viewType: 'visualize'
+ })
)
- ];
-
- var mainView = [
- h('div.items-container', [
- h('h2', 'Bookmarks'),
- renderUserShortcuts(browseState, browseEvents, navEvents)
- ]),
- h('div.items-container', [
- h('h2', 'Top Recommendations'),
- renderRecommendedShortcuts(browseState, browseEvents, navEvents)
- ])
- ];
-
- var sideViewWidth = '50%';
- var progressbar;
- if( !browseState.isFinishedLoadingItems ) {
- progressbar = h('paper-progress.delayed', {
- 'indeterminate': new AttributeHook(true),
- 'aria-label': new AttributeHook('Loading namespace items')
- });
- }
- if (browseState.isFinishedLoadingItems && browseState.items.length === 0) {
- mainView.push(h('div.empty',
- h('span',(browseState.globQuery ? 'No search results' : 'No children')))
- );
- } else {
- mainView.push(h('div.items-container', [
- progressbar,
- h('h2', (browseState.globQuery ? 'Search results' : 'Children')),
- renderItems(browseState, browseEvents, navEvents)
- ]));
- }
-
- var view = [
- h('core-toolbar.browse-toolbar', [
- renderBreadcrumbs(browseState, navEvents),
- renderSearch(browseState, navEvents)
- ]),
- h('core-drawer-panel', {
- 'rightDrawer': new AttributeHook(true),
- 'drawerWidth': new AttributeHook(sideViewWidth),
- 'responsiveWidth': new AttributeHook('0px')
- }, [
- h('core-header-panel.browse-main-panel', {
- 'main': new AttributeHook(true)
- }, [
- mainView
- ]),
- h('core-header-panel.browse-details-sidebar', {
- 'drawer': new AttributeHook(true)
- }, [
- sideView
- ])
- ])
- ];
-
- return h('core-drawer-panel', {
- 'drawerWidth': new AttributeHook('0px')
- }, [
- h('core-header-panel', {
- 'main': new AttributeHook(true)
- }, [
- view
- ])
]);
+ var ruler = h('div.vertical-ruler');
+ var bookmarkGroup = h('div.icon-group', [
+ createActionIcon('Bookmarks', 'bookmark-outline',
+ bookmarksRoute.createUrl()
+ ),
+ createActionIcon('Recommendations', 'social:whatshot',
+ recommendationsRoute.createUrl()
+ )
+ ]);
+ var searchGroup = renderSearch(browseState, navEvents);
+ var view = h('div', {
+ 'layout': new AttributeHook('true'),
+ 'horizontal': new AttributeHook('true')
+ }, [
+ switchGroup,
+ ruler,
+ bookmarkGroup,
+ ruler,
+ searchGroup
+ ]);
+
+ return view;
}
/*
@@ -319,22 +415,23 @@
function renderSearch(browseState, navEvents) {
// Trigger an actual navigation event when value of the inputs change
var changeEvent = new PropertyValueEvent(function(val) {
- var globQuery = browseState.globQuery;
- if (exists(val)) {
- globQuery = val;
- }
navEvents.navigate({
- path: browseRoute.createUrl(browseState.namespace, globQuery)
+ path: browseRoute.createUrl(browseState, {
+ globQuery: val,
+ //TODO(aghassemi) We only supprt grid view for search, we could
+ //potentially support other views such as tree too but it's tricky.
+ viewType: 'grid'
+ })
});
}, 'value', true);
var clearSearch;
- if(browseState.globQuery) {
+ if (browseState.globQuery) {
clearSearch = h('paper-icon-button.icon.clear-search', {
'icon': new AttributeHook('clear'),
'label': new AttributeHook('Clear search'),
'ev-click': mercury.event(navEvents.navigate, {
- path: browseRoute.createUrl(browseState.namespace)
+ path: browseRoute.createUrl(browseState)
})
});
}
@@ -343,7 +440,7 @@
'label': new AttributeHook(
'Enter Glob query for searching, e.g. */*/a*'
),
- 'position': 'left'
+ 'position': 'bottom'
},
h('div', {
'layout': new AttributeHook('true'),
@@ -356,7 +453,8 @@
'flex': new AttributeHook('true'),
'name': 'globQuery',
'value': browseState.globQuery,
- 'ev-change': changeEvent
+ 'ev-change': changeEvent,
+ 'label': new AttributeHook('Glob Search')
}),
clearSearch
])
@@ -365,118 +463,19 @@
}
/*
- * The shortcuts chosen by the user are rendered with renderItem.
- */
-function renderUserShortcuts(browseState, browseEvents, navEvents) {
- return browseState.userShortcuts.map(function(shortcut) {
- return renderItem(browseState, browseEvents, navEvents, shortcut, true);
- });
-}
-
-/*
- * The shortcuts recommended by the smartService are rendered with renderItem.
- * A shortcut is no longer recommended if it is already a user shortcut.
- * TODO(alexfandrianto): Should we really filter out these repeats?
- */
-function renderRecommendedShortcuts(browseState, browseEvents, navEvents) {
- return browseState.recShortcuts.filter(function(shortcut) {
- return shortcut !== undefined &&
- handleShortcuts.find(browseState, shortcut) === -1;
- }).map(function(shortcut) {
- return renderItem(browseState, browseEvents, navEvents, shortcut, false);
- });
-}
-
-/*
- * The items (obtained by globbing) are rendered with renderItem.
- */
-function renderItems(browseState, browseEvents, navEvents) {
- return browseState.items.map(function(item) {
- var isShortcut = handleShortcuts.find(browseState, item) !== -1;
- return renderItem(browseState, browseEvents, navEvents, item, isShortcut);
- });
-}
-
-/*
- * Render a browse item card. The card consists of a service icon, a mounted
- * name, and if globbable, a drill icon.
- */
-function renderItem(browseState, browseEvents, navEvents, item, isShortcut) {
- var selected = false;
-
- if (browseState.selectedItemName === item.objectName) {
- selected = true;
- }
-
- // Prepare the drill if this item happens to be globbable.
- var expandAction = null;
- if (item.isGlobbable) {
- expandAction = h('a.drill', {
- 'href': browseRoute.createUrl(item.objectName),
- 'ev-click': mercury.event(navEvents.navigate, {
- path: browseRoute.createUrl(item.objectName)
- })
- }, h('core-icon.icon', {
- 'icon': new AttributeHook('chevron-right')
- }));
- }
-
- // Prepare tooltip and service icon information for the item.
- var isAccessible = true;
- var itemTooltip = item.objectName;
- var iconCssClass = '.service-type-icon' + (isShortcut ? '.shortcut' : '');
- var iconAttributes = {
- 'ev-click': mercury.event(browseEvents.setShortcut, {
- 'item': item,
- 'save': !isShortcut,
- })
- };
-
- if (item.isServer) {
- isAccessible = item.serverInfo.isAccessible;
- if (!isAccessible) {
- itemTooltip += ' - Service seems to be offline or inaccessible';
- }
- iconAttributes.title = new AttributeHook(item.serverInfo.typeInfo.typeName);
- iconAttributes.icon = new AttributeHook(
- getServiceIcon(item.serverInfo.typeInfo.key, isShortcut)
- );
- } else {
- iconAttributes.title = new AttributeHook('Intermediary Name');
- iconAttributes.icon = new AttributeHook(getServiceIcon('', isShortcut));
- }
-
- // Construct the service icon.
- var iconNode = h('core-icon' + iconCssClass, iconAttributes);
-
- // Put the item card's pieces together.
- var itemClassNames = 'item.card' +
- (selected ? '.selected' : '') +
- (!isAccessible ? '.inaccessible' : '');
-
- return h('div.' + itemClassNames, {
- 'title': itemTooltip
- }, [
- h('a.label', {
- 'href': 'javascript:;',
- 'ev-click': mercury.event(
- browseEvents.selectItem, {
- name: item.objectName
- })
- }, [
- iconNode,
- h('span', item.mountedName)
- ]),
- expandAction
- ]);
-}
-
-/*
* Renders the current name being browsed, split into parts.
* Each name part is a link to a parent.
*/
function renderBreadcrumbs(browseState, navEvents) {
+ // only render the breadcrumbs for items and not bookmarks/recommendations
+ if (browseState.subPage !== 'items') {
+ // use a flex div to leave white-space inplace of breadcrumbs
+ return h('div', {
+ 'flex': new AttributeHook('true')
+ });
+ }
+
var isRooted = namespaceService.util.isRooted(browseState.namespace);
var namespaceParts = browseState.namespace.split('/').filter(
function(n) {
@@ -485,12 +484,16 @@
);
var breadCrumbs = [];
if (!isRooted) {
+ // Add a relative root (empty namespace)
+ var rootUrl = browseRoute.createUrl(browseState, {
+ namespace: ''
+ });
breadCrumbs.push(h('li.breadcrumb-item', [
//TODO(aghassemi) refactor link generation code
h('a', {
- 'href': browseRoute.createUrl(),
+ 'href': rootUrl,
'ev-click': mercury.event(navEvents.navigate, {
- path: browseRoute.createUrl()
+ path: rootUrl
})
}, 'Home')
]));
@@ -501,11 +504,15 @@
var fullName = (isRooted ? '/' : '') +
namespaceService.util.join(namespaceParts.slice(0, i + 1));
+ var url = browseRoute.createUrl(browseState, {
+ namespace: fullName
+ });
+
var listItem = h('li.breadcrumb-item', [
h('a', {
- 'href': browseRoute.createUrl(fullName),
+ 'href': url,
'ev-click': mercury.event(navEvents.navigate, {
- path: browseRoute.createUrl(fullName)
+ path: url
})
}, namePart)
]);
@@ -513,14 +520,15 @@
breadCrumbs.push(listItem);
}
- return h('ul.breadcrumbs', breadCrumbs);
+ return h('ul.breadcrumbs', {
+ 'flex': new AttributeHook('true')
+ }, breadCrumbs);
}
// Wire up events that we know how to handle
function wireUpEvents(state, events) {
events.browseNamespace(browseNamespace.bind(null, state, events));
events.getNamespaceSuggestions(getNamespaceSuggestions.bind(null, state));
- events.setShortcut(handleShortcuts.set.bind(null, state, events));
events.selectItem(function(data) {
state.selectedItemName.set(data.name);
events.selectedItemDetails.displayItemDetails(data);
diff --git a/src/components/browse/item-card-list/index.css b/src/components/browse/item-card-list/index.css
new file mode 100644
index 0000000..fd012ab
--- /dev/null
+++ b/src/components/browse/item-card-list/index.css
@@ -0,0 +1,13 @@
+@import "common-style/theme.css";
+
+.items-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ border-bottom: var(--border);
+ padding-bottom: 0.5em;
+}
+
+.items-container:last-child {
+ border-bottom: none;
+}
\ No newline at end of file
diff --git a/src/components/browse/item-card-list/index.js b/src/components/browse/item-card-list/index.js
new file mode 100644
index 0000000..b4f6bac
--- /dev/null
+++ b/src/components/browse/item-card-list/index.js
@@ -0,0 +1,33 @@
+var mercury = require('mercury');
+var insertCss = require('insert-css');
+
+var ItemCard = require('./item-card/index');
+
+var css = require('./index.css');
+var h = mercury.h;
+
+module.exports.render = render;
+
+/*
+ * Renders a list of namespace items as cards in a list.
+ * @param opts.title {string} Title for the list. e.g "Bookmarks"
+ * @param opts.emptyText {string} Text to render when items is empty.
+ * e.g No Bookmarks found.
+ * @param items {Array<namespaceitem>} @see services/namespace/item
+ */
+function render(items, browseState, browseEvents, navEvents, opts) {
+ insertCss(css);
+
+ var view;
+ if (browseState.isFinishedLoadingItems && items.length === 0) {
+ view = h('div.empty', h('span', opts.emptyText));
+ } else {
+ view = items.map(function(item) {
+ return ItemCard.render(item, browseState, browseEvents, navEvents);
+ });
+ }
+
+ var heading = h('h2', opts.title);
+
+ return h('div.items-container', [heading, view]);
+}
\ No newline at end of file
diff --git a/src/components/browse/item-card-list/item-card/get-service-icon.js b/src/components/browse/item-card-list/item-card/get-service-icon.js
new file mode 100644
index 0000000..0c18e71
--- /dev/null
+++ b/src/components/browse/item-card-list/item-card/get-service-icon.js
@@ -0,0 +1,15 @@
+module.exports = getServiceIcon;
+
+var serviceIconMap = Object.freeze({
+ 'veyron-mounttable': 'social:circles-extended',
+ 'veyron-unknown': 'cloud-queue',
+ '': 'folder-open'
+});
+
+/*
+ * Given the type of a service and whether the element should be filled or not,
+ * return the name of the corresponding core-icon to use for rendering.
+ */
+function getServiceIcon(type) {
+ return serviceIconMap[type];
+}
\ No newline at end of file
diff --git a/src/components/browse/item-card-list/item-card/index.css b/src/components/browse/item-card-list/item-card/index.css
new file mode 100644
index 0000000..68f5629
--- /dev/null
+++ b/src/components/browse/item-card-list/item-card/index.css
@@ -0,0 +1,5 @@
+@import "common-style/card.css";
+
+.item.inaccessible {
+ opacity: 0.5;
+}
diff --git a/src/components/browse/item-card-list/item-card/index.js b/src/components/browse/item-card-list/item-card/index.js
new file mode 100644
index 0000000..627edd8
--- /dev/null
+++ b/src/components/browse/item-card-list/item-card/index.js
@@ -0,0 +1,84 @@
+var mercury = require('mercury');
+var insertCss = require('insert-css');
+var getServiceIcon = require('./get-service-icon');
+
+var AttributeHook = require('../../../../lib/mercury/attribute-hook');
+
+var browseRoute = require('../../../../routes/browse');
+
+var css = require('./index.css');
+var h = mercury.h;
+
+module.exports.render = render;
+
+/*
+ * Renders a namespace item in a card view.
+ * @param item {namespaceitem} @see services/namespace/item
+ */
+function render(item, browseState, browseEvents, navEvents) {
+ insertCss(css);
+
+ var selected = (browseState.selectedItemName === item.objectName);
+
+ var url = browseRoute.createUrl(browseState, {
+ namespace: item.objectName,
+ viewType: 'grid'
+ });
+
+ // Prepare the drill if this item happens to be globbable.
+ var expandAction = null;
+ if (item.isGlobbable) {
+ expandAction = h('a.drill', {
+ 'href': url,
+ 'ev-click': mercury.event(navEvents.navigate, {
+ path: url
+ })
+ }, h('core-icon.icon', {
+ 'icon': new AttributeHook('chevron-right')
+ }));
+ }
+
+ // Prepare tooltip and service icon information for the item.
+ var isAccessible = true;
+ var itemTooltip = item.objectName;
+ var iconCssClass = '.service-type-icon';
+ var iconAttributes = {};
+
+ if (item.isServer) {
+ isAccessible = item.serverInfo.isAccessible;
+ if (!isAccessible) {
+ itemTooltip += ' - Service seems to be offline or inaccessible';
+ }
+ iconAttributes.title = new AttributeHook(item.serverInfo.typeInfo.typeName);
+ iconAttributes.icon = new AttributeHook(
+ getServiceIcon(item.serverInfo.typeInfo.key)
+ );
+ } else {
+ iconAttributes.title = new AttributeHook('Intermediary Name');
+ iconAttributes.icon = new AttributeHook(getServiceIcon(''));
+ }
+
+ // Construct the service icon.
+ var iconNode = h('core-icon' + iconCssClass, iconAttributes);
+
+ // Put the item card's pieces together.
+ var itemClassNames = 'item.card' +
+ (selected ? '.selected' : '') +
+ (!isAccessible ? '.inaccessible' : '');
+
+ return h('div.' + itemClassNames, {
+ 'title': itemTooltip
+ }, [
+ h('a.label', {
+ 'href': 'javascript:;',
+ 'ev-click': mercury.event(
+ browseEvents.selectItem, {
+ name: item.objectName
+ })
+ }, [
+ iconNode,
+ h('span', item.mountedName || '<root>')
+ ]),
+ expandAction
+ ]);
+}
\ No newline at end of file
diff --git a/src/components/browse/item-details/bookmark.js b/src/components/browse/item-details/bookmark.js
new file mode 100644
index 0000000..303a7c6
--- /dev/null
+++ b/src/components/browse/item-details/bookmark.js
@@ -0,0 +1,41 @@
+var bookmarksService = require('../../../services/bookmarks/service');
+
+var log = require('../../../lib/log')(
+ 'components:browse:item-details:bookmark');
+
+module.exports = bookmark;
+
+function bookmark(state, events, data) {
+ var wasBookmarked = state.isBookmarked();
+ state.isBookmarked.set(data.bookmark);
+
+ bookmarksService.bookmark(data.name, data.bookmark).then(function() {
+ var toastText = 'Bookmark ' +
+ (data.bookmark ? 'added' : 'removed') +
+ ' for ' + data.name;
+
+ var undoAction = bookmark.bind(null, state, events, {
+ name: data.name,
+ bookmark: !data.bookmark
+ });
+
+ events.toast({
+ text: toastText,
+ action: undoAction,
+ actionText: 'UNDO'
+ });
+ }).catch(function(err) {
+ var errText = 'Failed to ' +
+ (data.bookmark ? 'add ' : 'remove') +
+ 'bookmark for ' + data.name;
+
+ log.error(errText, err);
+
+ // reset state on error back to what it used to be
+ state.isBookmarked.set(wasBookmarked);
+ events.toast({
+ text: errText,
+ type: 'error'
+ });
+ });
+}
\ No newline at end of file
diff --git a/src/components/browse/item-details/display-item-details.js b/src/components/browse/item-details/display-item-details.js
index 2431c3d..e2a89f8 100644
--- a/src/components/browse/item-details/display-item-details.js
+++ b/src/components/browse/item-details/display-item-details.js
@@ -1,13 +1,18 @@
var mercury = require('mercury');
-var namespaceService = require('../../../services/namespace/service');
-var smartService = require('../../../services/smart/service');
+
var methodNameToVarHashKey = require('./methodNameToVarHashKey');
+var methodStart = require('./method-start.js');
+var methodEnd = require('./method-end.js');
+
+var methodForm = require('./method-form/index.js');
+
+var namespaceService = require('../../../services/namespace/service');
+var bookmarkService = require('../../../services/bookmarks/service');
+var smartService = require('../../../services/smart/service');
+
var log = require('../../../lib/log')(
'components:browse:item-details:display-item-details'
);
-var methodForm = require('./method-form/index.js');
-var methodStart = require('./method-start.js');
-var methodEnd = require('./method-end.js');
module.exports = displayItemDetails;
@@ -40,8 +45,12 @@
state.showLoadingIndicator.set(true);
}, SHOW_LOADING_THRESHOLD);
- namespaceService.getNamespaceItem(name).then(function(itemObs) {
+ var resultsPromise = Promise.all([
+ bookmarkService.isBookmarked(name),
+ namespaceService.getNamespaceItem(name)
+ ]);
+ resultsPromise.then(function(results) {
/*
* Since async call, by the time we are here, a different name
* might be selected.
@@ -51,11 +60,16 @@
return;
}
+ var isBookmarked = results[0];
+ var itemObs = results[1];
+
// Indicate we finished loading
setIsLoaded();
state.put('item', itemObs);
+ state.isBookmarked.set(isBookmarked);
+
mercury.watch(itemObs, function(item) {
if (!item.isServer) {
return;
diff --git a/src/components/browse/item-details/index.css b/src/components/browse/item-details/index.css
index 2ff892f..c3f0710 100644
--- a/src/components/browse/item-details/index.css
+++ b/src/components/browse/item-details/index.css
@@ -1,5 +1,6 @@
@import "common-style/sizes.css";
@import "common-style/theme.css";
+@import "common-style/card.css";
.field {
font-size: var(--size-font-small);
@@ -11,6 +12,17 @@
color: var(--color-text-secondary);
}
+paper-icon-button.bookmarked {
+ color: var(--color-bright);
+}
+
+.item-actions {
+ margin: var(--size-space-xxsmall) -var(--size-space-xsmall);
+ margin-top: -var(--size-space-xxsmall);
+ padding: var(--size-space-xxsmall) 0;
+ border-bottom: var(--border);
+}
+
.method-input {
display: inline-block;
vertical-align: top;
@@ -18,10 +30,20 @@
overflow: hidden;
}
-.tooltip.method-tooltip::shadow .core-tooltip {
+.tooltip.field-tooltip::shadow .core-tooltip, .tooltip.method-tooltip::shadow .core-tooltip {
+ line-height: 0.9em; /* overrides Polymer's 6px line-height */
+ white-space: pre-wrap;
+ font-size: var(--size-font-xsmall);
+}
+
+.tooltip.field-tooltip::shadow .core-tooltip {
width: 24em;
}
+.tooltip.field-tooltip::shadow .core-tooltip {
+ width: 36em;
+}
+
.method-input .label {
align-items: center;
overflow: hidden;
diff --git a/src/components/browse/item-details/index.js b/src/components/browse/item-details/index.js
index b0f83e9..94fb9fa 100644
--- a/src/components/browse/item-details/index.js
+++ b/src/components/browse/item-details/index.js
@@ -1,12 +1,18 @@
var mercury = require('mercury');
-var AttributeHook = require('../../../lib/mercury/attribute-hook');
var insertCss = require('insert-css');
-var displayItemDetails = require('./display-item-details');
-var h = mercury.h;
-var css = require('./index.css');
+
+var AttributeHook = require('../../../lib/mercury/attribute-hook');
+
var methodNameToVarHashKey = require('./methodNameToVarHashKey.js');
+
+var displayItemDetails = require('./display-item-details');
+var bookmark = require('./bookmark');
+
var methodForm = require('./method-form/index.js');
+var css = require('./index.css');
+var h = mercury.h;
+
module.exports = create;
module.exports.render = render;
@@ -52,10 +58,17 @@
* Whether a loading indicator should be displayed instead of content
* @type {mercury.value<boolean>}
*/
- showLoadingIndicator: mercury.value(false)
+ showLoadingIndicator: mercury.value(false),
+
+ /*
+ * Whether item is bookmarked
+ * @type {mercury.value<boolean>}
+ */
+ isBookmarked: mercury.value(false)
});
var events = mercury.input([
+ 'bookmark',
'displayItemDetails',
'tabSelected',
'methodForm',
@@ -79,12 +92,12 @@
var tabContent;
- if(state.showLoadingIndicator) {
+ if (state.showLoadingIndicator) {
tabContent = h('paper-spinner', {
'active': new AttributeHook(true),
'aria-label': new AttributeHook('Loading')
});
- } else if(state.item) {
+ } else if (state.item) {
var detailsContent = renderDetailsContent(state, events);
var methodsContent;
@@ -114,6 +127,33 @@
}
/*
+ * Renders an action bar on top of the details panel page.
+ */
+function renderActions(state, events) {
+ var item = state.item;
+
+ // Bookmark action
+ var isBookmarked = state.isBookmarked;
+ var bookmarkIcon = 'bookmark' + (!isBookmarked ? '-outline' : '');
+ var bookmarkTitle = (isBookmarked ? 'Remove bookmark ' : 'Bookmark');
+ var bookmarkAction = h('core-tooltip', {
+ 'label': new AttributeHook(bookmarkTitle),
+ 'position': new AttributeHook('right'),
+ },
+ h('paper-icon-button' + (isBookmarked ? '.bookmarked' : ''), {
+ 'icon': new AttributeHook(bookmarkIcon),
+ 'alt': new AttributeHook(bookmarkTitle),
+ 'ev-click': mercury.event(events.bookmark, {
+ bookmark: !isBookmarked,
+ name: item.objectName
+ })
+ })
+ );
+
+ return h('div.icon-group.item-actions', [bookmarkAction]);
+}
+
+/*
* Renders details about the current service object.
* Note: Currently renders in the same tab as renderMethodsContent.
*/
@@ -128,7 +168,10 @@
typeName = 'Intermediary Name';
}
+ var actions = renderActions(state, events);
+
var displayItems = [
+ actions,
renderFieldItem('Name', (item.objectName || '<root>')),
renderFieldItem('Type', typeName, typeDescription)
];
@@ -142,7 +185,7 @@
// Use an info icon whose tooltip reveals the description.
serviceDescs.push(h('div', [
- h('core-tooltip.tooltip', {
+ h('core-tooltip.tooltip.field-tooltip', {
'label': desc || '<no description>',
'position': 'right'
}, h('core-icon.icon.info', {
@@ -199,9 +242,9 @@
methodNames.sort().forEach(function(methodName) {
var methodKey = methodNameToVarHashKey(methodName);
methods.push(methodForm.render(
- state.methodForm[methodKey],
- events.methodForm[methodKey]
- ));
+ state.methodForm[methodKey],
+ events.methodForm[methodKey]
+ ));
});
return h('div', methods); // Note: allows 0 method signatures
@@ -249,7 +292,7 @@
content = h('span', content);
if (tooltip) {
// If there is a tooltip, wrap the content in it
- content = h('core-tooltip.tooltip', {
+ content = h('core-tooltip.tooltip.field-tooltip', {
'label': new AttributeHook(tooltip),
'position': 'right'
}, content);
@@ -267,4 +310,5 @@
events.tabSelected(function(data) {
state.selectedTabIndex.set(data.index);
});
+ events.bookmark(bookmark.bind(null, state, events));
}
\ No newline at end of file
diff --git a/src/components/browse/item-details/method-end.js b/src/components/browse/item-details/method-end.js
index 2a25f1f..3cdd315 100644
--- a/src/components/browse/item-details/method-end.js
+++ b/src/components/browse/item-details/method-end.js
@@ -1,8 +1,11 @@
-var h = require('mercury').h;
+var formatDetail = require('./format-detail');
+
var smartService = require('../../../services/smart/service');
+
var log = require('../../../lib/log')(
'components:browse:item-details:method-end');
-var formatDetail = require('./format-detail');
+
+var h = require('mercury').h;
module.exports = methodEnd;
diff --git a/src/components/browse/item-details/method-form/index.js b/src/components/browse/item-details/method-form/index.js
index c459f14..85c16cd 100644
--- a/src/components/browse/item-details/method-form/index.js
+++ b/src/components/browse/item-details/method-form/index.js
@@ -1,19 +1,26 @@
var mercury = require('mercury');
-var h = mercury.h;
var _ = require('lodash');
var guid = require('guid');
+
+var makeRPC = require('./make-rpc.js');
+
var arraySet = require('../../../../lib/arraySet');
var setMercuryArray = require('../../../../lib/mercury/setMercuryArray');
var AttributeHook = require('../../../../lib/mercury/attribute-hook');
var PropertyValueEvent =
require('../../../../lib/mercury/property-value-event');
+
var store = require('../../../../lib/store');
-var log = require('../../../../lib/log')(
- 'components:browse:item-details:method-form');
+
var smartService = require('../../../../services/smart/service');
var hashSignature =
require('../../../../services/namespace/service').hashSignature;
-var makeRPC = require('./make-rpc.js');
+
+var log = require('../../../../lib/log')(
+ 'components:browse:item-details:method-form');
+
+var h = mercury.h;
+
module.exports = create;
module.exports.render = render;
diff --git a/src/components/browse/item-details/method-form/make-rpc.js b/src/components/browse/item-details/method-form/make-rpc.js
index d1399e1..7a93500 100644
--- a/src/components/browse/item-details/method-form/make-rpc.js
+++ b/src/components/browse/item-details/method-form/make-rpc.js
@@ -1,4 +1,5 @@
var namespaceService = require('../../../../services/namespace/service');
+
var log = require('../../../../lib/log')(
'components:browse:item-details:method-form:make-rpc'
);
diff --git a/src/components/browse/item-details/methodNameToVarHashKey.js b/src/components/browse/item-details/methodNameToVarHashKey.js
index 61b3b1d..dc8e670 100644
--- a/src/components/browse/item-details/methodNameToVarHashKey.js
+++ b/src/components/browse/item-details/methodNameToVarHashKey.js
@@ -1,4 +1,3 @@
-
/*
* Mercury VarHash has an issue where reserved keywords like 'delete', 'put'
* can not be used a hash keys :`(
diff --git a/src/components/browse/items/grid-view/index.js b/src/components/browse/items/grid-view/index.js
new file mode 100644
index 0000000..3e07496
--- /dev/null
+++ b/src/components/browse/items/grid-view/index.js
@@ -0,0 +1,22 @@
+var ItemCardList = require('../../item-card-list/index');
+
+module.exports = create;
+module.exports.render = render;
+
+function create() {}
+
+function render(itemsState, browseState, browseEvents, navEvents) {
+ var isSearch = !!browseState.globQuery;
+ var emptyText = (isSearch ? 'No glob search results' : 'No children');
+ var title = (isSearch ? 'Glob Search Results' : 'Grid View');
+
+ return ItemCardList.render(
+ itemsState.items,
+ browseState,
+ browseEvents,
+ navEvents, {
+ title: title,
+ emptyText: emptyText
+ }
+ );
+}
\ No newline at end of file
diff --git a/src/components/browse/items/index.css b/src/components/browse/items/index.css
new file mode 100644
index 0000000..bdcccb3
--- /dev/null
+++ b/src/components/browse/items/index.css
@@ -0,0 +1,11 @@
+.items-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ border-bottom: var(--border);
+ padding-bottom: 0.5em;
+}
+
+.items-container:last-child {
+ border-bottom: none;
+}
\ No newline at end of file
diff --git a/src/components/browse/items/index.js b/src/components/browse/items/index.js
new file mode 100644
index 0000000..6849a92
--- /dev/null
+++ b/src/components/browse/items/index.js
@@ -0,0 +1,131 @@
+var mercury = require('mercury');
+var guid = require('guid');
+
+var GridView = require('./grid-view/index');
+var TreeView = require('./tree-view/index');
+var VisualizeView = require('./visualize-view/index');
+
+var namespaceService = require('../../../services/namespace/service');
+
+var log = require('../../../lib/log')('components:browse:items');
+
+module.exports = create;
+module.exports.render = render;
+module.exports.load = load;
+module.exports.trySetViewType = trySetViewType;
+
+var VALID_VIEW_TYPES = ['grid', 'tree', 'visualize'];
+
+/*
+ * Items view.
+ * Renders one of: Grid, Tree or Visualize views depending on the state
+ */
+function create() {
+
+ var state = mercury.varhash({
+
+ /*
+ * List of namespace items to display
+ * @see services/namespace/item
+ * @type {Array<namespaceitem>}
+ */
+ items: mercury.array([]),
+
+ /*
+ * Specifies the current view type of the items.
+ * One of: grid, tree, visualize
+ */
+ viewType: mercury.value('grid'),
+
+ /*
+ * uuid for the current browse-namespace request.
+ * Needed to handle out-of-order return of async calls.
+ * @type {String}
+ */
+ currentRequestId: mercury.value('')
+ });
+
+ return {
+ state: state
+ };
+}
+
+function trySetViewType(state, viewType) {
+ var isValid = VALID_VIEW_TYPES.indexOf(viewType) >= 0;
+ if (!isValid) {
+ return false;
+ }
+
+ state.viewType.set(viewType);
+ return true;
+}
+
+/*
+ * Does the initialization and loading of the data necessary to display the
+ * namespace items.
+ * Called and used by the parent browse view to initialize the view on
+ * request.
+ * Returns a promise that will be resolved when loading is finished. Promise
+ * is used by the parent browse view to display a loading progressbar.
+ */
+function load(state, namespace, globQuery) {
+
+ // TODO(aghassemi)
+ // -Rename the concept to "init", not every component may have it.
+ // does not return anything, we can have showLoadingIndicator(bool) be an
+ // functionality that can be requested of the browse view.
+ // -Move items to "GridView"
+ // -Have a common component between tree and vis to share the childrens map
+
+ if (state.viewType() !== 'grid') {
+ return Promise.resolve();
+ }
+
+ // Search the namespace and update the browseState's items.
+ var requestId = guid.create().value;
+ state.currentRequestId.set(requestId);
+ state.put('items', mercury.array([]));
+
+ return new Promise(function(resolve, reject) {
+ namespaceService.search(namespace, globQuery).
+ then(function globResultsReceived(items) {
+ if (!isCurrentRequest()) {
+ resolve();
+ return;
+ }
+ state.put('items', items);
+ items.events.on('end', loadingFinished);
+ items.events.on('streamError', loadingFinished);
+ }).catch(function(err) {
+ log.error(err);
+ reject();
+ });
+
+ function loadingFinished() {
+ if (!isCurrentRequest()) {
+ return;
+ }
+ resolve();
+ }
+ });
+
+ // Whether we are still the current request. This is used to ignore out of
+ // order return of async calls where user has moved on to another item
+ // by the time previous requests result comes back.
+ function isCurrentRequest() {
+ return state.currentRequestId() === requestId;
+ }
+}
+
+function render(state, browseState, browseEvents, navEvents) {
+ switch (state.viewType) {
+ case 'grid':
+ return GridView.render(state, browseState, browseEvents, navEvents);
+ case 'tree':
+ return TreeView.render(state, browseState, browseEvents, navEvents);
+ case 'visualize':
+ return VisualizeView.render(state, browseState, browseEvents, navEvents);
+ default:
+ log.error('Unsupported viewType: ' + state.viewType);
+ }
+}
\ No newline at end of file
diff --git a/src/components/browse/items/tree-view/index.js b/src/components/browse/items/tree-view/index.js
new file mode 100644
index 0000000..6a2a104
--- /dev/null
+++ b/src/components/browse/items/tree-view/index.js
@@ -0,0 +1,12 @@
+var mercury = require('mercury');
+
+var h = mercury.h;
+
+module.exports = create;
+module.exports.render = render;
+
+function create() {}
+
+function render(itemsState, browseState, browseEvents, navEvents) {
+ return h('h2', 'Tree View (TODO)');
+}
\ No newline at end of file
diff --git a/src/components/visualize/index.css b/src/components/browse/items/visualize-view/index.css
similarity index 100%
rename from src/components/visualize/index.css
rename to src/components/browse/items/visualize-view/index.css
diff --git a/src/components/visualize/index.js b/src/components/browse/items/visualize-view/index.js
similarity index 82%
rename from src/components/visualize/index.js
rename to src/components/browse/items/visualize-view/index.js
index 0ce6d1f..602af98 100644
--- a/src/components/visualize/index.js
+++ b/src/components/browse/items/visualize-view/index.js
@@ -1,34 +1,39 @@
var mercury = require('mercury');
var insertCss = require('insert-css');
var vis = require('vis');
+
+var namespaceService = require('../../../../services/namespace/service');
+
+var log = require('../../../../lib/log')('components:browse:items:tree-view');
+
var css = require('./index.css');
-var namespaceService = require('../../services/namespace/service');
-var log = require('../../lib/log')('components:visualize');
+var h = mercury.h;
module.exports = create;
module.exports.render = render;
-// Maximum number of levels that are automatically shown
-var MAX_AUTO_LOAD_DEPTH = 3;
-
-/*
- * Visualize view
- */
function create() {}
-function render(browseState) {
+function render(itemsState, browseState, browseEvents, navEvents) {
+
insertCss(css);
+
return [
- new TreeWidget(browseState)
+ h('h2', 'Visualize View'),
+ new TreeWidget(browseState, browseEvents)
];
}
-function TreeWidget(browseState) {
+// Maximum number of levels that are automatically shown
+var MAX_AUTO_LOAD_DEPTH = 3;
+
+function TreeWidget(browseState, browseEvents) {
if (!(this instanceof TreeWidget)) {
return new TreeWidget(browseState);
}
this.browseState = browseState;
+ this.browseEvents = browseEvents;
this.nodes = new vis.DataSet();
this.edges = new vis.DataSet();
}
@@ -62,7 +67,7 @@
var options = {
hover: false,
selectable: true, // Need this or nodes won't be click-able
- smoothCurves: true,
+ smoothCurves: false,
stabilize: false,
edges: {
width: 1
@@ -87,6 +92,19 @@
// Event listeners.
network.on('click', function onClick(data) {
+ // refresh side view
+ var nodeId = data.nodes[0];
+ var node = network.nodes[nodeId];
+
+ if (node) {
+ self.browseEvents.selectItem({
+ name: nodeId
+ });
+ }
+ });
+
+ network.on('doubleClick', function onClick(data) {
+ // drill
var nodeId = data.nodes[0];
var node = network.nodes[nodeId];
@@ -94,7 +112,6 @@
self.loadSubNodes(node);
}
});
-
return network;
};
@@ -144,7 +161,7 @@
if (item.level - self.rootNode.level < MAX_AUTO_LOAD_DEPTH) {
self.loadSubNodes(item);
} else {
- item.title = 'Click to expand';
+ item.title = 'Double-click to expand';
}
});
self.nodes.add(newNodes);
diff --git a/src/components/browse/recommend-shortcuts.js b/src/components/browse/recommend-shortcuts.js
deleted file mode 100644
index 5bf541f..0000000
--- a/src/components/browse/recommend-shortcuts.js
+++ /dev/null
@@ -1,30 +0,0 @@
-var mercury = require('mercury');
-var _ = require('lodash');
-var log = require('../../lib/log')('components:browse:recommend-shortcuts');
-var namespaceService = require('../../services/namespace/service');
-var smartService = require('../../services/smart/service');
-
-module.exports = recommendShortcuts;
-
-/*
- * Asks the smartService to asynchronously update the browseState with the
- * associated recommendations.
- */
-function recommendShortcuts(browseState) {
- var input = {
- 'name': '',
- 'exclude': _.pluck(browseState.userShortcuts(), 'objectName')
- };
- smartService.predict('learner-shortcut', input).then(function(predictions) {
- browseState.put('recShortcuts', mercury.array([]));
- predictions.forEach(function(prediction, i) {
- namespaceService.getNamespaceItem(prediction.item).then(function(item) {
- browseState.recShortcuts.put(i, item);
- }).catch(function(err) {
- log.error('Failed to get recommended shortcut:', prediction, err);
- });
- });
- }).catch(function(err) {
- log.error('Could not load recommended shortcuts', err);
- });
-}
\ No newline at end of file
diff --git a/src/components/browse/recommendations/index.js b/src/components/browse/recommendations/index.js
new file mode 100644
index 0000000..4362fdf
--- /dev/null
+++ b/src/components/browse/recommendations/index.js
@@ -0,0 +1,64 @@
+var mercury = require('mercury');
+var ItemCardList = require('../item-card-list/index');
+
+var recommendationsService =
+ require('../../../services/recommendations/service');
+
+var log = require('../../../lib/log')('components:browse:recommendation');
+
+module.exports = create;
+module.exports.render = render;
+module.exports.load = load;
+
+/*
+ * Recommendation view
+ */
+function create() {
+
+ var state = mercury.varhash({
+ /*
+ * List of recommended shortcuts to display
+ * @see services/namespace/item
+ * @type {Array<namespaceitem>}
+ */
+ recShortcuts: mercury.array([]),
+
+ });
+
+ return {
+ state: state
+ };
+}
+
+function render(state, browseState, browseEvents, navEvents) {
+ return ItemCardList.render(
+ state.recShortcuts,
+ browseState,
+ browseEvents,
+ navEvents, {
+ title: 'Recommendations',
+ emptyText: 'No recommendations'
+ }
+ );
+}
+
+/*
+ * Does the initialization and loading of the data necessary to display the
+ * recommendations.
+ * Called and used by the parent browse view to initialize the view on
+ * request.
+ * Returns a promise that will be resolved when loading is finished. Promise
+ * is used by the parent browse view to display a loading progressbar.
+ */
+function load(state) {
+ return new Promise(function(resolve, reject) {
+ recommendationsService.getAll()
+ .then(function recReceived(items) {
+ state.put('recShortcuts', items);
+ items.events.on('end', resolve);
+ }).catch(function(err) {
+ log.error(err);
+ reject();
+ });
+ });
+}
\ No newline at end of file
diff --git a/src/components/common-style/card.css b/src/components/common-style/card.css
new file mode 100644
index 0000000..bc47054
--- /dev/null
+++ b/src/components/common-style/card.css
@@ -0,0 +1,53 @@
+@import "common-style/theme.css";
+@import "common-style/sizes.css";
+
+.item.card {
+ box-sizing: border-box;
+ background-color: var(--color-white);
+ display: flex;
+ flex-shrink: 0;
+ flex-grow: 0;
+ height: 2.5em;
+ min-width: 8em;
+ margin: 0.75em;
+ border-radius: 3px;
+ overflow: hidden;
+ position: relative;
+ box-shadow: var(--shadow-all-around);
+ border: var(--border);
+}
+.item .label, .item .drill {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ padding: 0.5em;
+}
+.item .label {
+ text-decoration: none;
+ flex: 1;
+ overflow: hidden;
+ white-space: nowrap;
+}
+.item .drill {
+ width: 1.5em;
+ background-color: var(--color-grey-light);
+ border-left: var(--border);
+}
+
+.item.selected .drill {
+ background-color: var(--color-bright);
+}
+
+.item a:hover, .item a:focus{
+ opacity: 0.7;
+}
+
+.item.card .icon {
+ align-self: center;
+ padding-right: 0.5em;
+}
+
+.item.selected.card {
+ background-color: var(--color-bright);
+ color: var(--color-text-primary-invert);
+}
diff --git a/src/components/common-style/defaults.css b/src/components/common-style/defaults.css
index 33efad7..f9195a9 100644
--- a/src/components/common-style/defaults.css
+++ b/src/components/common-style/defaults.css
@@ -36,7 +36,7 @@
paper-progress {
width: 100%;
- height: 2px;
+ height: 4px;
}
/*
@@ -79,6 +79,13 @@
margin-right: var(--size-space-xsmall);
}
+.empty {
+ width: 100%;
+ padding: 1em;
+ text-align: center;
+ color: var(--color-text-secondary);
+}
+
/*
* Applies a tiny margin to the left of the element.
*/
diff --git a/src/components/common-style/sizes.css b/src/components/common-style/sizes.css
index e8d37d2..b32f372 100644
--- a/src/components/common-style/sizes.css
+++ b/src/components/common-style/sizes.css
@@ -9,12 +9,14 @@
--size-font-xlarge: 1.2em;
/* margin and padding size */
- --size-space-xxsmall: 0.25em;
+ --size-space-xxsmall: 0.2em;
--size-space-xsmall: 0.5em;
--size-space-small: 0.75em;
--size-space-normal: 1em;
--size-space-large: 1.25em;
--size-space-xlarge: 1.5em;
+ --size-space-xxlarge: 3em;
--size-input-width-normal: 16em;
+ --size-input-width-small: 8em;
}
diff --git a/src/components/help/selectTab.js b/src/components/help/selectTab.js
index f992b7b..ddb4eb0 100644
--- a/src/components/help/selectTab.js
+++ b/src/components/help/selectTab.js
@@ -9,8 +9,7 @@
function selectTab(state, events, tabKey) {
// If the tab is invalid, go to the error page.
if (sections.get(tabKey) === undefined) {
- // TODO(aghassemi): Add 404 error.
- // events.error(type.404);
+ //TODO(aghassemi) Needs to be 404 error when we have support for 404
events.error(new Error('Invalid help page: ' + tabKey));
} else {
// Since the tabKey is valid, the selectedTab can be updated.
diff --git a/src/components/main-content/index.js b/src/components/main-content/index.js
index ebaa676..acc5c03 100644
--- a/src/components/main-content/index.js
+++ b/src/components/main-content/index.js
@@ -3,7 +3,6 @@
var Browse = require('../browse/index');
var Help = require('../help/index');
var ErrorPage = require('../error/index');
-var Visualize = require('../visualize/index');
var css = require('./index.css');
var h = mercury.h;
@@ -47,8 +46,6 @@
return Help.render(state.help, events.help);
case 'error':
return ErrorPage.render(state.error);
- case 'visualize':
- return Visualize.render(state.browse);
default:
// We shouldn't get here with proper route handlers, so it's an error(bug)
throw new Error('Could not find page ' + pageKey);
diff --git a/src/components/sidebar/index.js b/src/components/sidebar/index.js
index 3679dee..4ffffa8 100644
--- a/src/components/sidebar/index.js
+++ b/src/components/sidebar/index.js
@@ -2,7 +2,6 @@
var insertCss = require('insert-css');
var browseRoute = require('../../routes/browse');
var helpRoute = require('../../routes/help');
-var visualizeRoute = require('../../routes/visualize');
var css = require('./index.css');
var h = mercury.h;
@@ -28,25 +27,9 @@
function renderNavigationItems() {
var navigationItems = [{
key: 'browse',
- label: 'Iconview',
- icon: 'apps',
- href: browseRoute.createUrl(
- state.browse.namespace,
- state.browse.globQuery
- )
- }, {
- key: 'tree',
- label: 'Treeview',
- icon: 'chevron-left',
- href: browseRoute.createUrl(
- state.browse.namespace,
- state.browse.globQuery
- )
- }, {
- key: 'visualize',
- label: 'Visualize',
- icon: 'social:circles-extended',
- href: visualizeRoute.createUrl()
+ label: 'Browse',
+ icon: 'explore',
+ href: browseRoute.createUrl(state.browse)
}, {
key: 'help',
label: 'Help',
diff --git a/src/lib/log.js b/src/lib/log.js
index 0be661f..344689c 100644
--- a/src/lib/log.js
+++ b/src/lib/log.js
@@ -17,7 +17,7 @@
* as DeLogger? DeLog is taken :(
*/
var debug = require('debug');
-var extend = require('xtend');
+var extend = require('extend');
module.exports = log;
module.exports.disable = disable;
diff --git a/src/router.js b/src/router.js
index 36fe972..da0f12b 100644
--- a/src/router.js
+++ b/src/router.js
@@ -18,7 +18,7 @@
var path = normalizePath(data.path);
var route = routes.match(path);
if (!route) {
- //TOOD(aghassemi) redirect to 404 error view?
+ //TODO(aghassemi) Needs to be 404 error when we have support for 404
return;
}
if (!data.skipHistoryPush) {
diff --git a/src/routes/bookmarks.js b/src/routes/bookmarks.js
new file mode 100644
index 0000000..5d37da7
--- /dev/null
+++ b/src/routes/bookmarks.js
@@ -0,0 +1,20 @@
+module.exports = function(routes) {
+ routes.addRoute('/bookmarks', handleBookmarksRoute);
+};
+
+module.exports.createUrl = function() {
+ return '#/bookmarks';
+};
+
+function handleBookmarksRoute(state, events, params) {
+
+ // Set the page to browse
+ state.navigation.pageKey.set('browse');
+ state.viewport.title.set('Browse');
+
+ // Trigger browse components browseNamespace event
+ events.browse.browseNamespace({
+ 'namespace': state.browse.namespace(),
+ 'subPage': 'bookmarks'
+ });
+}
\ No newline at end of file
diff --git a/src/routes/browse.js b/src/routes/browse.js
index 3f56fde..a3ec64b 100644
--- a/src/routes/browse.js
+++ b/src/routes/browse.js
@@ -1,25 +1,42 @@
var urlUtil = require('url');
var qsUtil = require('querystring');
+
var exists = require('../lib/exists');
-var log = require('../lib/log');
var store = require('../lib/store');
+var log = require('../lib/log')('routes:browse');
+
module.exports = function(routes) {
- // Url pattern: /browse/veyronNameSpace?glob=*
+ // Url pattern: /browse/veyronNameSpace?glob=*&viewType=grid
routes.addRoute('/browse/:namespace?', handleBrowseRoute);
};
-module.exports.createUrl = function(namespace, globquery) {
+module.exports.createUrl = function(browseState, opts) {
+ var globQuery;
+ var viewType;
+ var namespace;
+ if (opts) {
+ globQuery = opts.globQuery;
+ viewType = opts.viewType;
+ namespace = opts.namespace;
+ }
+
+ // We preserve namespace and viewtype if they are not provided
+ // We reset globquery unless provided
+ namespace = (namespace === undefined ? browseState.namespace : namespace);
+ viewType = (viewType === undefined ? browseState.viewType : viewType);
+
var path = '/browse';
if (exists(namespace)) {
namespace = encodeURIComponent(namespace);
path += '/' + namespace;
}
- var query;
- if (exists(globquery)) {
- query = {
- 'glob': globquery
- };
+ var query = {};
+ if (exists(globQuery)) {
+ query['glob'] = globQuery;
+ }
+ if (exists(viewType)) {
+ query['viewtype'] = viewType;
}
return '#' + urlUtil.format({
pathname: path,
@@ -33,13 +50,19 @@
state.navigation.pageKey.set('browse');
state.viewport.title.set('Browse');
- var namespace = '';
- var globquery = '';
+ var namespace;
+ var globquery;
+ var viewtype;
if (params.namespace) {
var parsed = urlUtil.parse(params.namespace);
- namespace = parsed.pathname || '';
+ if (parsed.pathname) {
+ namespace = parsed.pathname;
+ }
+
if (parsed.query) {
- globquery = qsUtil.parse(parsed.query).glob;
+ var queryString = qsUtil.parse(parsed.query);
+ globquery = queryString.glob;
+ viewtype = queryString.viewtype;
}
}
@@ -51,6 +74,8 @@
// Trigger browse components browseNamespace event
events.browse.browseNamespace({
'namespace': namespace,
- 'globQuery': globquery
+ 'globQuery': globquery,
+ 'viewType': viewtype,
+ 'subPage': 'items'
});
}
\ No newline at end of file
diff --git a/src/routes/index.js b/src/routes/index.js
index bc56277..273720a 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -1,7 +1,8 @@
var browseRoute = require('./browse');
-var log = require('../lib/log');
var store = require('../lib/store');
+var log = require('../lib/log')('routes:index');
+
module.exports = function(routes) {
routes.addRoute('/', handleIndexRoute);
};
@@ -17,14 +18,18 @@
// Redirect to browse
events.navigation.navigate({
- path: browseRoute.createUrl(index)
+ path: browseRoute.createUrl(state.browse, {
+ namespace: index
+ })
});
}).catch(function(err) {
log.warn('Unable to access stored index', err);
// Redirect to browse
events.navigation.navigate({
- path: browseRoute.createUrl(index)
+ path: browseRoute.createUrl(state.browse, {
+ namespace: index
+ })
});
});
}
diff --git a/src/routes/recommendations.js b/src/routes/recommendations.js
new file mode 100644
index 0000000..0c10dbf
--- /dev/null
+++ b/src/routes/recommendations.js
@@ -0,0 +1,20 @@
+module.exports = function(routes) {
+ routes.addRoute('/recommendation', handleRecommendationRoute);
+};
+
+module.exports.createUrl = function() {
+ return '#/recommendation';
+};
+
+function handleRecommendationRoute(state, events, params) {
+
+ // Set the page to browse
+ state.navigation.pageKey.set('browse');
+ state.viewport.title.set('Browse');
+
+ // Trigger browse components browseNamespace event
+ events.browse.browseNamespace({
+ 'namespace': state.browse.namespace(),
+ 'subPage': 'recommendations'
+ });
+}
\ No newline at end of file
diff --git a/src/routes/register-routes.js b/src/routes/register-routes.js
index b91c99f..0c01d6c 100644
--- a/src/routes/register-routes.js
+++ b/src/routes/register-routes.js
@@ -8,5 +8,6 @@
require('./help')(routes);
require('./browse')(routes);
require('./error')(routes);
- require('./visualize')(routes);
+ require('./recommendations')(routes);
+ require('./bookmarks')(routes);
}
\ No newline at end of file
diff --git a/src/routes/visualize.js b/src/routes/visualize.js
deleted file mode 100644
index ba0b398..0000000
--- a/src/routes/visualize.js
+++ /dev/null
@@ -1,14 +0,0 @@
-module.exports = function(routes) {
- routes.addRoute('/visualize', handleVisualizeRoute);
-};
-
-module.exports.createUrl = function() {
- return '#/visualize';
-};
-
-function handleVisualizeRoute(state) {
-
- // Set the page to visualize
- state.navigation.pageKey.set('visualize');
- state.viewport.title.set('Visualize');
-}
\ No newline at end of file
diff --git a/src/services/bookmarks/service.js b/src/services/bookmarks/service.js
new file mode 100644
index 0000000..36e2214
--- /dev/null
+++ b/src/services/bookmarks/service.js
@@ -0,0 +1,130 @@
+var mercury = require('mercury');
+var _ = require('lodash');
+var EventEmitter = require('events').EventEmitter;
+
+var arraySet = require('../../lib/arraySet');
+var store = require('../../lib/store');
+var freeze = require('../../lib/mercury/freeze');
+
+var namespaceService = require('../namespace/service');
+
+var log = require('../../lib/log')('services:bookmarks:service');
+
+module.exports = {
+ getAll: getAll,
+ bookmark: bookmark,
+ isBookmarked: isBookmarked
+};
+
+// Data is loaded from and saved to this key in the store.
+var USER_BOOKMARKS_KEY = 'bookmarks-store-key';
+
+// Singleton state for all the bookmarks.
+var bookmarksObs = mercury.array([]);
+
+/*
+ * Gets all the namespace items that are bookmarked
+ * As new bookmarks become available/removed the observable array will change
+ * to reflect the changes.
+ *
+ * The observable result has an events property which is an EventEmitter
+ * and emits 'end', 'itemError' events.
+ *
+ * @return {Promise.<mercury.array>} Promise of an observable array
+ * of bookmark items
+ */
+function getAll() {
+ // Empty out the array
+ bookmarksObs.splice(0, bookmarksObs.getLength());
+ var immutableBookmarksObs = freeze(bookmarksObs);
+ immutableBookmarksObs.events = new EventEmitter();
+
+ return loadKeys().then(getBookmarkItems);
+
+ function getBookmarkItems(names) {
+ var allItems = names.map(function(name) {
+ return addNamespaceItem(name).catch(function(err) {
+ immutableBookmarksObs.events.emit('itemError', {
+ name: name,
+ error: err
+ });
+ log.error('Failed to create item for "' + name + '"', err);
+ });
+ });
+
+ Promise.all(allItems).then(function() {
+ immutableBookmarksObs.events.emit('end');
+ }).catch(function() {
+ immutableBookmarksObs.events.emit('end');
+ });
+
+ return immutableBookmarksObs;
+ }
+}
+
+/*
+ * Gets the namespace items for a name and adds it to the observable array
+ */
+function addNamespaceItem(name) {
+ return namespaceService.getNamespaceItem(name)
+ .then(function(item) {
+ bookmarksObs.push(item);
+ });
+}
+
+/*
+ * Whether a specific name is bookmarked or not
+ * @return {Promise.<boolean>} Promise indicating a name is bookmarked
+ */
+function isBookmarked(name) {
+ return loadKeys().then(function(keys) {
+ return (keys && keys.indexOf(name) >= 0);
+ });
+}
+
+/*
+ * Bookmarks/Unbookmarks a name.
+ * @return {Promise.<void>} Promise indicating whether operation succeeded.
+ */
+function bookmark(name, isBookmarked) {
+ if (isBookmarked) {
+ // new bookmark, add it to the state
+ addNamespaceItem(name);
+ } else {
+ // remove bookmark
+ arraySet.set(bookmarksObs, null, false, indexOf.bind(null, name));
+ }
+
+ // update store
+ return loadKeys().then(function(keys) {
+ keys = keys || []; // Initialize the bookmarks, if none were loaded.
+ arraySet.set(keys, name, isBookmarked);
+ return store.setValue(USER_BOOKMARKS_KEY, keys);
+ });
+}
+
+/*
+ * Check the browseState for the index of the given item. -1 if not present.
+ * Note: browseState should be observed.
+ */
+function indexOf(name) {
+ return _.findIndex(bookmarksObs(), function(bookmark) {
+ // Since bookmarks can be assigned out of order, check for undefined.
+ return bookmark !== undefined && name === bookmark.objectName;
+ });
+}
+
+/*
+ * Loads all the bookmarked names from the store
+ */
+function loadKeys() {
+ return store.getValue(USER_BOOKMARKS_KEY).then(function(keys) {
+ keys = keys || [];
+ return keys.filter(function(key, index, self) {
+ // only return unique and existing values
+ return key !== null &&
+ key !== undefined &&
+ self.indexOf(key) === index;
+ });
+ });
+}
\ No newline at end of file
diff --git a/src/services/recommendations/service.js b/src/services/recommendations/service.js
new file mode 100644
index 0000000..7def416
--- /dev/null
+++ b/src/services/recommendations/service.js
@@ -0,0 +1,80 @@
+var mercury = require('mercury');
+var EventEmitter = require('events').EventEmitter;
+
+var freeze = require('../../lib/mercury/freeze');
+
+var namespaceService = require('../namespace/service');
+var smartService = require('../smart/service');
+
+var log = require('../../lib/log')('services:recommendations:service');
+
+var LEARNER_KEY = 'learner-shortcut';
+var MAX_NUM_RECOMMENDATIONS = 10;
+
+module.exports = {
+ getAll: getAll
+};
+
+// Singleton state for all the bookmarks.
+var recommendationsObs = mercury.array([]);
+
+smartService.loadOrCreate(
+ LEARNER_KEY,
+ smartService.constants.LEARNER_SHORTCUT, {
+ k: MAX_NUM_RECOMMENDATIONS
+ }
+).catch(function(err) {
+ log.error(err);
+});
+
+/*
+ * Gets all the namespace items that are recommended based on our learning agent
+ * As new recommendations become available/removed the observable array will
+ * change to reflect the changes.
+ *
+ * The observable result has an events property which is an EventEmitter
+ * and emits 'end', 'itemError' events.
+ *
+ * @return {Promise.<mercury.array>} Promise of an observable array
+ * of recommended items
+ */
+function getAll() {
+
+ // Empty out the array
+ recommendationsObs.splice(0, recommendationsObs.getLength());
+ var immutableRecommendationsObs = freeze(recommendationsObs);
+ immutableRecommendationsObs.events = new EventEmitter();
+
+ return smartService.predict(LEARNER_KEY).then(getRecommendationItems);
+
+ function getRecommendationItems(recs) {
+ var allItems = recs.map(function(rec) {
+ var name = rec.item;
+ return addNamespaceItem(name).catch(function(err) {
+ immutableRecommendationsObs.events.emit('itemError', {
+ name: name,
+ error: err
+ });
+ log.error('Failed to create item for "' + name + '"', err);
+ });
+ });
+
+ Promise.all(allItems).then(function() {
+ immutableRecommendationsObs.events.emit('end');
+ }).catch(function() {
+ immutableRecommendationsObs.events.emit('end');
+ });
+
+ return immutableRecommendationsObs;
+ }
+}
+
+/*
+ * Gets the namespace items for a name and adds it to the observable array
+ */
+function addNamespaceItem(name) {
+ return namespaceService.getNamespaceItem(name)
+ .then(function(item) {
+ recommendationsObs.push(item);
+ });
+}
\ No newline at end of file
diff --git a/src/services/smart/service.js b/src/services/smart/service.js
index 9292e1e..e5ea83d 100644
--- a/src/services/smart/service.js
+++ b/src/services/smart/service.js
@@ -8,6 +8,7 @@
var store = require('../../lib/store');
var log = require('../../lib/log')('services:smart-service');
var constants = require('./service-implementation');
+var extend = require('extend');
// Export methods and constants
module.exports = {
@@ -39,6 +40,7 @@
// Store the learner right away. Calls to get should use this promise.
learners[id] = load(id).then(function loadSuccess(learner) {
log.debug('loaded learner', id);
+ learner.params = extend(learner.params, params);
return Promise.resolve(learner);
}, function loadFailure() {
return create(id, type, params).then(function(learner) {
diff --git a/test/unit/components/browse/browse-namespace.js b/test/unit/components/browse/browse-namespace.js
index ecd27f9..1e8cf84 100644
--- a/test/unit/components/browse/browse-namespace.js
+++ b/test/unit/components/browse/browse-namespace.js
@@ -24,14 +24,7 @@
return Promise.resolve(mercury.array([mockItem]));
}
};
-var namespaceServiceMockWithFailure = {
- isGlobbable: function(name) {
- return Promise.resolve(true);
- },
- search: function(name, globQuery) {
- return Promise.reject();
- }
-};
+
function itemDetailsComponentMock() {
return {
state: {},
@@ -49,11 +42,6 @@
'../../services/namespace/service': namespaceServiceMock
});
-var browseNamespaceWithFailure =
-proxyquire('../../../../src/components/browse/browse-namespace',{
- '../../services/namespace/service': namespaceServiceMockWithFailure
-});
-
test('Updates state.namespace', function(t) {
t.plan(4);
@@ -66,15 +54,15 @@
});
t.equal(state.namespace(), 'foo/bar');
- // Should not update state.namespace if data.namespace is null
+ // Should reset state.namespace if data.namespace is null
browseNamespace(state, events, {
namespace: null
});
- t.equal(state.namespace(), 'foo/bar');
+ t.equal(state.namespace(), '');
- // Should not update state.namespace if data.namespace is undefined
+ // Should reset state.namespace if data.namespace is undefined
browseNamespace(state, events, {});
- t.equal(state.namespace(), 'foo/bar');
+ t.equal(state.namespace(), '');
// Should update state.namespace if data.namespace is empty string
browseNamespace(state, events, {
@@ -95,15 +83,15 @@
});
t.equal(state.globQuery(), '**/*');
- // Should not update state.globQuery if data.globQuery is null
+ // Should reset state.globQuery if data.globQuery is null
browseNamespace(state, events, {
globQuery: null
});
- t.equal(state.globQuery(), '**/*');
+ t.equal(state.globQuery(), '');
- // Should not update state.globQuery if data.globQuery is undefined
+ // Should reset state.globQuery if data.globQuery is undefined
browseNamespace(state, events, {});
- t.equal(state.globQuery(), '**/*');
+ t.equal(state.globQuery(), '');
// Empty glob keeps it empty in the state but behind the scenes does a '*'
browseNamespace(state, events, {
@@ -111,46 +99,3 @@
});
t.equal(state.globQuery(), '');
});
-
-test('Updates state.items', function(t) {
- t.plan(1);
-
- var state = browseComponent().state;
- var events = browseComponent().events;
-
- // Should update the items to items returned by glob method call (async)
- browseNamespace(state, events, {
- globQuery: '*',
- namespace: 'foo/bar'
- });
-
- // Wait until next tick and assert the expected state change
- process.nextTick( function() {
- var items = state.items();
- t.deepEqual(items, [mockItem]);
- });
-
-});
-
-test('Updates state.items to empty on failure', function(t) {
- t.plan(1);
-
- var state = browseComponent().state;
- var events = browseComponent().events;
-
- // Give initial non-empty value
- state.items.push([mockItem]);
-
- //Should reset the items to empty on failed glob method call (async)
- browseNamespaceWithFailure(state, events, {
- globQuery: '*',
- namespace: 'foo/bar'
- });
-
- // Wait until next tick and assert the expected state change
- process.nextTick( function() {
- var items = state.items();
- t.deepEqual(items, []);
- });
-
-});