mirror of
https://github.com/ansible/awx.git
synced 2026-01-16 04:10:44 -03:30
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:
parent
d0e402c39a
commit
8fb54efa8e
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
50
awx/network_ui/migrations/0016_auto_20170717_1520.py
Normal file
50
awx/network_ui/migrations/0016_auto_20170717_1520.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
45
awx/network_ui/migrations/0017_auto_20170717_1813.py
Normal file
45
awx/network_ui/migrations/0017_auto_20170717_1813.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@ -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',)
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
15
awx/network_ui/static/network_ui/src/tables.ui.app.js
Normal file
15
awx/network_ui/static/network_ui/src/tables.ui.app.js
Normal 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;
|
||||
152
awx/network_ui/static/network_ui/src/tables.ui.controller.js
Normal file
152
awx/network_ui/static/network_ui/src/tables.ui.controller.js
Normal 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");
|
||||
@ -0,0 +1,5 @@
|
||||
|
||||
function awxTablesUI () {
|
||||
return { restrict: 'E', templateUrl: '/static/network_ui/widgets/tables_ui.html' };
|
||||
}
|
||||
exports.awxTablesUI = awxTablesUI;
|
||||
@ -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;
|
||||
|
||||
@ -6,7 +6,8 @@ module.exports = {
|
||||
"angular-ui-router",
|
||||
"hamsterjs",
|
||||
"angular-mousewheel",
|
||||
"reconnectingwebsocket"],
|
||||
"reconnectingwebsocket",
|
||||
"angular-xeditable"]
|
||||
},
|
||||
output: {
|
||||
path: __dirname + "/js",
|
||||
|
||||
29
awx/network_ui/static/network_ui/widgets/tables_ui.html
Normal file
29
awx/network_ui/static/network_ui/widgets/tables_ui.html
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user