Adds a tabular view of the topology data

The traditional network engineer workflow includes a diagram, a
spreadsheet, and the CLI.  This adds an experimental view of the
network topology data in a spreadsheet like table view.

* Adds angular-xeditable dependency for tables view.
* Add data binding models
* Add message transformations from table to topology formats
* Adding dependencies for tables view
This commit is contained in:
Ben Thomasson 2018-03-15 11:17:48 -04:00
parent d0e402c39a
commit 8fb54efa8e
No known key found for this signature in database
GPG Key ID: 5818EF4CC895D5F5
17 changed files with 649 additions and 33 deletions

View File

@ -18,6 +18,12 @@ from awx.network_ui.models import Group
from awx.network_ui.models import GroupDevice
from awx.network_ui.models import DataBinding
from awx.network_ui.models import DataType
from awx.network_ui.models import DataSheet
class DeviceAdmin(admin.ModelAdmin):
fields = ('topology', 'name', 'x', 'y', 'id', 'type', 'interface_id_seq',)
@ -37,7 +43,7 @@ admin.site.register(Link, LinkAdmin)
class TopologyAdmin(admin.ModelAdmin):
fields = ('name', 'scale', 'panX', 'panY', 'device_id_seq', 'link_id_seq', 'group_id_seq',)
raw_id_fields = ('device_id_seq',)
raw_id_fields = ()
admin.site.register(Topology, TopologyAdmin)
@ -77,7 +83,7 @@ admin.site.register(Interface, InterfaceAdmin)
class GroupAdmin(admin.ModelAdmin):
fields = ('id', 'name', 'x1', 'y1', 'x2', 'y2', 'topology',)
raw_id_fields = ('id', 'y1', 'x2', 'topology',)
raw_id_fields = ('topology',)
admin.site.register(Group, GroupAdmin)
@ -89,3 +95,27 @@ class GroupDeviceAdmin(admin.ModelAdmin):
admin.site.register(GroupDevice, GroupDeviceAdmin)
class DataBindingAdmin(admin.ModelAdmin):
fields = ('column', 'row', 'table', 'primary_key_id', 'field', 'data_type', 'sheet',)
raw_id_fields = ('data_type', 'sheet',)
admin.site.register(DataBinding, DataBindingAdmin)
class DataTypeAdmin(admin.ModelAdmin):
fields = ('type_name',)
raw_id_fields = ()
admin.site.register(DataType, DataTypeAdmin)
class DataSheetAdmin(admin.ModelAdmin):
fields = ('name', 'topology', 'client',)
raw_id_fields = ('topology', 'client',)
admin.site.register(DataSheet, DataSheetAdmin)

View File

