diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index 5497e034344..9549157e1e1 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -6835,3 +6835,50 @@ div.panel.ui-dialog div.list-view div.fixed-header { background-position: -229px -586px; } +.moveTop .icon { + background-position: -24px -161px; +} + +.moveTop:hover .icon { + background-position: -24px -734px; +} + +.moveBottom .icon { + background-position: -98px -161px; +} + +.moveBottom:hover .icon { + background-position: -98px -734px; +} + +.moveUp .icon { + background-position: -2px -161px; +} + +.moveUp:hover .icon { + background-position: -2px -734px; +} + +.moveDown .icon { + background-position: -55px -161px; +} + +.moveDown:hover .icon { + background-position: -55px -734px; +} + +.moveDrag .icon { + cursor: move; + /*+border-radius:10px;*/ + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + -khtml-border-radius: 10px; + border-radius: 10px 10px 10px 10px; + background-position: -74px -162px; +} + +.moveDrag:hover .icon { + background-color: #FFFFFF; + cursor: move !important; +} + diff --git a/ui/images/sprites.png b/ui/images/sprites.png index 4babfd498b7..4804598d526 100644 Binary files a/ui/images/sprites.png and b/ui/images/sprites.png differ diff --git a/ui/scripts-test/configuration.js b/ui/scripts-test/configuration.js index 67f56e62616..4c6d1a407e4 100644 --- a/ui/scripts-test/configuration.js +++ b/ui/scripts-test/configuration.js @@ -68,6 +68,45 @@ } } }, + + reorder: { + moveTop: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveBottom: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveUp: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveDown: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveDrag: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + } + }, + dataProvider: function(args) { setTimeout(function() { args.response.success({ @@ -90,6 +129,45 @@ memory: { label: 'Memory' }, domain: { label: 'Domain'} }, + + reorder: { + moveTop: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveBottom: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveUp: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveDown: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveDrag: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + } + }, + actions: { add: { label: 'Add system service offering', @@ -168,6 +246,44 @@ }); }, + reorder: { + moveTop: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveBottom: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveUp: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveDown: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveDrag: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + } + }, + actions: { add: { label: 'Add disk offering', @@ -251,6 +367,44 @@ traffictype: { label: 'Traffic Type'} }, + reorder: { + moveTop: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveBottom: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveUp: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveDown: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + }, + moveDrag: { + action: function(args) { + setTimeout(function() { + args.response.success(); + }, 500); + } + } + }, + actions: { add: { label: 'Add network offering', @@ -327,7 +481,7 @@ vlanId: { label: 'VLAN ID', isHidden: true, dependsOn: 'specifyVlan'}, - supportedServices: { + supportedServices: { label: 'Supported Services', dynamic: function(args) { @@ -343,7 +497,7 @@ }; fields[id.isEnabled] = { label: this, isBoolean: true }; - fields[id.provider] = { + fields[id.provider] = { label: this + ' Provider', isHidden: true, dependsOn: id.isEnabled, @@ -368,7 +522,7 @@ tags: { label: 'Tags' } } }, - + notification: { poll: testData.notifications.testPoll }, diff --git a/ui/scripts-test/network.js b/ui/scripts-test/network.js index 9b8f91c9692..18eb509b716 100644 --- a/ui/scripts-test/network.js +++ b/ui/scripts-test/network.js @@ -6,6 +6,49 @@ label: 'Select view' }, sections: { + networks: { + type: 'select', + title: 'Networks', + listView: { + filters: { + all: { label: 'All' }, + mine: { label: 'My network' } + }, + fields: { + displaytext: { label: 'Name' }, + traffictype: { label: 'Traffic Type' }, + gateway: { label: 'Gateway' }, + vlan: { label: 'VLAN' } + }, + dataProvider: testData.dataProvider.listView('networks'), + + detailView: { + name: 'Network details', + viewAll: { path: 'network.ipAddresses', label: 'IP Addresses' }, + tabs: { + details: { + title: 'Details', + fields: [ + { + displaytext: { label: 'Name' } + }, + { + name: { label: 'Short name' }, + traffictype: { label: 'Traffic Type' }, + gateway: { label: 'Gateway' }, + vlan: { label: 'VLAN' } + }, + { + startip: { label: 'Start IP' }, + endip: { label: 'End IP' } + } + ], + dataProvider: testData.dataProvider.detailView('networks') + } + } + } + } + }, ipAddresses: { type: 'select', title: 'IP Addresses', @@ -86,9 +129,6 @@ }) }, messages: { - confirm: function(args) { - return 'Are you sure you want to enable static NAT?'; - }, notification: function(args) { return 'Enabled Static NAT'; } @@ -113,48 +153,6 @@ notification: { poll: testData.notifications.customPoll({ isstaticnat: false }) } - }, - enableVPN: { - label: 'Enable VPN', - action: function(args) { - args.response.success(); - }, - messages: { - confirm: function(args) { - return 'Please confirm that you want VPN enabled for this IP address.'; - }, - notification: function(args) { - return 'Enabled VPN'; - }, - complete: function(args) { - return 'VPN is now enabled for IP ' + args.publicip + '.' - + '
Your IPsec pre-shared key is:
' + args.presharedkey; - } - }, - notification: { - poll: testData.notifications.customPoll({ - publicip: '10.2.2.1', - presharedkey: '23fudh881ssx88199488PP!#Dwdw', - vpnenabled: true - }) - } - }, - disableVPN: { - label: 'Disable VPN', - action: function(args) { - args.response.success(); - }, - messages: { - confirm: function(args) { - return 'Are you sure you want to disable VPN?'; - }, - notification: function(args) { - return 'Disabled VPN'; - } - }, - notification: { - poll: testData.notifications.customPoll({ vpnenabled: false }) - } } }, dataProvider: testData.dataProvider.listView('network'), @@ -174,6 +172,87 @@ return disabledTabs; }, + actions: { + enableStaticNAT: { + label: 'Enable static NAT', + action: { + noAdd: true, + custom: cloudStack.uiCustom.enableStaticNAT({ + listView: cloudStack.sections.instances, + action: function(args) { + args.response.success(); + } + }) + }, + messages: { + notification: function(args) { + return 'Enabled Static NAT'; + } + }, + notification: { + poll: testData.notifications.customPoll({ isstaticnat: true }) + } + }, + disableStaticNAT: { + label: 'Disable static NAT', + action: function(args) { + args.response.success(); + }, + messages: { + confirm: function(args) { + return 'Are you sure you want to disable static NAT?'; + }, + notification: function(args) { + return 'Disabled Static NAT'; + } + }, + notification: { + poll: testData.notifications.customPoll({ isstaticnat: false }) + } + }, + enableVPN: { + label: 'Enable VPN', + action: function(args) { + args.response.success(); + }, + messages: { + confirm: function(args) { + return 'Please confirm that you want VPN enabled for this IP address.'; + }, + notification: function(args) { + return 'Enabled VPN'; + }, + complete: function(args) { + return 'VPN is now enabled for IP ' + args.publicip + '.' + + '
Your IPsec pre-shared key is:
' + args.presharedkey; + } + }, + notification: { + poll: testData.notifications.customPoll({ + publicip: '10.2.2.1', + presharedkey: '23fudh881ssx88199488PP!#Dwdw', + vpnenabled: true + }) + } + }, + disableVPN: { + label: 'Disable VPN', + action: function(args) { + args.response.success(); + }, + messages: { + confirm: function(args) { + return 'Are you sure you want to disable VPN?'; + }, + notification: function(args) { + return 'Disabled VPN'; + } + }, + notification: { + poll: testData.notifications.customPoll({ vpnenabled: false }) + } + } + }, tabs: { details: { title: 'Details', diff --git a/ui/scripts-test/test-data.js b/ui/scripts-test/test-data.js index 040a9dc720d..7f96e4293dc 100644 --- a/ui/scripts-test/test-data.js +++ b/ui/scripts-test/test-data.js @@ -111,31 +111,15 @@ detailView: function(section) { return function(args) { setTimeout(function() { - var item = $.grep(testData.data[section], function(elem) { + var pulledData = $.grep(testData.data[section], function(elem) { return elem.id == args.id; - }); + })[0]; + + var item = $.extend(true, {}, pulledData, args.jsonObj); args.response.success({ - actionFilter: function(args) { - var allowedActions = args.context.actions; - var disallowedActions = []; - var item = args.context.item; - var status = item.state; - - if (status == 'Running' || status == 'Starting') { - disallowedActions.push('start'); - } else if (status == 'Stopped' || status == 'Stopping') { - disallowedActions.push('stop'); - disallowedActions.push('restart'); - } - - allowedActions = $.grep(allowedActions, function(item) { - return $.inArray(item, disallowedActions) == -1; - }); - - return allowedActions; - }, - data: item[0] + actionFilter: testData.actionFilter, + data: item }); }, 300); }; diff --git a/ui/scripts/ui/widgets/dataTable.js b/ui/scripts/ui/widgets/dataTable.js index 4dc96d77380..0b2d4e6d554 100644 --- a/ui/scripts/ui/widgets/dataTable.js +++ b/ui/scripts/ui/widgets/dataTable.js @@ -14,7 +14,7 @@ */ var withinResizeBounds = function($elem, posX) { var leftBound = $elem.offset().left + $elem.width() / 1.2; - + return posX > leftBound; }; @@ -165,7 +165,7 @@ computeEvenOddRows(); }; - + var resizeHeaders = function() { var $thead = $table.closest('div.data-table').find('thead'); var $tbody = $table.find('tbody'); @@ -209,6 +209,13 @@ refresh: function() { resizeHeaders(); computeEvenOddRows(); + }, + + selectRow: function(rowIndex) { + var $row = $($table.find('tbody tr')[rowIndex]); + + $row.siblings().removeClass('selected'); + $row.addClass('selected'); } }; diff --git a/ui/scripts/ui/widgets/detailView.js b/ui/scripts/ui/widgets/detailView.js index a95fdec34c1..5a5165b8451 100644 --- a/ui/scripts/ui/widgets/detailView.js +++ b/ui/scripts/ui/widgets/detailView.js @@ -67,6 +67,116 @@ var id = args.id; var context = $detailView.data('view-args').context; var _custom = $detailView.data('_custom'); + var customAction = action.action.custom; + var noAdd = action.noAdd; + + var updateTabContent = function(newData) { + var $detailViewElems = $detailView.find('ul.ui-tabs-nav, .detail-group').remove(); + $detailView.tabs('destroy'); + $detailView.data('view-args').jsonObj = newData; + + makeTabs( + $detailView, + $detailView.data('view-args').tabs, + { + context: context, + tabFilter: $detailView.data('view-args').tabFilter, + newData: newData + } + ).appendTo($detailView); + + $detailView.tabs(); + }; + + var performAction = function(data, options) { + if (!options) options = {}; + + var $form = options.$form; + + if (customAction && !noAdd) { + customAction({ + context: context, + complete: function(args) { + // Set loading appearance + var $loading = $('
').addClass('loading-overlay'); + $detailView.prepend($loading); + + args = args ? args : {}; + + var $item = args.$item; + + notification.desc = messages.notification(args.messageArgs); + notification._custom = args._custom; + + addNotification( + notification, + + // Success + function(args) { + $loading.remove(); + updateTabContent(args.data); + }, + + {}, + + // Error + function(args) {} + ); + } + }); + } else { + // Set loading appearance + var $loading = $('
').addClass('loading-overlay'); + $detailView.prepend($loading); + + action.action({ + data: data, + _custom: _custom, + ref: options.ref, + context: $detailView.data('view-args').context, + $form: $form, + response: { + success: function(args) { + args = args ? args : {}; + notification._custom = args._custom; + + if (additional && additional.success) additional.success(args); + + // Setup notification + addNotification( + notification, + function(args) { + if ($detailView.is(':visible')) { + $loading.remove(); + updateTabContent(args.data); + } + + if (messages.complete) { + cloudStack.dialog.notice({ + message: messages.complete(args.data) + }); + } + if (additional && additional.complete) additional.complete($.extend(true, args, { + $detailView: $detailView + })); + }, + + {}, + + // Error + function(args) { + + } + ); + }, + error: function(args) { + if (args.message) + cloudStack.dialog.notice({ message: args.message }); + } + } + }); + } + }; var externalLinkAction = action.action.externalLink; if (externalLinkAction) { @@ -87,102 +197,39 @@ + 'width=' + externalLinkAction.width + ',' + 'height=' + externalLinkAction.height ); - - return; - } + } else { + notification.desc = messages.notification(messageArgs); + notification.section = 'instances'; - notification.desc = messages.notification(messageArgs); - notification.section = 'instances'; - - var performAction = function(data, options) { - if (!options) options = {}; - - var $form = options.$form; - - // Set loading appearance - var $loading = $('
').addClass('loading-overlay'); - $detailView.prepend($loading); - - action.action({ - data: data, - _custom: _custom, - ref: options.ref, - context: $detailView.data('view-args').context, - $form: $form, - response: { - success: function(args) { - args = args ? args : {}; - notification._custom = args._custom; - - if (additional && additional.success) additional.success(args); - - // Setup notification - addNotification( - notification, - function(args) { - if ($detailView.is(':visible')) { - $loading.remove(); - - // Refresh actions - loadTabContent( - $detailView.find('.detail-group:visible'), - $detailView.data('view-args'), - { - newData: args.data - } - ); - } - - if (messages.complete) { - cloudStack.dialog.notice({ - message: messages.complete(args.data) - }); - } - if (additional && additional.complete) additional.complete($.extend(true, args, { - $detailView: $detailView - })); - }, - - {}, - - // Error - function(args) { - - } - ); + if (!action.createForm) { + if (messages && messages.confirm) { + cloudStack.dialog.confirm({ + message: messages.confirm(messageArgs), + action: function() { + performAction({ + id: id + }); + } + }); + } else { + performAction({ id: id }); + } + } else { + cloudStack.dialog.createForm({ + form: action.createForm, + after: function(args) { + performAction(args.data, { + ref: args.ref, + context: $detailView.data('view-args').context, + $form: args.$form + }); }, - error: function(args) { - if (args.message) - cloudStack.dialog.notice({ message: args.message }); - } - } - }); - }; - - if (!action.createForm) - cloudStack.dialog.confirm({ - message: messages.confirm(messageArgs), - action: function() { - performAction({ + ref: { id: id - }); - } - }); - else { - cloudStack.dialog.createForm({ - form: action.createForm, - after: function(args) { - performAction(args.data, { - ref: args.ref, - context: $detailView.data('view-args').context, - $form: args.$form - }); - }, - ref: { - id: id - }, - context: $detailView.data('view-args').context - }); + }, + context: $detailView.data('view-args').context + }); + } } }, @@ -207,7 +254,7 @@ */ edit: function($detailView, args) { if ($detailView.find('.button.done').size()) return false; - + // Convert value TDs var $inputs = $detailView.find('input[type=text], select'); var action = args.actions[args.actionName]; @@ -235,7 +282,7 @@ ); $value.data('detail-view-selected-option', $input.find('option:selected').val()); } - }); + }); }; var applyEdits = function($inputs, $editButton) { @@ -269,7 +316,7 @@ convertInputs($inputs); addNotification( notificationArgs, function(data) {}, [] - ); + ); } else { $loading.appendTo($detailView); addNotification( @@ -505,8 +552,8 @@ if (tabData.preFilter) { hiddenFields = tabData.preFilter({ context: context, - fields: $.map(fields, function(fieldGroup) { - return $.map(fieldGroup, function(value, key) { return key; }); + fields: $.map(fields, function(fieldGroup) { + return $.map(fieldGroup, function(value, key) { return key; }); }) }); } @@ -634,6 +681,7 @@ var isMultiple = tabs.multiple || tabs.isMultiple; var viewAll = args.viewAll; var $detailView = $tabContent.closest('.detail-view'); + var jsonObj = $detailView.data('view-args').jsonObj; if (tabs.custom) { return tabs.custom({ @@ -659,14 +707,14 @@ return dataProvider({ tab: targetTabID, id: args.id, - jsonObj: options.newData ? $.extend(true, {}, args.jsonObj, options.newData) : args.jsonObj, + jsonObj: jsonObj, context: args.context, response: { success: function(args) { if (options.newData) { $.extend(args.data, options.newData); } - + if (args._custom) { $detailView.data('_custom', args._custom); } @@ -708,6 +756,78 @@ }); }; + var makeTabs = function($detailView, tabs, options) { + if (!options) options = {}; + + var $tabs = $('
    '); + var $tabContentGroup = $('
    '); + var removedTabs = []; + var tabFilter = options.tabFilter; + var context = options.context ? options.context : {}; + + if (options.newData) { + $.extend( + context[$detailView.data('view-args').section][0], + options.newData + ); + } + + if (tabFilter) { + removedTabs = tabFilter({ + context: context + }); + } + + $.each(tabs, function(key, value) { + // Don't render tab, if filtered out + if ($.inArray(key, removedTabs) > -1) return true; + + var propGroup = key; + var prop = value; + var title = prop.title; + var $tab = $('
  • ').attr('detail-view-tab', true).appendTo($tabs); + + var $tabLink = $('').attr({ + href: '#details-tab-' + propGroup + }).html(title).appendTo($tab); + + var $tabContent = $('
    ').attr({ + id: 'details-tab-' + propGroup + }).addClass('detail-group').appendTo($tabContentGroup); + + $tabContent.data('detail-view-tab-id', key); + $tabContent.data('detail-view-tab-data', value); + + return true; + }); + + $tabs.find('li:first').addClass('first'); + $tabs.find('li:last').addClass('last'); + + return $.merge( + $tabs, $tabContentGroup.children() + ); + }; + + var replaceTabs = function($detailView, $newTabs, tabs, options) { + var $detailViewElems = $detailView.find('ul.ui-tabs-nav, .detail-group'); + $detailView.tabs('destroy'); + $detailViewElems.remove(); + + makeTabs($detailView, tabs, options).appendTo($detailView); + }; + + var makeToolbar = function() { + return $('
    ') + .append( + $('
    ') + .addClass('button refresh') + .append( + $('').html('Refresh') + ) + ); + }; + $.fn.detailView = function(args) { var $detailView = this; @@ -715,49 +835,13 @@ $detailView.data('view-args', args); // Create toolbar - var $toolbar = $('
    ') - .append( - $('
    ') - .addClass('button refresh') - .append( - $('').html('Refresh') - ) - ) - .appendTo($detailView); - var $tabs = $('
      ').appendTo($detailView); + var $toolbar = makeToolbar().appendTo($detailView); - // Get tab filter, if present - var removedTabs = []; - if (args.tabFilter) { - removedTabs = args.tabFilter({ - context: args.context - }); - } - - // Make tabs - $.each(args.tabs, function(key, value) { - // Don't render tab, if filtered out - if ($.inArray(key, removedTabs) > -1) return true; - - var propGroup = key; - var prop = value; - var title = prop.title; - var $tab = $('
    • ').attr('detail-view-tab', true).appendTo($tabs); - - var $tabLink = $('').attr({ - href: '#details-tab-' + propGroup - }).html(title).appendTo($tab); - - var $tabContent = $('
      ').attr({ - id: 'details-tab-' + propGroup - }).addClass('detail-group').appendTo($detailView); - - $tabContent.data('detail-view-tab-id', key); - $tabContent.data('detail-view-tab-data', value); - }); - - $tabs.find('li:first').addClass('first'); - $tabs.find('li:last').addClass('last'); + // Create tabs + var $tabs = makeTabs($detailView, args.tabs, { + context: args.context, + tabFilter: args.tabFilter + }).appendTo($detailView); $detailView.tabs(); diff --git a/ui/scripts/ui/widgets/listView.js b/ui/scripts/ui/widgets/listView.js index 8fc2f31e6bd..6206ca06d63 100644 --- a/ui/scripts/ui/widgets/listView.js +++ b/ui/scripts/ui/widgets/listView.js @@ -314,7 +314,7 @@ // Hide edit field, validate and save changes var showLabel = function(val, options) { if (!options) options = {}; - + var oldVal = $label.html(); $label.html(val); @@ -376,7 +376,7 @@ 'Unset value for ' + $instanceRow.find('td.name span').html() }, function(args) { - + }, [{ name: newName }] ); @@ -390,6 +390,62 @@ } }; + var rowActions = { + _std: function($tr, action) { + action(); + + $tr.closest('.data-table').dataTable('refresh'); + + setTimeout(function() { + $tr.closest('.data-table').dataTable('selectRow', $tr.index()); + }, 0); + }, + + moveTop: function($tr) { + rowActions._std($tr, function() { + $tr.closest('tbody').prepend($tr); + $tr.closest('.list-view').animate({ scrollTop: 0 }); + }); + }, + + moveBottom: function($tr) { + rowActions._std($tr, function() { + $tr.closest('tbody').append($tr); + $tr.closest('.list-view').animate({ scrollTop: 0 }); + }); + }, + + moveUp: function($tr) { + rowActions._std($tr, function() { + $tr.prev().before($tr); + }); + }, + + moveDown: function($tr) { + rowActions._std($tr, function() { + $tr.next().after($tr); + }); + }, + + moveTo: function($tr, index, after) { + rowActions._std($tr, function() { + var $target = $tr.closest('tbody').find('tr').filter(function() { + return $(this).index() == index; + }); + + if ($target.index() > $tr.index()) $target.after($tr); + else $target.before($tr); + + $tr.closest('.list-view').scrollTop($tr.position().top - $tr.height() * 2); + + if (after) + setTimeout(function() { + after(); + }); + }); + } + }; + /** * Edit field text * @@ -422,8 +478,20 @@ return $editArea.hide(); }; - var createHeader = function(fields, $table, actions) { + var renderActionCol = function(actions) { + return $.grep( + $.map(actions, function(value, key) { + return key; + }), + function(elem) { return elem != 'add'; } + ).length; + }; + + var createHeader = function(fields, $table, actions, options) { + if (!options) options = {}; + var $thead = $('').appendTo($table); + var reorder = options.reorder; $.each(fields, function(key) { var field = this; @@ -434,7 +502,7 @@ $th.html(field.label); }); - if (actions) { + if (actions && renderActionCol(actions)) { $thead.append( $('') .html('Actions') @@ -442,6 +510,12 @@ ); } + if (reorder) { + $thead.append( + $('').html('Order').addClass('reorder-actions reduced-hide') + ); + } + return $thead; }; @@ -452,7 +526,7 @@ $filters.append(''); var $filterSelect = $('').appendTo($filters); - + if (filters) $.each(filters, function(key) { var $option = $('