Prevent long names breaking table layouts

Adds a word-break class that can be added to tables to prevent large
column values breaking the table shape.

Change-Id: Icca10d9c29254d176dc7f8b7c039bc19c3f52c72
Related-Bug: 1565724
Closes-Bug: 1584785
This commit is contained in:
Rob Cresswell 2016-05-31 15:12:57 +01:00
parent 3f35caf180
commit 07d33cf462
36 changed files with 109 additions and 96 deletions

View File

@ -26,6 +26,7 @@ from horizon.tables.actions import UpdateAction # noqa
from horizon.tables.base import Column # noqa
from horizon.tables.base import DataTable # noqa
from horizon.tables.base import Row # noqa
from horizon.tables.base import WrappingColumn # noqa
from horizon.tables.views import DataTableView # noqa
from horizon.tables.views import MixedDataTableView # noqa
from horizon.tables.views import MultiTableMixin # noqa

View File

@ -463,6 +463,14 @@ class Column(html.HTMLElement):
return None
class WrappingColumn(Column):
"""A column that wraps its contents. Useful for data like UUIDs or names"""
def __init__(self, *args, **kwargs):
super(WrappingColumn, self).__init__(*args, **kwargs)
self.classes.append('word-break')
class Row(html.HTMLElement):
"""Represents a row in the table.

View File

@ -135,7 +135,7 @@ def safe_unordered_list(value):
class HostAggregatesTable(tables.DataTable):
name = tables.Column('name', verbose_name=_('Name'))
name = tables.WrappingColumn('name', verbose_name=_('Name'))
availability_zone = tables.Column('availability_zone',
verbose_name=_('Availability Zone'))
hosts = tables.Column(get_aggregate_hosts,
@ -161,8 +161,8 @@ class HostAggregatesTable(tables.DataTable):
class AvailabilityZonesTable(tables.DataTable):
name = tables.Column('zoneName',
verbose_name=_('Availability Zone Name'))
name = tables.WrappingColumn('zoneName',
verbose_name=_('Availability Zone Name'))
hosts = tables.Column(get_zone_hosts,
verbose_name=_('Hosts'),
wrap_list=True,

View File

@ -143,7 +143,7 @@ def get_extra_specs(flavor):
class FlavorsTable(tables.DataTable):
name = tables.Column('name', verbose_name=_('Flavor Name'))
name = tables.WrappingColumn('name', verbose_name=_('Flavor Name'))
vcpus = tables.Column('vcpus', verbose_name=_('VCPUs'))
ram = tables.Column(get_size,
verbose_name=_('RAM'),

View File

@ -124,7 +124,7 @@ class ComputeHostTable(tables.DataTable):
u"Down")),
)
host = tables.Column('host', verbose_name=_('Host'))
host = tables.WrappingColumn('host', verbose_name=_('Host'))
zone = tables.Column('zone', verbose_name=_('Availability zone'))
status = tables.Column('status',
status=True,

View File

@ -19,9 +19,9 @@ from horizon.templatetags import sizeformat
class AdminHypervisorsTable(tables.DataTable):
hostname = tables.Column("hypervisor_hostname",
link="horizon:admin:hypervisors:detail",
verbose_name=_("Hostname"))
hostname = tables.WrappingColumn("hypervisor_hostname",
link="horizon:admin:hypervisors:detail",
verbose_name=_("Hostname"))
hypervisor_type = tables.Column("hypervisor_type",
verbose_name=_("Type"))
@ -65,9 +65,9 @@ class AdminHypervisorsTable(tables.DataTable):
class AdminHypervisorInstancesTable(tables.DataTable):
name = tables.Column("name",
link="horizon:admin:instances:detail",
verbose_name=_("Instance Name"))
name = tables.WrappingColumn("name",
link="horizon:admin:instances:detail",
verbose_name=_("Instance Name"))
instance_id = tables.Column("uuid",
verbose_name=_("Instance ID"))

View File

@ -86,10 +86,9 @@ class AdminImageFilterAction(tables.FilterAction):
class AdminImagesTable(project_tables.ImagesTable):
name = tables.Column("name",
link="horizon:admin:images:detail",
truncate=40,
verbose_name=_("Image Name"))
name = tables.WrappingColumn("name",
link="horizon:admin:images:detail",
verbose_name=_("Image Name"))
tenant = tables.Column(lambda obj: getattr(obj, 'tenant_name', obj.owner),
verbose_name=_("Project"))

View File

@ -135,9 +135,9 @@ class AdminInstancesTable(tables.DataTable):
host = tables.Column("OS-EXT-SRV-ATTR:host",
verbose_name=_("Host"),
classes=('nowrap-col',))
name = tables.Column("name",
link="horizon:admin:instances:detail",
verbose_name=_("Name"))
name = tables.WrappingColumn("name",
link="horizon:admin:instances:detail",
verbose_name=_("Name"))
image_name = tables.Column("image_name",
verbose_name=_("Image Name"))
ip = tables.Column(project_tables.get_ips,

View File

@ -80,9 +80,9 @@ class UpdatePort(project_tables.UpdatePort):
class PortsTable(project_tables.PortsTable):
name = tables.Column("name_or_id",
verbose_name=_("Name"),
link="horizon:admin:networks:ports:detail")
name = tables.WrappingColumn("name_or_id",
verbose_name=_("Name"),
link="horizon:admin:networks:ports:detail")
class Meta(object):
name = "ports"

View File

@ -101,8 +101,8 @@ def subnet_ip_availability(availability):
class SubnetsTable(tables.DataTable):
name = tables.Column("name_or_id", verbose_name=_("Name"),
link='horizon:admin:networks:subnets:detail')
name = tables.WrappingColumn("name_or_id", verbose_name=_("Name"),
link='horizon:admin:networks:subnets:detail')
cidr = tables.Column("cidr", verbose_name=_("CIDR"))
ip_version = tables.Column("ipver_str", verbose_name=_("IP Version"))
gateway_ip = tables.Column("gateway_ip", verbose_name=_("Gateway IP"))

View File

@ -86,8 +86,8 @@ DISPLAY_CHOICES = (
class NetworksTable(tables.DataTable):
tenant = tables.Column("tenant_name", verbose_name=_("Project"))
name = tables.Column("name_or_id", verbose_name=_("Network Name"),
link='horizon:admin:networks:detail')
name = tables.WrappingColumn("name_or_id", verbose_name=_("Network Name"),
link='horizon:admin:networks:detail')
subnets = tables.Column(project_tables.get_subnets,
verbose_name=_("Subnets Associated"),)
num_agents = tables.Column("num_agents",

View File

@ -37,9 +37,9 @@ class UpdateRow(tables.Row):
class RoutersTable(r_tables.RoutersTable):
tenant = tables.Column("tenant_name", verbose_name=_("Project"))
name = tables.Column("name",
verbose_name=_("Name"),
link="horizon:admin:routers:detail")
name = tables.WrappingColumn("name",
verbose_name=_("Name"),
link="horizon:admin:routers:detail")
class Meta(object):
name = "routers"

View File

@ -56,8 +56,8 @@ class UpdateRow(tables.Row):
class VolumeSnapshotsTable(volumes_tables.VolumesTableBase):
name = tables.Column("name", verbose_name=_("Name"),
link="horizon:admin:volumes:snapshots:detail")
name = tables.WrappingColumn("name", verbose_name=_("Name"),
link="horizon:admin:volumes:snapshots:detail")
volume_name = snapshots_tables.SnapshotVolumeNameColumn(
"name", verbose_name=_("Volume Name"),
link="horizon:admin:volumes:volumes:detail")

View File

@ -196,8 +196,8 @@ class UpdateRow(tables.Row):
class VolumeTypesTable(tables.DataTable):
name = tables.Column("name", verbose_name=_("Name"),
form_field=forms.CharField(max_length=64))
name = tables.WrappingColumn("name", verbose_name=_("Name"),
form_field=forms.CharField(max_length=64))
description = tables.Column(lambda obj: getattr(obj, 'description', None),
verbose_name=_('Description'),
form_field=forms.CharField(
@ -294,7 +294,7 @@ class EditConsumer(tables.LinkAction):
class QosSpecsTable(tables.DataTable):
name = tables.Column('name', verbose_name=_('Name'))
name = tables.WrappingColumn('name', verbose_name=_('Name'))
consumer = tables.Column('consumer', verbose_name=_('Consumer'))
specs = tables.Column(render_spec_keys,
verbose_name=_('Specs'),

View File

@ -87,9 +87,9 @@ class UpdateVolumeStatusAction(tables.LinkAction):
class VolumesTable(volumes_tables.VolumesTable):
name = tables.Column("name",
verbose_name=_("Name"),
link="horizon:admin:volumes:volumes:detail")
name = tables.WrappingColumn("name",
verbose_name=_("Name"),
link="horizon:admin:volumes:volumes:detail")
host = tables.Column("os-vol-host-attr:host", verbose_name=_("Host"))
tenant = tables.Column(lambda obj: getattr(obj, 'tenant_name', None),
verbose_name=_("Project"))

View File

@ -275,7 +275,7 @@ class UnsetDomainContext(tables.Action):
class DomainsTable(tables.DataTable):
name = tables.Column('name', verbose_name=_('Name'))
name = tables.WrappingColumn('name', verbose_name=_('Name'))
description = tables.Column(lambda obj: getattr(obj, 'description', None),
verbose_name=_('Description'))
id = tables.Column('id', verbose_name=_('Domain ID'))

View File

@ -188,7 +188,7 @@ class AddMembersLink(tables.LinkAction):
class UsersTable(tables.DataTable):
name = tables.Column('name', verbose_name=_('User Name'))
name = tables.WrappingColumn('name', verbose_name=_('User Name'))
email = tables.Column('email', verbose_name=_('Email'),
filters=[defaultfilters.escape,
defaultfilters.urlize])

View File

@ -221,9 +221,9 @@ class UpdateRow(tables.Row):
class TenantsTable(tables.DataTable):
name = tables.Column('name', verbose_name=_('Name'),
link=("horizon:identity:projects:detail"),
form_field=forms.CharField(max_length=64))
name = tables.WrappingColumn('name', verbose_name=_('Name'),
link=("horizon:identity:projects:detail"),
form_field=forms.CharField(max_length=64))
description = tables.Column(lambda obj: getattr(obj, 'description', None),
verbose_name=_('Description'),
form_field=forms.CharField(

View File

@ -78,7 +78,7 @@ class RoleFilterAction(tables.FilterAction):
class RolesTable(tables.DataTable):
name = tables.Column('name', verbose_name=_('Role Name'))
name = tables.WrappingColumn('name', verbose_name=_('Role Name'))
id = tables.Column('id', verbose_name=_('Role ID'))
class Meta(object):

View File

@ -178,10 +178,10 @@ class UsersTable(tables.DataTable):
("true", True),
("false", False)
)
name = tables.Column('name',
link="horizon:identity:users:detail",
verbose_name=_('User Name'),
form_field=forms.CharField(required=False))
name = tables.WrappingColumn('name',
link="horizon:identity:users:detail",
verbose_name=_('User Name'),
form_field=forms.CharField(required=False))
description = tables.Column(lambda obj: getattr(obj, 'description', None),
verbose_name=_('Description'),
hidden=KEYSTONE_V2_ENABLED,

View File

@ -300,10 +300,9 @@ class ImagesTable(tables.DataTable):
("image", pgettext_lazy("Type of an image", u"Image")),
("snapshot", pgettext_lazy("Type of an image", u"Snapshot")),
)
name = tables.Column(get_image_name,
link="horizon:project:images:images:detail",
truncate=40,
verbose_name=_("Image Name"),)
name = tables.WrappingColumn(get_image_name,
link="horizon:project:images:images:detail",
verbose_name=_("Image Name"),)
image_type = tables.Column(get_image_type,
verbose_name=_("Type"),
display_choices=TYPE_CHOICES)

View File

@ -1195,9 +1195,9 @@ class InstancesTable(tables.DataTable):
("shelved", True),
("shelved_offloaded", True),
)
name = tables.Column("name",
link="horizon:project:instances:detail",
verbose_name=_("Instance Name"))
name = tables.WrappingColumn("name",
link="horizon:project:instances:detail",
verbose_name=_("Instance Name"))
image_name = tables.Column("image_name",
verbose_name=_("Image Name"))
ip = tables.Column(get_ips,

View File

@ -65,9 +65,9 @@ STATUS_DISPLAY_CHOICES = (
class PortsTable(tables.DataTable):
name = tables.Column("name_or_id",
verbose_name=_("Name"),
link="horizon:project:networks:ports:detail")
name = tables.WrappingColumn("name_or_id",
verbose_name=_("Name"),
link="horizon:project:networks:ports:detail")
fixed_ips = tables.Column(get_fixed_ips, verbose_name=_("Fixed IPs"))
attached = tables.Column(get_attached, verbose_name=_("Attached Device"))
status = tables.Column("status",

View File

@ -128,8 +128,10 @@ class UpdateSubnet(SubnetPolicyTargetMixin, CheckNetworkEditable,
class SubnetsTable(tables.DataTable):
name = tables.Column("name_or_id", verbose_name=_("Name"),
link='horizon:project:networks:subnets:detail')
name = tables.WrappingColumn(
"name_or_id",
verbose_name=_("Name"),
link='horizon:project:networks:subnets:detail')
cidr = tables.Column("cidr", verbose_name=_("Network Address"))
ip_version = tables.Column("ipver_str", verbose_name=_("IP Version"))
gateway_ip = tables.Column("gateway_ip", verbose_name=_("Gateway IP"))

View File

@ -168,9 +168,9 @@ class NetworksFilterAction(tables.FilterAction):
class NetworksTable(tables.DataTable):
name = tables.Column("name_or_id",
verbose_name=_("Name"),
link='horizon:project:networks:detail')
name = tables.WrappingColumn("name_or_id",
verbose_name=_("Name"),
link='horizon:project:networks:detail')
subnets = tables.Column(get_subnets,
verbose_name=_("Subnets Associated"),)
shared = tables.Column("shared", verbose_name=_("Shared"),

View File

@ -209,9 +209,9 @@ ADMIN_STATE_DISPLAY_CHOICES = (
class RoutersTable(tables.DataTable):
name = tables.Column("name",
verbose_name=_("Name"),
link="horizon:project:routers:detail")
name = tables.WrappingColumn("name",
verbose_name=_("Name"),
link="horizon:project:routers:detail")
status = tables.Column("status",
verbose_name=_("Status"),
status=True,

View File

@ -50,7 +50,7 @@ limitations under the License.
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ ::expandDetailsText $}"></span>
</td>
<td class="rsp-p1">{$ ::item.name $}</td>
<td class="rsp-p1 word-break">{$ ::item.name $}</td>
<td class="rsp-p1">
<span class="invalid fa fa-exclamation-triangle"
ng-show="item.errors.vcpus"
@ -161,7 +161,7 @@ limitations under the License.
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ ::expandDetailsText $}"></span>
</td>
<td class="rsp-p1">{$ ::item.name $}</td>
<td class="rsp-p1 word-break">{$ ::item.name $}</td>
<td class="rsp-p1">
<span class="invalid fa fa-exclamation-triangle"
ng-show="item.errors.vcpus"

View File

@ -45,7 +45,7 @@
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ 'Click to see more details'|translate $}"></span>
</td>
<td class="rsp-p1">{$ row.name $}</td>
<td class="rsp-p1 word-break">{$ row.name $}</td>
<td class="rsp-p2">
<div ng-repeat="subnet in row.subnets">{$ subnet.name $}</div>
</td>
@ -120,7 +120,7 @@
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ 'Click to see more details'|translate $}"></span>
</td>
<td class="rsp-p1">{$ row.name$}</td>
<td class="rsp-p1 word-break">{$ row.name$}</td>
<td class="rsp-p2">
<div ng-repeat="subnet in row.subnets">{$ subnet.name $}</div>
</td>

View File

@ -32,12 +32,12 @@
<td class="reorder">
<span class="fa fa-sort" title="{$ 'Re-order items using drag and drop'|translate $}"></span>
{$ $index + 1 $}
</td>
</td>
<td class="expander">
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ 'Click to see more details'|translate $}"></span>
</td>
<td class="rsp-p1">{$ ctrl.nameOrID(item) $}</td>
<td class="rsp-p1 word-break">{$ ctrl.nameOrID(item) $}</td>
<td class="rsp-p2">
<div ng-repeat="ip in item.fixed_ips">
{$ ip.ip_address $} on subnet: {$ item.subnet_names[ip.ip_address] $}
@ -103,14 +103,14 @@
No available items
</div>
</td>
</tr>
</tr>
<tr ng-repeat-start="item in ctrl.tableDataMulti.displayedAvailable track by item.id"
ng-if="!trCtrl.allocatedIds[item.id]">
<td class="expander">
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ 'Click to see more details'|translate $}"></span>
</td>
<td class="rsp-p1">{$ ctrl.nameOrID(item) $}</td>
<td class="rsp-p1 word-break">{$ ctrl.nameOrID(item) $}</td>
<td class="rsp-p2">
<div ng-repeat="ip in item.fixed_ips">
{$ ip.ip_address $} on subnet: {$ item.subnet_names[ip.ip_address] $}

View File

@ -31,7 +31,7 @@
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ ::trCtrl.helpText.expandDetailsText $}"></span>
</td>
<td class="rsp-p1">{$ row.name $}</td>
<td class="rsp-p1 word-break">{$ row.name $}</td>
<td class="rsp-p2">{$ row.description $}</td>
<td class="actions_column">
<action-list>
@ -81,7 +81,7 @@
<span class="fa fa-chevron-right" hz-expand-detail
title="{$ ::trCtrl.helpText.expandDetailsText $}"></span>
</td>
<td class="rsp-p1">{$ row.name$}</td>
<td class="rsp-p1 word-break">{$ row.name$}</td>
<td class="rsp-p1">{$ row.description $}</td>
<td class="actions_column">
<action-list>

View File

@ -170,28 +170,28 @@
// Mapping for dynamic table data
var tableBodyCellsMap = {
image: [
{ key: 'name', classList: ['hi-light'] },
{ key: 'name', classList: ['hi-light', 'word-break'] },
{ key: 'updated_at', filter: dateFilter, filterArg: 'short' },
{ key: 'size', filter: bytesFilter, classList: ['number'] },
{ key: 'disk_format', filter: diskFormatFilter, filterRawData: true },
{ key: 'is_public', filter: decodeFilter, filterArg: _visibilitymap }
],
snapshot: [
{ key: 'name', classList: ['hi-light'] },
{ key: 'name', classList: ['hi-light', 'word-break'] },
{ key: 'updated_at', filter: dateFilter, filterArg: 'short' },
{ key: 'size', filter: bytesFilter, classList: ['number'] },
{ key: 'disk_format', filter: diskFormatFilter, filterRawData: true },
{ key: 'is_public', filter: decodeFilter, filterArg: _visibilitymap }
],
volume: [
{ key: 'name', classList: ['hi-light'] },
{ key: 'name', classList: ['hi-light', 'word-break'] },
{ key: 'description' },
{ key: 'size', filter: gbFilter, classList: ['number'] },
{ key: 'volume_image_metadata', filter: diskFormatFilter },
{ key: 'availability_zone' }
],
volume_snapshot: [
{ key: 'name', classList: ['hi-light'] },
{ key: 'name', classList: ['hi-light', 'word-break'] },
{ key: 'description' },
{ key: 'size', filter: gbFilter, classList: ['number'] },
{ key: 'created_at', filter: dateFilter, filterArg: 'short' },

View File

@ -140,9 +140,9 @@ class VolumeCGroupsTable(tables.DataTable):
pgettext_lazy("Current status of Consistency Group", u"Error")),
)
name = tables.Column("name",
verbose_name=_("Name"),
link="horizon:project:volumes:cgroups:detail")
name = tables.WrappingColumn("name",
verbose_name=_("Name"),
link="horizon:project:volumes:cgroups:detail")
description = tables.Column("description",
verbose_name=_("Description"),
truncate=40)

View File

@ -141,7 +141,7 @@ class UpdateRow(tables.Row):
return snapshot
class SnapshotVolumeNameColumn(tables.Column):
class SnapshotVolumeNameColumn(tables.WrappingColumn):
def get_raw_data(self, snapshot):
volume = snapshot._volume
if volume:
@ -168,9 +168,10 @@ class VolumeSnapshotsFilterAction(tables.FilterAction):
class VolumeSnapshotsTable(volume_tables.VolumesTableBase):
name = tables.Column("name",
verbose_name=_("Name"),
link="horizon:project:volumes:snapshots:detail")
name = tables.WrappingColumn(
"name",
verbose_name=_("Name"),
link="horizon:project:volumes:snapshots:detail")
volume_name = SnapshotVolumeNameColumn(
"name",
verbose_name=_("Volume Name"),

View File

@ -364,7 +364,7 @@ def get_attachment_name(request, attachment):
return instance
class AttachmentColumn(tables.Column):
class AttachmentColumn(tables.WrappingColumn):
"""Customized column class.
So it that does complex processing on the attachments
@ -470,9 +470,9 @@ class VolumesFilterAction(tables.FilterAction):
class VolumesTable(VolumesTableBase):
name = tables.Column("name",
verbose_name=_("Name"),
link="horizon:project:volumes:volumes:detail")
name = tables.WrappingColumn("name",
verbose_name=_("Name"),
link="horizon:project:volumes:volumes:detail")
volume_type = tables.Column(get_volume_type,
verbose_name=_("Type"))
attachments = AttachmentColumn("attachments",
@ -543,7 +543,7 @@ class DetachVolume(tables.BatchAction):
return reverse('horizon:project:volumes:index')
class AttachedInstanceColumn(tables.Column):
class AttachedInstanceColumn(tables.WrappingColumn):
"""Customized column class that does complex processing on the attachments
for a volume instance.
"""

View File

@ -153,3 +153,12 @@
.nowrap-col {
white-space: nowrap;
}
// Make sure long UUIDs, names etc. don't force the table outside a
// modal, by forcing a word break. The 'td' specificity is required
// because the class is applied in a column, but we don't want the
// <th> to wrap.
// See launchpad bugs 1565724 && 1584785
td.word-break {
word-break: break-all;
}

View File

@ -15,10 +15,4 @@
.magic-search-bar, .basic-search-bar {
margin: $padding-small-vertical 0;
}
// Make sure long UUIDs don't force the table outside the modal
// See https://bugs.launchpad.net/horizon/+bug/1565724
td {
word-break: break-all;
}
}