@ -4,6 +4,7 @@ from channels.sessions import channel_session
from awx.network_ui.models import Topology, Device, Link, Client, TopologyHistory, MessageType, Interface
from awx.network_ui.models import Group as DeviceGroup
from awx.network_ui.models import GroupDevice as GroupDeviceMap
from awx.network_ui.models import DataSheet, DataBinding, DataType
from awx.network_ui.serializers import yaml_serialize_topology
import urlparse
from django.db.models import Q
@ -270,7 +271,7 @@ class _Persistence(object):
if handler is not None:
handler(message_value, topology_id, client_id)
else:
print "Unsupported message ", message_type
logger.warning("Unsupported message %s", message_type)
def get_handler(self, message_type):
return getattr(self, "on{0}".format(message_type), None)
@ -298,6 +299,23 @@ class _Persistence(object):
def onDeviceLabelEdit(self, device, topology_id, client_id):
Device.objects.filter(topology_id=topology_id, id=device['id']).update(name=device['name'])
for pk in Device.objects.filter(topology_id=topology_id, id=device['id']).values_list('pk', flat=True):
for db in DataBinding.objects.filter(primary_key_id=pk,
table="Device",
field="name").values('sheet__client_id',
'sheet__name',
'column',
'row'):
message = ['TableCellEdit', dict(sender=0,
msg_type="TableCellEdit",
sheet=db['sheet__name'],
col=db['column'] + 1,
row=db['row'] + 2,
new_value=device['name'],
old_value=device['previous_name'])]
logger.info("Sending message %r", message)
Group("topology-%s-client-%s" % (topology_id, db['sheet__client_id'])).send({"text": json.dumps(message)})
def onInterfaceLabelEdit(self, interface, topology_id, client_id):
(Interface.objects
@ -376,7 +394,7 @@ class _Persistence(object):
if handler is not None:
handler(message, topology_id, client_id)
else:
print "Unsupported message ", message['msg_type']
logger.warning("Unsupported message %s", message['msg_type'])
def onDeploy(self, message_value, topology_id, client_id):
DeviceGroup("workers").send({"text": json.dumps(["Deploy", topology_id, yaml_serialize_topology(topology_id)])})
@ -480,7 +498,7 @@ class _UndoPersistence(object):
if handler is not None:
handler(message_value, topology_id, client_id)
else:
print "Unsupported undo message ", message_type
logger.warnding("Unsupported undo message %s", message_type)
def onSnapshot(self, snapshot, topology_id, client_id):
pass
@ -541,7 +559,7 @@ class _RedoPersistence(object):
if handler is not None:
handler(message_value, topology_id, client_id)
else:
print "Unsupported redo message ", message_type
logger.warning("Unsupported redo message %s", message_type)
def onDeviceSelected(self, message_value, topology_id, client_id):
'Ignore DeviceSelected messages'
@ -574,7 +592,7 @@ class _Discovery(object):
if handler is not None:
handler(message_value, topology_id)
else:
print "Unsupported message ", message_type
logger.warning("Unsupported message %s", message_type)
def get_handler(self, message_type):
return getattr(self, "on{0}".format(message_type), None)
@ -851,3 +869,174 @@ def tester_message(message):
@channel_session
def tester_disconnect(message):
pass
# Tables UI channel events
def make_sheet(data, column_headers=[]):
sheet = []
n_columns = max([len(x) for x in data]) - 1
row_i = 0
sheet.append([dict(value=x, editable=False) for x in list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")[0:n_columns]])
row_i += 1
if column_headers:
sheet.append([dict(value=row_i, editable=False)] + [dict(value=x, editable=False) for x in column_headers])
row_i += 1
for row in data:
sheet_row = [dict(value=row_i, editable=False)]
row_i += 1
sheet_row.extend([dict(value=x, editable=True, col=i, row=row_i) for i, x in enumerate(row[1:])])
sheet.append(sheet_row)
return sheet
def make_bindings(sheet_id, klass, filter_q, values_list, order_by):
values_list = ['pk'] + values_list
data = list(klass.objects.filter(**filter_q).values_list(*values_list).order_by(*order_by))
data_types = set()
for row in data:
for cell in row:
data_types.add(type(cell).__name__)
data_type_map = dict()
logger.info(repr(data_types))
for dt in list(data_types):
data_type_map[dt] = DataType.objects.get_or_create(type_name=dt)[0].pk
logger.info(repr(data_type_map))
bindings = []
for row_i, row in enumerate(data):
pk = row[0]
for col_i, cell in enumerate(row[1:]):
field = values_list[col_i + 1]
if '__' in field:
continue
logger.info("make_bindings %s %s %s %s %s %s %s", sheet_id, klass.__name__, pk, col_i, row_i, field, data_type_map[type(cell).__name__])
bindings.append(DataBinding.objects.get_or_create(sheet_id=sheet_id,
column=col_i,
row=row_i,
table=klass.__name__,
primary_key_id=pk,
field=field,
data_type_id=data_type_map[type(cell).__name__])[0])
return data
@channel_session
def tables_connect(message):
data = urlparse.parse_qs(message.content['query_string'])
topology_id = parse_topology_id(data)
message.channel_session['topology_id'] = topology_id
client = Client()
client.save()
Group("topology-%s-client-%s" % (topology_id, client.pk)).add(message.reply_channel)
message.channel_session['client_id'] = client.pk
message.reply_channel.send({"text": json.dumps(["id", client.pk])})
message.reply_channel.send({"text": json.dumps(["topology_id", topology_id])})
device_sheet, _ = DataSheet.objects.get_or_create(topology_id=topology_id, client_id=client.pk, name="Devices")
data = make_bindings(device_sheet.pk, Device, dict(topology_id=topology_id), ['name'], ['name'])
message.reply_channel.send({"text": json.dumps(["sheet", dict(name="Devices", data=make_sheet(data, ['Device Name']))])})
interface_sheet, _ = DataSheet.objects.get_or_create(topology_id=topology_id, client_id=client.pk, name="Interfaces")
data = make_bindings(interface_sheet.pk, Interface, dict(device__topology_id=topology_id), ['device__name', 'name'], ['device__name', 'name'])
message.reply_channel.send({"text": json.dumps(["sheet", dict(name="Interfaces", data=make_sheet(data, ['Device Name', 'Interface Name']))])})
group_sheet, _ = DataSheet.objects.get_or_create(topology_id=topology_id, client_id=client.pk, name="Groups")
data = make_bindings(group_sheet.pk, DeviceGroup, dict(topology_id=topology_id), ['name'], ['name'])
message.reply_channel.send({"text": json.dumps(["sheet", dict(name="Groups", data=make_sheet(data, ['Group Name']))])})
def device_label_edit(o):
d = transform_dict(dict(name='name',
id='id',
old_value='previous_name'), o.__dict__)
d['msg_type'] = 'DeviceLabelEdit'
return ['DeviceLabelEdit', d]
def group_label_edit(o):
d = transform_dict(dict(name='name',
id='id',
old_value='previous_name'), o.__dict__)
d['msg_type'] = 'GroupLabelEdit'
return ['GroupLabelEdit', d]
def interface_label_edit(o):
d = o.__dict__
d['device_id'] = o.device.id
d = transform_dict(dict(name='name',
id='id',
device_id='device_id',
old_value='previous_name'), o.__dict__)
d['msg_type'] = 'InterfaceLabelEdit'
return ['InterfaceLabelEdit', d]
@channel_session
def tables_message(message):
data = json.loads(message['text'])
logger.info(data[0])
logger.info(data[1])
data_type_mapping = {'unicode': unicode,
'int': int}
table_mapping = {'Device': Device,
'Interface': Interface,
'Group': DeviceGroup}
transformation_mapping = {('Device', 'name'): device_label_edit,
('Interface', 'name'): interface_label_edit,
('Group', 'name'): group_label_edit}
if data[0] == "TableCellEdit":
topology_id = message.channel_session['topology_id']
group_channel = Group("topology-%s" % topology_id)
client_id = message.channel_session['client_id']
data_sheet = DataSheet.objects.get(topology_id=topology_id, client_id=client_id, name=data[1]['sheet']).pk
logger.info("DataSheet %s", data_sheet)
data_bindings = DataBinding.objects.filter(sheet_id=data_sheet,
column=data[1]['col'] - 1,
row=data[1]['row'] - 2)
logger.info("Found %s bindings", data_bindings.count())
logger.info(repr(data_bindings.values('table', 'data_type__type_name', 'field', 'primary_key_id')))
for table, data_type, field, pk in data_bindings.values_list('table', 'data_type__type_name', 'field', 'primary_key_id'):
new_value = data_type_mapping[data_type](data[1]['new_value'])
old_value = data_type_mapping[data_type](data[1]['old_value'])
logger.info("Updating %s", table_mapping[table].objects.filter(pk=pk).values())
table_mapping[table].objects.filter(pk=pk).update(**{field: new_value})
logger.info("Updated %s", table_mapping[table].objects.filter(pk=pk).count())
for o in table_mapping[table].objects.filter(pk=pk):
o.old_value = old_value
message = transformation_mapping[(table, field)](o)
message[1]['sender'] = 0
logger.info("Sending %r", message)
group_channel.send({"text": json.dumps(message)})
@channel_session
def tables_disconnect(message):
pass

View File

@ -76,8 +76,6 @@ models:
type: FloatField
- default: 0
name: device_id_seq
ref: Topology
ref_field: device_id_seq
type: IntegerField
- default: 0
name: link_id_seq
@ -154,8 +152,6 @@ models:
pk: true
type: AutoField
- name: id
ref: Group
ref_field: id
type: IntegerField
- len: 200
name: name
@ -163,12 +159,8 @@ models:
- name: x1
type: IntegerField
- name: y1
ref: Group
ref_field: y1
type: IntegerField
- name: x2
ref: Group
ref_field: x2
type: IntegerField
- name: y2
type: IntegerField
@ -177,23 +169,78 @@ models:
ref_field: topology_id
type: ForeignKey
name: Group
x: 907
y: 520
x: 407
y: -379
- fields:
- name: group_device_id
pk: true
type: AutoField
- name: group
ref: GroupDevice
ref_field: group
ref: Group
ref_field: group_id
type: ForeignKey
- name: device
ref: Device
ref_field: device_id
type: ForeignKey
name: GroupDevice
x: 631
y: 561
x: 739
y: -234
- fields:
- name: data_binding_id
pk: true
type: AutoField
- name: column
type: IntegerField
- name: row
type: IntegerField
- len: 200
name: table
type: CharField
- name: primary_key_id
type: IntegerField
- len: 200
name: field
type: CharField
- name: data_type
ref: DataType
ref_field: data_type_id
type: ForeignKey
- name: sheet
ref: DataSheet
ref_field: data_sheet_id
type: ForeignKey
name: DataBinding
x: -515
y: -370
- fields:
- name: data_type_id
pk: true
type: AutoField
- len: 200
name: type_name
type: CharField
name: DataType
x: -782
y: -172
- fields:
- name: data_sheet_id
pk: true
type: AutoField
- len: 200
name: name
type: CharField
- name: topology
ref: Topology
ref_field: topology_id
type: ForeignKey
- name: client
ref: Client
ref_field: client_id
type: ForeignKey
name: DataSheet
x: -207
y: -282
modules: []
view:
panX: 213.72955551921206

View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('network_ui', '0015_auto_20170710_1937'),
]
operations = [
migrations.CreateModel(
name='DataBinding',
fields=[
('data_binding_id', models.AutoField(serialize=False, verbose_name=b'DataBinding', primary_key=True)),
('column', models.IntegerField()),
('row', models.IntegerField()),
('table', models.CharField(max_length=200)),
('primary_key_id', models.IntegerField()),
('field', models.CharField(max_length=200)),
],
),
migrations.CreateModel(
name='DataSheet',
fields=[
('data_sheet_id', models.AutoField(serialize=False, primary_key=True)),
('name', models.CharField(max_length=200)),
('topology', models.ForeignKey(to='network_ui.Topology')),
],
),
migrations.CreateModel(
name='DataType',
fields=[
('data_type_id', models.AutoField(serialize=False, primary_key=True)),
('type_name', models.CharField(max_length=200)),
],
),
migrations.AddField(
model_name='databinding',
name='data_type',
field=models.ForeignKey(to='network_ui.DataType'),
),
migrations.AddField(
model_name='databinding',
name='sheet',
field=models.ForeignKey(to='network_ui.DataSheet'),
),
]

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('network_ui', '0016_auto_20170717_1520'),
]
operations = [
migrations.AddField(
model_name='datasheet',
name='client',
field=models.ForeignKey(default=1, to='network_ui.Client'),
preserve_default=False,
),
migrations.AlterField(
model_name='databinding',
name='data_binding_id',
field=models.AutoField(serialize=False, primary_key=True),
),
migrations.AlterField(
model_name='group',
name='id',
field=models.IntegerField(),
),
migrations.AlterField(
model_name='group',
name='x2',
field=models.IntegerField(),
),
migrations.AlterField(
model_name='group',
name='y1',
field=models.IntegerField(),
),
migrations.AlterField(
model_name='topology',
name='device_id_seq',
field=models.IntegerField(default=0),
),
]

View File

@ -34,7 +34,7 @@ class Topology(models.Model):
scale = models.FloatField()
panX = models.FloatField()
panY = models.FloatField()
device_id_seq = models.IntegerField('Topology', default=0)
device_id_seq = models.IntegerField(default=0)
link_id_seq = models.IntegerField(default=0)
group_id_seq = models.IntegerField(default=0)
@ -81,11 +81,11 @@ class Interface(models.Model):
class Group(models.Model):
group_id = models.AutoField(primary_key=True,)
id = models.IntegerField('Group',)
id = models.IntegerField()
name = models.CharField(max_length=200,)
x1 = models.IntegerField()
y1 = models.IntegerField('Group',)
x2 = models.IntegerField('Group',)
y1 = models.IntegerField()
x2 = models.IntegerField()
y2 = models.IntegerField()
topology = models.ForeignKey('Topology',)
@ -95,3 +95,29 @@ class GroupDevice(models.Model):
group_device_id = models.AutoField(primary_key=True,)
group = models.ForeignKey('Group',)
device = models.ForeignKey('Device',)
class DataBinding(models.Model):
data_binding_id = models.AutoField(primary_key=True,)
column = models.IntegerField()
row = models.IntegerField()
table = models.CharField(max_length=200,)
primary_key_id = models.IntegerField()
field = models.CharField(max_length=200,)
data_type = models.ForeignKey('DataType',)
sheet = models.ForeignKey('DataSheet',)
class DataType(models.Model):
data_type_id = models.AutoField(primary_key=True,)
type_name = models.CharField(max_length=200,)
class DataSheet(models.Model):
data_sheet_id = models.AutoField(primary_key=True,)
name = models.CharField(max_length=200,)
topology = models.ForeignKey('Topology',)
client = models.ForeignKey('Client',)

View File

@ -3,6 +3,7 @@ from awx.network_ui.consumers import ws_connect, ws_message, ws_disconnect, cons
from awx.network_ui.consumers import ansible_connect, ansible_message, ansible_disconnect
from awx.network_ui.consumers import worker_connect, worker_message, worker_disconnect
from awx.network_ui.consumers import tester_connect, tester_message, tester_disconnect
from awx.network_ui.consumers import tables_connect, tables_message, tables_disconnect
channel_routing = [
route("websocket.connect", ws_connect, path=r"^/network_ui/topology"),
@ -17,6 +18,9 @@ channel_routing = [
route("websocket.connect", tester_connect, path=r"^/network_ui/tester"),
route("websocket.receive", tester_message, path=r"^/network_ui/tester"),
route("websocket.disconnect", tester_disconnect, path=r"^/network_ui/tester"),
route("websocket.connect", tables_connect, path=r"^/network_ui/tables"),
route("websocket.receive", tables_message, path=r"^/network_ui/tables"),
route("websocket.disconnect", tables_disconnect, path=r"^/network_ui/tables"),
route("console_printer", console_printer),
route("persistence", persistence.handle),
route("discovery", discovery.handle),

View File

@ -1,7 +1,9 @@
<!DOCTYPE html>
<html ng-app="tower">
<head>
<link href="https://netdna.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="css/style.css" />
<link rel="stylesheet" href="css/xeditable.css" />
<script src="js/vendor.bundle.js"></script>
<script src="js/bundle.js"></script>
</head>

View File

@ -10,17 +10,18 @@
"license": "ISC",
"dependencies": {
"angular": "~1.6.2",
"angular-mousewheel": "~1.0.5",
"angular-ui-router": "",
"webpack": "",
"angular-xeditable": "~0.8.0",
"browserify": "",
"hamsterjs": "~1.1.2",
"inherits": "",
"require": "",
"jshint": "",
"less": "",
"less": "^2.7.2",
"mathjs": "",
"reconnectingwebsocket": "^1.0.0",
"hamsterjs": "~1.1.2",
"angular-mousewheel": "~1.0.5"
"require": "",
"webpack": ""
},
"devDependencies": {
"eslint": "^3.17.1",

View File

@ -1,4 +1,6 @@
var networkUI = require('./network.ui.app.js');
var tablesUI = require('./tables.ui.app.js');
var tower = require('./tower.app.js');
exports.networkUI = networkUI.networkUI;
+exports.tablesUI = tablesUI.tablesUI;
exports.tower = tower.tower;

View File

@ -317,3 +317,14 @@ function GroupMembership(sender, id, members) {
this.members = members;
}
exports.GroupMembership = GroupMembership;
function TableCellEdit(sender, sheet, col, row, old_value, new_value) {
this.msg_type = "TableCellEdit";
this.sender = sender;
this.sheet = sheet;
this.col = col;
this.row = row;
this.old_value = old_value;
this.new_value = new_value;
}
exports.TableCellEdit = TableCellEdit;

View File

@ -0,0 +1,15 @@
//console.log = function () { };
var angular = require('angular');
var TablesUIController = require('./tables.ui.controller.js');
var awxTablesUI = require('./tables.ui.directive.js');
var tablesUI = angular.module('tablesUI', ['xeditable'])
.controller('TablesUIController', TablesUIController.TablesUIController)
.directive('awxTablesUi', awxTablesUI.awxTablesUI)
.run(function(editableOptions) {
editableOptions.theme = 'bs3';
editableOptions.activate = 'select';
});
exports.tablesUI = tablesUI;

View File

@ -0,0 +1,152 @@
var util = require('./util.js');
var messages = require('./messages.js');
var ReconnectingWebSocket = require('reconnectingwebsocket');
var TablesUIController = function($scope, $window, $location, $timeout) {
$window.scope = $scope;
$scope.disconnected = false;
$scope.topology_id = $location.search().topology_id || 0;
if (!$scope.disconnected) {
$scope.control_socket = new ReconnectingWebSocket("ws://" + window.location.host + "/network_ui/tables?topology_id=" + $scope.topology_id,
null,
{debug: false, reconnectInterval: 300});
} else {
$scope.control_socket = {
on_message: util.noop
};
}
$scope.client_id = 0;
$scope.message_id_seq = util.natural_numbers(0);
$scope.onClientId = function(data) {
$scope.client_id = data;
};
$scope.control_socket.onmessage = function(message) {
var type_data = JSON.parse(message.data);
var type = type_data[0];
var data = type_data[1];
$scope.handle_message(type, data);
$scope.$apply();
};
$scope.control_socket.onopen = function() {
//Ignore
};
// Call onopen directly if $scope.control_socket is already open
if ($scope.control_socket.readyState === WebSocket.OPEN) {
$scope.control_socket.onopen();
}
$scope.send_control_message = function (message) {
var i = 0;
message.sender = $scope.client_id;
message.message_id = $scope.message_id_seq();
if (message.constructor.name === "MultipleMessage") {
for (i=0; i < message.messages.length; i++) {
message.messages[i].message_id = $scope.message_id_seq();
}
}
var data = messages.serialize(message);
if (!$scope.disconnected) {
$scope.control_socket.send(data);
console.log("Sent message");
} else {
console.log(data);
}
};
$scope.handle_message = function(msg_type, message) {
var handler_name = 'on' + msg_type;
if (typeof(this[handler_name]) !== "undefined") {
this[handler_name](msg_type, message);
} else {
this.default_handler(msg_type, message);
}
};
$scope.default_handler = function(msg_type, message) {
console.log([msg_type, message]);
};
// End web socket
//
//
$scope.onid = function(msg_type, message) {
console.log(["Set client_id to" , message]);
$scope.client_id = message;
};
$scope.ontopology_id = function(msg_type, message) {
console.log(["Set topology_id to" , message]);
$scope.topology_id = message;
$location.search({topology_id: message});
};
$scope.onsheet = function(msg_type, message) {
console.log("Update sheet");
console.log(message);
$scope.data = message.data;
$scope.name = message.name;
$scope.sheets.push(message.name);
$scope.sheets_by_name[message.name] = message.data;
};
$scope.onTableCellEdit = function(msg_type, message) {
if (message.sender === $scope.client_id) {
return;
}
console.log(["Updating data", message.sheet, message.row, message.col, message.new_value]);
$scope.sheets_by_name[message.sheet][message.row][message.col].value = message.new_value;
$scope.$apply();
};
$scope.user = {
name: 'world'
};
$scope.data = [];
$scope.sheets = [];
$scope.sheets_by_name = {};
console.log("Tables UI started");
$scope.$on('$destroy', function () {
console.log("Tables UI stopping");
});
$scope.updateData = function (old_data, new_data, column_index, row_index, column_name, row_name) {
console.log(['updateData', $scope.name, old_data, new_data, column_index, row_index, column_name, row_name]);
$scope.send_control_message(new messages.TableCellEdit($scope.client_id,
$scope.name,
column_index,
row_index,
old_data,
new_data));
$timeout(function () {
var q = document.querySelectorAll("#" + $scope.name + "_" + column_index + "_" + (row_index + 1));
if (q.length > 0) {
q[0].click();
}
});
};
$scope.changeSheet = function(sheet) {
$scope.name = sheet;
$scope.data = $scope.sheets_by_name[sheet];
};
};
exports.TablesUIController = TablesUIController;
console.log("Tables UI loaded");

View File

@ -0,0 +1,5 @@
function awxTablesUI () {
return { restrict: 'E', templateUrl: '/static/network_ui/widgets/tables_ui.html' };
}
exports.awxTablesUI = awxTablesUI;

View File

@ -2,7 +2,7 @@
var angular = require('angular');
var ui_router = require('angular-ui-router');
var tower = angular.module('tower', ['networkUI', 'ui.router']);
var tower = angular.module('tower', ['tablesUI', 'networkUI', 'ui.router']);
tower.config(function($stateProvider, $urlRouterProvider) {
@ -12,7 +12,7 @@ tower.config(function($stateProvider, $urlRouterProvider) {
.state({
name: 'index',
url: '/index',
template: '<a href="#!/topology">Topology</a>'
template: '<ul><li><a href="#!/topology">Topology</a></li><li><a href="#!/tables">Tables</a></li></ul>'
});
$stateProvider
@ -21,6 +21,13 @@ tower.config(function($stateProvider, $urlRouterProvider) {
url: '/topology',
template: "<awx-network-ui></awx-network-ui>"
});
$stateProvider
.state({
name: 'tables',
url: '/tables',
template: "<awx-tables-ui></awx-tables-ui>"
});
});
exports.tower = tower;

View File

@ -6,7 +6,8 @@ module.exports = {
"angular-ui-router",
"hamsterjs",
"angular-mousewheel",
"reconnectingwebsocket"],
"reconnectingwebsocket",
"angular-xeditable"]
},
output: {
path: __dirname + "/js",

View File

@ -0,0 +1,29 @@
<div ng-controller="TablesUIController">
<ul class="nav nav-tabs">
<li ng-repeat="sheet in sheets" role="presentation" class="{{sheet === name ? 'active': ''}}"><a href ng-click="changeSheet(sheet)">{{sheet}}</a></li>
</ul>
<table class="table table-bordered">
<colgroup>
<col ng-repeat="column_header in data[0]" style="width:10%">
</colgroup>
<thead>
<tr>
<th>
</th>
<th ng-repeat="column_header in data[0]">
{{column_header.value}}
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="(row_index, row) in data track by $index" ng-if="$index > 0">
<th>
{{row[0].value}}
</th>
<td ng-repeat="(column_index, cell) in row track by $index" ng-if="$index > 0" buttons="no" onbeforesave="updateData(cell.value, $data, column_index, row_index, data[0][column_index-1].value, row[0].value)" editable-text="cell.value" edit-disabled="{{!cell.editable}}" ng-attr-id="{{name + '_' + column_index + '_' + row_index}}">
{{cell.value}}
</td>
</tr>
</tbody>
</table>
</div>