CLOUDSTACK-10129: UX improvements and event timeline

- Fixes timezone issue where dates show up as nvalid in UI
- Introduces new event timeline listing/filtering of events
- Several UI improvements to add columns in list views
- Bulk operations support in instance list view to shutdown and destroy
  multiple-selected VMs (limitation: after operation, redundant entries
  may show up in the list view, refreshing VM list view fixes that)
- Align table thead/tbody to avoid splitting of tables

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Rohit Yadav 2017-11-28 19:25:09 +05:30
parent d0005d8353
commit 0102e8593d
17 changed files with 413 additions and 167 deletions

View File

@ -205,6 +205,7 @@ public class ApiConstants {
public static final String OUTOFBANDMANAGEMENT_POWERSTATE = "outofbandmanagementpowerstate";
public static final String OUTOFBANDMANAGEMENT_ENABLED = "outofbandmanagementenabled";
public static final String PARAMS = "params";
public static final String PARENT_ID = "parentid";
public static final String PARENT_DOMAIN_ID = "parentdomainid";
public static final String PASSWORD = "password";
public static final String SHOULD_UPDATE_PASSWORD = "update_passwd_on_host";
@ -274,6 +275,7 @@ public class ApiConstants {
public static final String SNAPSHOT_QUIESCEVM = "quiescevm";
public static final String SOURCE_ZONE_ID = "sourcezoneid";
public static final String START_DATE = "startdate";
public static final String START_ID = "startid";
public static final String START_IP = "startip";
public static final String START_IPV6 = "startipv6";
public static final String START_PORT = "startport";

View File

@ -65,6 +65,9 @@ public class ListEventsCmd extends BaseListProjectAndAccountResourcesCmd {
@Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, description = "the event type (see event types)")
private String type;
@Parameter(name = ApiConstants.START_ID, type = CommandType.UUID, entityType = EventResponse.class, description = "the parent/start ID of the event, when provided this will list all the events with the start/parent ID including the parent event")
private Long startId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -97,6 +100,10 @@ public class ListEventsCmd extends BaseListProjectAndAccountResourcesCmd {
return type;
}
public Long getStartId() {
return startId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////

View File

@ -78,7 +78,7 @@ public class EventResponse extends BaseResponse implements ControlledViewEntityR
@Param(description = "the state of the event")
private Event.State state;
@SerializedName("parentid")
@SerializedName(ApiConstants.PARENT_ID)
@Param(description = "whether the event is parented")
private String parentId;

View File

@ -33,6 +33,7 @@ import org.apache.cloudstack.api.InternalIdentity;
import com.cloud.utils.db.Encrypt;
import com.cloud.utils.db.GenericDao;
import com.google.common.base.Strings;
@Entity
@Table(name = "user")
@ -257,6 +258,9 @@ public class UserAccountVO implements UserAccount, InternalIdentity {
@Override
public String getTimezone() {
if (Strings.isNullOrEmpty(timezone)) {
return "UTC";
}
return timezone;
}

View File

@ -34,6 +34,7 @@ import org.apache.cloudstack.api.InternalIdentity;
import com.cloud.user.Account.State;
import com.cloud.utils.db.Encrypt;
import com.cloud.utils.db.GenericDao;
import com.google.common.base.Strings;
/**
* A bean representing a user
@ -233,6 +234,9 @@ public class UserVO implements User, Identity, InternalIdentity {
@Override
public String getTimezone() {
if (Strings.isNullOrEmpty(timezone)) {
return "UTC";
}
return timezone;
}

View File

@ -520,6 +520,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
String keyword = cmd.getKeyword();
Integer entryTime = cmd.getEntryTime();
Integer duration = cmd.getDuration();
Long startId = cmd.getStartId();
Ternary<Long, Boolean, ListProjectResourcesCriteria> domainIdRecursiveListProject = new Ternary<Long, Boolean, ListProjectResourcesCriteria>(
cmd.getDomainId(), cmd.isRecursive(), null);
@ -542,7 +543,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
sb.and("createDateG", sb.entity().getCreateDate(), SearchCriteria.Op.GTEQ);
sb.and("createDateL", sb.entity().getCreateDate(), SearchCriteria.Op.LTEQ);
sb.and("state", sb.entity().getState(), SearchCriteria.Op.NEQ);
sb.and("startId", sb.entity().getStartId(), SearchCriteria.Op.EQ);
sb.or("startId", sb.entity().getStartId(), SearchCriteria.Op.EQ);
sb.and("createDate", sb.entity().getCreateDate(), SearchCriteria.Op.BETWEEN);
sb.and("displayEvent", sb.entity().getDisplay(), SearchCriteria.Op.EQ);
sb.and("archived", sb.entity().getArchived(), SearchCriteria.Op.EQ);
@ -561,6 +562,13 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
sc.setParameters("id", id);
}
if (startId != null) {
sc.setParameters("startId", startId);
if (id == null) {
sc.setParameters("id", startId);
}
}
if (keyword != null) {
SearchCriteria<EventJoinVO> ssc = _eventJoinDao.createSearchCriteria();
ssc.addOr("type", SearchCriteria.Op.LIKE, "%" + keyword + "%");

View File

@ -96,7 +96,7 @@ a:hover {
/*Table*/
table {
width: 940px;
width: 955px;
max-width: 977px;
margin: 15px 15px 12px 12px;
font-size: 13px;
@ -1307,7 +1307,6 @@ div.panel div.list-view {
div.panel div.list-view div.data-table table {
width: 955px;
margin-top: 44px;
}
.detail-view div.list-view div.data-table table {

View File

@ -558,6 +558,7 @@ var dictionary = {"ICMP.code":"ICMP Code",
"label.console.proxy.vm":"Console Proxy VM",
"label.continue":"Continue",
"label.continue.basic.install":"Continue with basic installation",
"label.control.ip":"Control IP",
"label.copying.iso":"Copying ISO",
"label.corrections.saved":"Corrections saved",
"label.counter":"Counter",
@ -751,6 +752,7 @@ var dictionary = {"ICMP.code":"ICMP Code",
"label.event":"Event",
"label.event.archived":"Event Archived",
"label.event.deleted":"Event Deleted",
"label.event.timeline":"Event Timeline",
"label.every":"Every",
"label.example":"Example",
"label.expunge":"Expunge",
@ -995,6 +997,7 @@ var dictionary = {"ICMP.code":"ICMP Code",
"label.manage":"Manage",
"label.manage.resources":"Manage Resources",
"label.managed":"Managed",
"label.managed.state":"Managed State",
"label.management":"Management",
"label.management.ips":"Management IP Addresses",
"label.management.server":"Management Server",

View File

@ -46,12 +46,12 @@
label: 'label.type',
truncate: true
},
domain: {
label: 'label.domain'
},
account: {
label: 'label.account'
},
domain: {
label: 'label.domain'
},
created: {
label: 'label.date',
converter: cloudStack.converters.toLocalDate
@ -338,6 +338,14 @@
var data = {};
listViewDataProvider(args, data);
if ("events" in args.context) {
var startId = args.context.events[0].parentid;
if (!startId) {
startId = args.context.events[0].id;
}
data.startid = startId;
}
$.ajax({
url: createURL('listEvents'),
data: data,
@ -357,8 +365,12 @@
},
detailView: {
name: 'label.details',
actions: {
viewAll: {
path: 'events',
label: 'label.event.timeline',
},
actions: {
// Remove single event
remove: {
label: 'label.delete',

View File

@ -17,6 +17,153 @@
(function($, cloudStack) {
var vmMigrationHostObjs, ostypeObjs;
var vmStopAction = function(args) {
var action = {
messages: {
confirm: function(args) {
return 'message.action.stop.instance';
},
notification: function(args) {
return 'label.action.stop.instance';
}
},
label: 'label.action.stop.instance',
compactLabel: 'label.stop',
addRow: 'false',
createForm: {
title: 'notification.stop.instance',
desc: 'message.action.stop.instance',
fields: {
forced: {
label: 'force.stop',
isBoolean: true,
isChecked: false
}
}
},
action: function(args) {
var instances = args.context.instances;
$(instances).map(function(index, instance) {
var data = {
id: instance.id,
forced: (args.data.forced == "on")
};
$.ajax({
url: createURL("stopVirtualMachine"),
data: data,
dataType: "json",
success: function(json) {
var jid = json.stopvirtualmachineresponse.jobid;
args.response.success({
_custom: {
jobId: jid,
getUpdatedItem: function(json) {
return $.extend(json.queryasyncjobresultresponse.jobresult.virtualmachine, { hostid: null });
},
getActionFilter: function() {
return vmActionfilter;
}
}
});
},
error: function(json) {
args.response.error(parseXMLHttpResponse(json));
}
});
});
},
notification: {
poll: pollAsyncJobResult
}
};
if (args && args.listView) {
$.extend(action, {
isHeader: true,
isMultiSelectAction: true
});
}
return action;
};
var vmDestroyAction = function(args) {
var action = {
messages: {
notification: function(args) {
return 'label.action.destroy.instance';
}
},
label: 'label.action.destroy.instance',
compactLabel: 'label.destroy',
addRow: 'false',
createForm: {
title: 'label.action.destroy.instance',
desc: 'label.action.destroy.instance',
isWarning: true,
preFilter: function(args) {
if (! g_allowUserExpungeRecoverVm) {
args.$form.find('.form-item[rel=expunge]').hide();
}
},
fields: {
expunge: {
label: 'label.expunge',
isBoolean: true,
isChecked: false
}
}
},
action: function(args) {
var instances = args.context.instances;
$(instances).map(function(index, instance) {
var data = {
id: instance.id
};
if (args.data.expunge == 'on') {
$.extend(data, {
expunge: true
});
}
$.ajax({
url: createURL('destroyVirtualMachine'),
data: data,
success: function(json) {
var jid = json.destroyvirtualmachineresponse.jobid;
args.response.success({
_custom: {
jobId: jid,
getUpdatedItem: function(json) {
if ('virtualmachine' in json.queryasyncjobresultresponse.jobresult) //destroy without expunge
return json.queryasyncjobresultresponse.jobresult.virtualmachine;
else //destroy with expunge
return { 'toRemove': true };
},
getActionFilter: function() {
return vmActionfilter;
}
}
});
}
});
});
},
notification: {
poll: pollAsyncJobResult
}
};
if (args && args.listView) {
$.extend(action, {
isHeader: true,
isMultiSelectAction: true
});
}
return action;
};
var vmSnapshotAction = function(args) {
var action = {
messages: {
@ -162,6 +309,7 @@
var hiddenFields = [];
if (!isAdmin()) {
hiddenFields.push('instancename');
hiddenFields.push('account');
}
return hiddenFields;
},
@ -180,16 +328,26 @@
ipaddress: {
label: 'label.ip.address'
},
account: {
label: 'label.account'
},
zonename: {
label: 'label.zone.name'
},
state: {
label: 'label.state',
label: 'label.metrics.state',
converter: function (str) {
// For localization
return str;
},
indicator: {
'Running': 'on',
'Stopped': 'off',
'Error': 'off',
'Destroyed': 'off',
'Error': 'off'
'Expunging': 'off',
'Stopping': 'warning',
'Shutdowned': 'warning'
}
}
},
@ -304,6 +462,8 @@
poll: pollAsyncJobResult
}
},
stop: vmStopAction({ listView: true}),
destroy: vmDestroyAction({ listView: true }),
snapshot: vmSnapshotAction({ listView: true }),
viewMetrics: {
label: 'label.metrics',
@ -683,55 +843,7 @@
poll: pollAsyncJobResult
}
},
stop: {
label: 'label.action.stop.instance',
compactLabel: 'label.stop',
createForm: {
title: 'notification.stop.instance',
desc: 'message.action.stop.instance',
fields: {
forced: {
label: 'force.stop',
isBoolean: true,
isChecked: false
}
}
},
action: function(args) {
var array1 = [];
array1.push("&forced=" + (args.data.forced == "on"));
$.ajax({
url: createURL("stopVirtualMachine&id=" + args.context.instances[0].id + array1.join("")),
dataType: "json",
async: true,
success: function(json) {
var jid = json.stopvirtualmachineresponse.jobid;
args.response.success({
_custom: {
jobId: jid,
getUpdatedItem: function(json) {
return $.extend(json.queryasyncjobresultresponse.jobresult.virtualmachine, { hostid: null });
},
getActionFilter: function() {
return vmActionfilter;
}
}
});
}
});
},
messages: {
confirm: function(args) {
return 'message.action.stop.instance';
},
notification: function(args) {
return 'label.action.stop.instance';
}
},
notification: {
poll: pollAsyncJobResult
}
},
stop: vmStopAction(),
restart: {
label: 'label.action.reboot.instance',
compactLabel: 'label.reboot',
@ -775,66 +887,7 @@
}
},
snapshot: vmSnapshotAction(),
destroy: {
label: 'label.action.destroy.instance',
compactLabel: 'label.destroy',
createForm: {
title: 'label.action.destroy.instance',
desc: 'label.action.destroy.instance',
isWarning: true,
preFilter: function(args) {
if (! g_allowUserExpungeRecoverVm) {
args.$form.find('.form-item[rel=expunge]').hide();
}
},
fields: {
expunge: {
label: 'label.expunge',
isBoolean: true,
isChecked: false
}
}
},
messages: {
notification: function(args) {
return 'label.action.destroy.instance';
}
},
action: function(args) {
var data = {
id: args.context.instances[0].id
};
if (args.data.expunge == 'on') {
$.extend(data, {
expunge: true
});
}
$.ajax({
url: createURL('destroyVirtualMachine'),
data: data,
success: function(json) {
var jid = json.destroyvirtualmachineresponse.jobid;
args.response.success({
_custom: {
jobId: jid,
getUpdatedItem: function(json) {
if ('virtualmachine' in json.queryasyncjobresultresponse.jobresult) //destroy without expunge
return json.queryasyncjobresultresponse.jobresult.virtualmachine;
else //destroy with expunge
return { 'toRemove': true };
},
getActionFilter: function() {
return vmActionfilter;
}
}
});
}
});
},
notification: {
poll: pollAsyncJobResult
}
},
destroy: vmDestroyAction(),
expunge: {
label: 'label.action.expunge.instance',
compactLabel: 'label.expunge',

View File

@ -293,8 +293,6 @@
'Disconnected': 'off',
'Removed': 'off',
'Error': 'off',
'Connecting': 'transition',
'Rebalancing': 'transition',
'Alert': 'warning'
},
compact: true
@ -448,9 +446,7 @@
'Error': 'off',
'Destroyed': 'off',
'Expunging': 'off',
'Stopping': 'transition',
'Starting': 'transition',
'Migrating': 'transition',
'Stopping': 'warning',
'Shutdowned': 'warning'
},
compact: true
@ -560,13 +556,12 @@
return str;
},
indicator: {
'Allocated': 'transition',
'Creating': 'transition',
'Allocated': 'on',
'Ready': 'on',
'Destroy': 'off',
'Expunging': 'off',
'Migrating': 'warning',
'UploadOp': 'transition',
'UploadOp': 'warning',
'Snapshotting': 'warning',
},
compact: true
@ -651,7 +646,7 @@
'Down': 'off',
'Removed': 'off',
'ErrorInMaintenance': 'off',
'PrepareForMaintenance': 'transition',
'PrepareForMaintenance': 'warning',
'CancelMaintenance': 'warning',
'Maintenance': 'warning',
},

View File

@ -808,13 +808,16 @@
},
id: 'networks',
preFilter: function(args) {
if (isAdmin() || isDomainAdmin()) {
return []
}
return ['account']
},
fields: {
name: {
label: 'label.name'
},
account: {
label: 'label.account'
},
type: {
label: 'label.type'
},
@ -823,6 +826,27 @@
},
ip6cidr: {
label: 'label.ipv6.CIDR'
},
account: {
label: 'label.account'
},
zonename: {
label: 'label.zone'
},
state: {
converter: function(str) {
// For localization
return str;
},
label: 'label.state',
indicator: {
'Allocated': 'on',
'Released': 'off',
'Destroy': 'off',
'Shutdown': 'off',
'Setup': 'warning',
'Implemented': 'on'
}
}
},
@ -1635,11 +1659,14 @@
networkid: args.context.networks[0].id
},
dataType: 'json',
async: true,
async: false,
success: function(json) {
var response = json.listegressfirewallrulesresponse.firewallrule ?
json.listegressfirewallrulesresponse.firewallrule : [];
if (response.length > 0) {
isConfigRulesMsgShown = true;
}
args.response.success({
data: $.map(response, function(rule) {
if (rule.protocol == 'all') {
@ -1899,6 +1926,12 @@
listView: {
id: 'ipAddresses',
label: 'label.ips',
preFilter: function(args) {
if (isAdmin()) {
return ['account']
}
return []
},
fields: {
ipaddress: {
label: 'label.ips',
@ -1910,12 +1943,18 @@
return text;
}
},
zonename: {
label: 'label.zone'
associatednetworkname: {
label: 'label.network'
},
virtualmachinedisplayname: {
label: 'label.vm.name'
},
account: {
label: 'label.account'
},
zonename: {
label: 'label.zone'
},
state: {
converter: function(str) {
// For localization

View File

@ -1285,7 +1285,7 @@ cloudStack.converters = {
var disconnected = new Date();
disconnected.setISO8601(UtcDate);
if (g_timezoneoffset != null) {
if (g_timezoneoffset != null && g_timezoneoffset != "null") {
localDate = disconnected.getTimePlusTimezoneOffset(g_timezoneoffset);
} else {
var browserDate = new Date();

View File

@ -36,8 +36,10 @@
label: 'label.volumes',
preFilter: function(args) {
var hiddenFields = [];
if (isAdmin() != true)
if (isAdmin() != true) {
hiddenFields.push('hypervisor');
hiddenFields.push('account');
}
return hiddenFields;
},
fields: {
@ -47,11 +49,33 @@
type: {
label: 'label.type'
},
vmdisplayname: {
label: 'label.vm.display.name'
},
hypervisor: {
label: 'label.hypervisor'
},
vmdisplayname: {
label: 'label.vm.display.name'
account: {
label: 'label.account'
},
zonename: {
label: 'label.zone'
},
state: {
label: 'label.metrics.state',
converter: function (str) {
// For localization
return str;
},
indicator: {
'Allocated': 'on',
'Ready': 'on',
'Destroy': 'off',
'Expunging': 'off',
'Migrating': 'warning',
'UploadOp': 'warning',
'Snapshotting': 'warning',
}
}
},

View File

@ -39,7 +39,7 @@
router.guestnetworkname = router.vpcname;
}
if ("isredundantrouter" in router && router.isredundantrouter) {
if (router.isredundantrouter) {
router.guestnetworkname = router.guestnetworkname + " (" + router.redundantstate + ")";
}
@ -2180,6 +2180,12 @@
},
isolationmethods: {
label: 'label.isolation.method'
},
vlan: {
label: 'label.vlan'
},
broadcastdomainrange: {
label: 'label.broadcast.domain.range'
}
},
@ -9205,6 +9211,14 @@
data: data,
success: function (json) {
var systemvmObjs = json.listsystemvmsresponse.systemvm;
$(systemvmObjs).each(function(idx, item) {
var controlIp = item.linklocalip;
if (item.hypervisor == "VMware") {
var controlIp = item.privateip;
}
item.controlip = controlIp;
});
if (systemvmObjs != undefined) {
$.ajax({
url: createURL('listHosts'),
@ -9585,16 +9599,19 @@
label: 'label.name'
},
publicip: {
label: 'label.public.ip'
label: 'label.ip'
},
account: {
label: 'label.account'
routerType: {
label: 'label.type'
},
guestnetworkname: {
label: 'label.network'
},
routerType: {
label: 'label.type'
account: {
label: 'label.account'
},
hostname: {
label: 'label.host'
},
state: {
converter: function (str) {
@ -10976,6 +10993,12 @@
return args;
}
},
controlip: {
label: 'label.control.ip'
},
hostname: {
label: 'label.host'
},
zonename: {
label: 'label.zone'
},
@ -13293,12 +13316,19 @@
netmask: {
label: 'label.netmask'
},
zonename: {
label: 'label.zone'
},
allocationstate: {
converter: function (str) {
// For localization
return str;
},
label: 'label.allocation.state'
label: 'label.allocation.state',
indicator: {
'Enabled': 'on',
'Disabled': 'off'
}
}
},
@ -13934,23 +13964,27 @@
name: {
label: 'label.name'
},
podname: {
label: 'label.pod'
},
hypervisortype: {
label: 'label.hypervisor'
},
//allocationstate: { label: 'label.allocation.state' },
//managedstate: { label: 'Managed State' },
zonename: {
label: 'label.zone'
},
podname: {
label: 'label.pod'
},
managedstate: {
label: 'label.managed.state'
},
allocationstate: {
converter: function (str) {
// For localization
return str;
},
label: 'label.state',
label: 'label.allocation.state',
indicator: {
'Enabled': 'on',
'Destroyed': 'off'
'Disabled': 'off'
}
}
},
@ -15487,15 +15521,26 @@
name: {
label: 'label.name'
},
ipaddress: {
label: 'label.ip.address'
},
hypervisor: {
label: 'label.hypervisor'
},
zonename: {
label: 'label.zone'
},
podname: {
label: 'label.pod'
},
clustername: {
label: 'label.cluster'
},
resourcestate: {
label: 'label.resource.state',
indicator: {
'Enabled': 'on',
'Disabled': 'off',
'Maintenance': 'warning'
}
},
state: {
label: 'label.state',
indicator: {
@ -17497,12 +17542,34 @@
label: 'label.path',
truncate: true
},
type: {
label: 'label.type'
},
scope: {
label: 'label.scope'
},
clustername: {
label: 'label.cluster',
truncate: true
},
scope: {
label: 'label.scope'
zonename: {
label: 'label.zone'
},
state: {
label: 'label.state',
converter: function (str) {
// For localization
return str;
},
indicator: {
'Up': 'on',
'Down': 'off',
'Removed': 'off',
'ErrorInMaintenance': 'off',
'PrepareForMaintenance': 'warning',
'CancelMaintenance': 'warning',
'Maintenance': 'warning',
}
}
},
@ -19492,8 +19559,17 @@
name: {
label: 'label.name'
},
url: {
label: 'label.url'
},
protocol: {
label: 'label.protocol'
},
scope: {
label: 'label.scope'
},
zonename: {
label: 'label.zone'
}
},

View File

@ -55,12 +55,24 @@
label: 'label.community'
}
},
preFilter: function() {
if (isAdmin()||isDomainAdmin()) {
return []
}
return ['account']
},
fields: {
name: {
label: 'label.name'
},
hypervisor: {
label: 'label.hypervisor'
},
ostypename: {
label: 'label.os.type'
},
account: {
label: 'label.account'
}
},
@ -2038,9 +2050,21 @@
label: 'label.community'
}
},
preFilter: function() {
if (isAdmin()||isDomainAdmin()) {
return []
}
return ['account']
},
fields: {
name: {
label: 'label.name'
},
ostypename: {
label: 'label.os.type'
},
account: {
label: 'label.account'
}
},
@ -2347,7 +2371,10 @@
id: item.id,
name: item.name,
description: item.description,
ostypename: item.ostypename,
ostypeid: item.ostypeid,
account: item.account,
domain: item.domain,
zones: item.zonename,
zoneids: [item.zoneid]
};

View File

@ -78,19 +78,12 @@
return true;
};
var splitTable = function() {
var reattachTable = function() {
var $mainContainer = $('<div>')
.addClass('data-table')
.appendTo($table.parent())
.append(
$table.detach()
.append($table.detach()
);
$table = $mainContainer;
var $theadContainer = $('<div>').addClass('fixed-header').prependTo($table);
var $theadTable = $('<table>').appendTo($theadContainer).attr('nowrap', 'nowrap');
var $thead = $table.find('thead').detach().appendTo($theadTable);
return $thead;
};
/**
@ -289,7 +282,7 @@
var init = function() {
var noSelect = options && options.noSelect == true ? true : false;
if (!$table.closest('div.data-table').size() && !$table.hasClass('no-split')) {
splitTable();
reattachTable();
$table.find('tbody').closest('table').addClass('body');
}