ebcli/display/table.py (206 lines of code) (raw):
# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from ebcli. core import io
from ebcli.display import term
from cement.utils.misc import minimal_logger
LOG = minimal_logger(__name__)
class Table(object):
def __init__(self, name, columns=None, screen=None):
self.name = name
self.columns = columns or []
self.visible = True
self.data = None
self.width = None
self.screen = screen
self.first_column = 0
self.vertical_offset = 0
self.visible_rows = 0
self.header_size = 2
self.shift_col = 0
def set_shift_col(self, offset):
self.shift_col = offset
def draw(self, num_of_rows, table_data):
self.width = term.width()
if not self.visible:
return
self.set_data(table_data)
self.visible_rows = num_of_rows
if self.vertical_offset > self.get_max_offset():
self.vertical_offset = self.get_max_offset()
self.first_column = min(self.screen.horizontal_offset + 1,
len(self.columns) - 1)
self.draw_header_row()
self.draw_rows()
def set_data(self, table_data):
self.data = table_data
HEADER_SPACE_NEEDED = 16
HEADER_WIDTH = 11
MAX_DESCRIPTION = 100
def draw_header_row(self):
t = term.get_terminal()
labels = [' ']
width = self.width
for c in [0] + list(range(self.first_column, len(self.columns))):
column = self.columns[c]
column_size = column.size
if column_size is None:
column_size = self.get_widest_data_length_in_column(self.columns[c]) + 2
# special case for Description column this should be the same for all
# description columns, allows very large descriptions that we are able
# to scroll through.
if column.name == 'Description' and column_size > self.MAX_DESCRIPTION:
column_size = self.MAX_DESCRIPTION
column.fit_size = column_size
header = justify_and_trim(column.name, column_size, column.justify)
if (self.screen.sort_index and
self.screen.sort_index[1] == c and # Sort column
self.name == self.screen.sort_index[0] and # sort table
len(' '.join(labels)) < width): # Column is on screen
format_string = '{n}{b}{u}{data}{n}{r}'
header = format_string.replace('{data}', header)
width += len(format_string) - 6
labels.append(header)
header_text = justify_and_trim(' '.join(labels), width, 'left')
# header title
if header_text[-Table.HEADER_SPACE_NEEDED:].isspace():
header_text = (header_text[:-Table.HEADER_SPACE_NEEDED] + ' {n}{b} ' +
justify_and_trim(self.name, Table.HEADER_WIDTH, 'right') + ' {r} ')
header_text = header_text.format(n=t.normal, b=t.bold, u=term.underline(),
r=term.reverse_())
header_text += t.normal
term.echo_line(term.reverse_colors(header_text))
def draw_rows(self):
first_row_index = self.first_row_index()
last_row_index = self.last_row_index()
for r in range(first_row_index, last_row_index):
row_data = self.get_row_data(self.data[r])
term.echo_line(' '.join(row_data))
self.draw_info_line(first_row_index, last_row_index)
def draw_info_line(self, first_row_index, last_row_index):
line = u' '
if last_row_index < len(self.data):
# Show down arrow
line += u' {}'.format(term.DOWN_ARROW)
else:
line += u' '
if first_row_index != 0:
# Show up arrow
line += u' {}'.format(term.UP_ARROW)
term.echo_line(line)
def first_row_index(self):
return self.vertical_offset
def last_row_index(self):
return min(len(self.data),
self.visible_rows + self.vertical_offset)
def get_row_data(self, data):
row_data = [
self.get_color_data(data)
]
for c in [0] + list(range(self.first_column, len(self.columns))):
column = self.columns[c]
if column.key == 'Description' and self.shift_col > 0:
c_data = self.shift_description_data(data, column)
else:
c_data = self.get_column_data(data, column)
row_data.append(
c_data
)
return row_data
def get_column_data(self, data, column):
c_data = justify_and_trim(
self.ascii_string(data.get(column.key, '-')),
column.size or column.fit_size,
column.justify,
column.key, self.shift_col)
return c_data
def shift_description_data(self, data, column):
c_data = justify_and_trim(
str(data.get(column.key, '-')[(self.shift_col):]),
column.size or column.fit_size,
column.justify,
column.key, self.shift_col)
return c_data
def get_widest_data_length_in_column(self, column):
max_size = len(str(column.name))
for r in range(self.first_row_index(), self.last_row_index()):
column_key = self.ascii_string(self.data[r].get(column.key))
len_row_data = self.ascii_length(column_key)
if len_row_data > max_size:
max_size = len_row_data
return max_size
def ascii_string(self, data):
try:
return str(data)
except UnicodeEncodeError:
return data
def ascii_length(self, data):
try:
return len(str(data))
except UnicodeEncodeError:
return int(len(data) * 1.5)
def get_color_data(self, data):
if self.screen.mono:
return data.get('Color', ' ')[:1]
else:
return io.on_color(data.get('Color', 'RESET'), ' ')
def scroll_down(self, reverse=False):
if reverse and (self.vertical_offset > 0):
last_id = self.get_row_id(self.first_row_index())
self.vertical_offset -= 1
new_id = self.get_row_id(self.first_row_index())
elif not reverse and self.vertical_offset < self.get_max_offset():
last_id = self.get_row_id(self.last_row_index() - 1)
self.vertical_offset += 1
new_id = self.get_row_id(self.last_row_index() - 1)
else:
return None
if new_id != last_id: # Return id if became visible
return new_id
else:
return None
def scroll_to_id(self, id, reverse=False):
if id in self.get_visible_row_ids():
return
new_id = None
while new_id != id:
new_id = self.scroll_down(reverse)
if new_id is None:
return
def get_max_offset(self):
return max(len(self.data) - self.visible_rows, 0)
def scroll_to_end(self):
self.vertical_offset = self.get_max_offset()
def scroll_to_beginning(self):
self.vertical_offset = 0
def get_row_id(self, row_index):
try:
row = self.data[row_index]
except IndexError:
raise IndexError('Can not access index:{}'.format(row_index))
return row.get('InstanceId')
def get_visible_row_ids(self):
ids = set()
for r in range(self.first_row_index(), self.last_row_index()):
ids.add(self.data[r].get('InstanceId', ''))
return ids
def justify_and_trim(string, justify_size, justify_side, key=None, shift_col=0):
if justify_side == 'right':
s = string.rjust(justify_size)
else:
s = string.ljust(justify_size)
# Special case where 'Description' column needs trimming in versions table
if key is not None and key == 'Description':
if len(s) > justify_size:
s = _add_arrow(s, justify_size - 1, u'\u25B6', '>')
if shift_col > 0:
s = _add_arrow(s, 0, u'\u25C0', '<', s[1:])
if justify_side == 'none':
return s
return s[:justify_size]
def _add_arrow(string, index, arrow1, arrow2, rest_of_string=''):
try:
s = string[:index] + arrow1 + rest_of_string
except UnicodeError:
LOG.debug("Bad unicode translation.")
s = string[:index] + arrow2
pass
return s
class Column(object):
def __init__(self, name=None, size=6, key=None, justify=None, sort_key=None):
self.name = name
self.size = size
self.fit_size = size
self.key = key
self.justify = justify
self.sort_key = sort_key or self.key