diff --git a/awx/main/middleware.py b/awx/main/middleware.py
index 8ae8d444e1..90e74af7e1 100644
--- a/awx/main/middleware.py
+++ b/awx/main/middleware.py
@@ -178,7 +178,7 @@ class URLModificationMiddleware(object):
return '/'.join(url_units)
def process_request(self, request):
- if 'REQUEST_URI' in request.environ:
+ if hasattr(request, 'environ') and 'REQUEST_URI' in request.environ:
old_path = six.moves.urllib.parse.urlsplit(request.environ['REQUEST_URI']).path
old_path = old_path[request.path.find(request.path_info):]
else:
diff --git a/awx/main/routing.py b/awx/main/routing.py
index 0a49f25c6c..79a3c84a5a 100644
--- a/awx/main/routing.py
+++ b/awx/main/routing.py
@@ -1,4 +1,5 @@
from channels.routing import route
+from awx.network_ui.routing import channel_routing as network_ui_routing
channel_routing = [
@@ -6,3 +7,6 @@ channel_routing = [
route("websocket.disconnect", "awx.main.consumers.ws_disconnect", path=r'^/websocket/$'),
route("websocket.receive", "awx.main.consumers.ws_receive", path=r'^/websocket/$'),
]
+
+
+channel_routing += network_ui_routing
diff --git a/awx/network_ui/CONTRIBUTING.md b/awx/network_ui/CONTRIBUTING.md
new file mode 100644
index 0000000000..cd028ef1e1
--- /dev/null
+++ b/awx/network_ui/CONTRIBUTING.md
@@ -0,0 +1,133 @@
+Network UI
+==========
+
+See [awx/ui/client/src/network-ui/CONTRIBUTING.md](../ui/client/src/network-ui/CONTRIBUTING.md) for the introduction
+to the Network UI client-side development.
+
+Server-Side Development
+-----------------------
+
+This document covers the Network UI server-side development.
+
+The Network UI is a UX driven feature to provide a graphical user
+experience that fits well into the network engineer's normal workflow. Their
+normal workflow includes a diagram drawn in a graphical drawing program, a
+spreadsheet, and the command line interface of their network gear. Network
+architects design the network on the graphical diagram and then hand off the
+architecture to network operators who implement the architecture on the network
+using spreadsheets to manage their data and manually converting the data into
+CLI commands using their networking expertise and expertise with their physical
+gear.
+
+The server-side code supports the persistence needed to provide this graphical
+user experience of architecting a network and using that information along with
+additional information (stored in vars files) to configure the network devices
+using the CLI or NETCONF using Ansible playbooks and roles.
+
+Network UI Data Schema
+----------------------
+
+For the 3.3 release the persistence needed includes the position information of
+the devices on the virtual canvas and the type of the devices as well as
+information about the interfaces on the devices and the links connecting those
+interfaces.
+
+These requirements determine the database schema needed for the network UI which
+requires these models: Topology, Device, Interface, Link, Client, and TopologyInventory.
+
+
+
+This diagram shows the relationships between the models in the Network UI schema.
+
+The models are:
+
+* Device - a host, switch, router, or other networking device
+* Interface - a connection point on a device for a link
+* Link - a physical connection between two devices to their respective interfaces
+* Topology - a collection of devices and links
+* TopologyInventory - a mapping between topologies and Tower inventories
+* Client - a UI client session
+
+
+Network UI Websocket Protocol
+-----------------------------
+
+Persistence for the network UI canvas state is implemented using an
+asynchronous websocket protocol to send information from the client to the
+server and vice-versa. This two-way communication was chosen to support future
+features for streaming data to the canvas, broadcast messaging between clients,
+and for interaction performance on the UI.
+
+
+Messages
+--------
+
+JSON messages are passed over the `/network_ui/topology` websocket between the
+test client and the test server. The protocol that is used for all messages is
+in ABNF (RFC5234):
+
+
+ message_type = 'DeviceMove' / 'DeviceCreate' / 'DeviceDestroy' / 'DeviceLabelEdit' / 'DeviceSelected' / 'DeviceUnSelected' / 'InterfaceCreate' / 'InterfaceLabelEdit' / 'LinkLabelEdit' / 'LinkCreate' / 'LinkDestroy' / 'LinkSelected' / 'LinkUnSelected' / 'MultipleMessage' / 'Snapshot'
+ message_data = '{' 'msg_type' ': ' message_type ', ' key-value *( ', ' key-value ) '}'
+ message = '[ id , ' posint ']' / '[ topology_id , ' posint ']' / '[' message_type ', ' message_data ']'
+
+See https://github.com/AndyA/abnfgen/blob/master/andy/json.abnf for the rest of
+the JSON ABNF.
+
+See [designs/messages.yml](designs/messages.yml) for the allowable keys and
+values for each message type.
+
+
+Initially when the websocket is first opened the server will send four messages
+to the client. These are:
+
+* the client id using the `id` message type.
+* the topology id using the `topology` message type.
+* a Topology record containing data for the canvas itself.
+* a Snapshot message containing all the data of the data on the canvas.
+
+As the user interacts with the canvas messages will be generated by the client
+and the `network_ui.consumers.Persistence` class will update the models that
+represent the canvas.
+
+
+
+Persistence
+-----------
+
+The class `awx.network_uiconsumers.Persistence` provides persistence for the Network UI canvas.
+It does so by providing message handlers that handle storage of the canvas change events
+into the database. Each event has a message handle with name `onX` where `X` is the name of the message
+type. The handlers use the `filter/values_list`, `filter/values`, `filter/update`, and `filter/delete`
+patterns to update the data in the database quickly with a constant O(1) number of queries per event
+often with only one query needed. With `filter/update` and `filter/delete` all the work is done
+in the database and Python never needs to instaniate and garbage collect the model objects.
+
+Bulk operations (`filter/values`) in `send_snapshot` are used to produce a constant number of
+queries produce a snapshot when the canvas is first loaded. This method avoids creating
+the model objects since it only produces dicts that are JSON serializable which are bundled
+together for the `Snapshot` message type.
+
+This method of persistence uses Django as a database query-compiler for transforms from
+the event types to the database types. Using Django in this way is very performant since
+Python does very little work processing the data and when possible the data never leaves
+the database.
+
+
+Client Tracking
+---------------
+
+Each user session to the network UI canvas is tracked with the `Client` model. Multiple
+clients can view and interact with the network UI canvas at a time. They will see each other's
+edits to the canvas in real time. This works by broadcasting the canvas change events to
+all clients viewing the same topology.
+
+```
+ # Send to all clients editing the topology
+ Group("topology-%s" % message.channel_session['topology_id']).send({"text": message['text']})
+```
+
+API
+---
+
+There is no user accessible API for this feature in the 3.3 release.
diff --git a/awx/network_ui/__init__.py b/awx/network_ui/__init__.py
new file mode 100644
index 0000000000..ebed9407c5
--- /dev/null
+++ b/awx/network_ui/__init__.py
@@ -0,0 +1,2 @@
+# Copyright (c) 2017 Red Hat, Inc
+
diff --git a/awx/network_ui/consumers.py b/awx/network_ui/consumers.py
new file mode 100644
index 0000000000..a67fe0a2e5
--- /dev/null
+++ b/awx/network_ui/consumers.py
@@ -0,0 +1,280 @@
+# Copyright (c) 2017 Red Hat, Inc
+from channels import Group
+from channels.sessions import channel_session
+from awx.network_ui.models import Topology, Device, Link, Client, Interface
+from awx.network_ui.models import TopologyInventory
+import urlparse
+from django.db.models import Q
+from collections import defaultdict
+import logging
+
+
+from awx.network_ui.utils import transform_dict
+
+import json
+
+logger = logging.getLogger("awx.network_ui.consumers")
+
+
+def parse_inventory_id(data):
+ inventory_id = data.get('inventory_id', ['null'])
+ try:
+ inventory_id = int(inventory_id[0])
+ except ValueError:
+ inventory_id = None
+ if not inventory_id:
+ inventory_id = None
+ return inventory_id
+
+
+class NetworkingEvents(object):
+
+ '''
+ Provides handlers for the networking events for the topology canvas.
+ '''
+
+ def parse_message_text(self, message_text, client_id):
+ '''
+ See the Messages of CONTRIBUTING.md for the message format.
+ '''
+ data = json.loads(message_text)
+ if len(data) == 2:
+ message_type = data.pop(0)
+ message_value = data.pop(0)
+ if isinstance(message_value, list):
+ logger.error("Message has no sender")
+ return None, None
+ if isinstance(message_value, dict) and client_id != message_value.get('sender'):
+ logger.error("client_id mismatch expected: %s actual %s", client_id, message_value.get('sender'))
+ return None, None
+ return message_type, message_value
+ else:
+ logger.error("Invalid message text")
+ return None, None
+
+ def handle(self, message):
+ '''
+ Dispatches a message based on the message type to a handler function
+ of name onX where X is the message type.
+ '''
+ topology_id = message.get('topology')
+ assert topology_id is not None, "No topology_id"
+ client_id = message.get('client')
+ assert client_id is not None, "No client_id"
+ message_type, message_value = self.parse_message_text(message['text'], client_id)
+ if message_type is None:
+ return
+ handler = self.get_handler(message_type)
+ if handler is not None:
+ handler(message_value, topology_id, client_id)
+ else:
+ logger.warning("Unsupported message %s: no handler", message_type)
+
+ def get_handler(self, message_type):
+ return getattr(self, "on{0}".format(message_type), None)
+
+ def onDeviceCreate(self, device, topology_id, client_id):
+ device = transform_dict(dict(x='x',
+ y='y',
+ name='name',
+ type='device_type',
+ id='cid',
+ host_id='host_id'), device)
+ logger.info("Device %s", device)
+ d, _ = Device.objects.get_or_create(topology_id=topology_id, cid=device['cid'], defaults=device)
+ d.x = device['x']
+ d.y = device['y']
+ d.device_type = device['device_type']
+ d.host_id = device['host_id']
+ d.save()
+ (Topology.objects
+ .filter(pk=topology_id, device_id_seq__lt=device['cid'])
+ .update(device_id_seq=device['cid']))
+
+ def onDeviceDestroy(self, device, topology_id, client_id):
+ Device.objects.filter(topology_id=topology_id, cid=device['id']).delete()
+
+ def onDeviceMove(self, device, topology_id, client_id):
+ Device.objects.filter(topology_id=topology_id, cid=device['id']).update(x=device['x'], y=device['y'])
+
+ def onDeviceInventoryUpdate(self, device, topology_id, client_id):
+ Device.objects.filter(topology_id=topology_id, cid=device['id']).update(host_id=device['host_id'])
+
+ def onDeviceLabelEdit(self, device, topology_id, client_id):
+ Device.objects.filter(topology_id=topology_id, cid=device['id']).update(name=device['name'])
+
+ def onInterfaceLabelEdit(self, interface, topology_id, client_id):
+ (Interface.objects
+ .filter(device__topology_id=topology_id,
+ cid=interface['id'],
+ device__cid=interface['device_id'])
+ .update(name=interface['name']))
+
+ def onLinkLabelEdit(self, link, topology_id, client_id):
+ Link.objects.filter(from_device__topology_id=topology_id, cid=link['id']).update(name=link['name'])
+
+ def onInterfaceCreate(self, interface, topology_id, client_id):
+ Interface.objects.get_or_create(device_id=Device.objects.get(cid=interface['device_id'],
+ topology_id=topology_id).pk,
+ cid=interface['id'],
+ defaults=dict(name=interface['name']))
+ (Device.objects
+ .filter(cid=interface['device_id'],
+ topology_id=topology_id,
+ interface_id_seq__lt=interface['id'])
+ .update(interface_id_seq=interface['id']))
+
+ def onLinkCreate(self, link, topology_id, client_id):
+ device_map = dict(Device.objects
+ .filter(topology_id=topology_id, cid__in=[link['from_device_id'], link['to_device_id']])
+ .values_list('cid', 'pk'))
+ Link.objects.get_or_create(cid=link['id'],
+ name=link['name'],
+ from_device_id=device_map[link['from_device_id']],
+ to_device_id=device_map[link['to_device_id']],
+ from_interface_id=Interface.objects.get(device_id=device_map[link['from_device_id']],
+ cid=link['from_interface_id']).pk,
+ to_interface_id=Interface.objects.get(device_id=device_map[link['to_device_id']],
+ cid=link['to_interface_id']).pk)
+ (Topology.objects
+ .filter(pk=topology_id, link_id_seq__lt=link['id'])
+ .update(link_id_seq=link['id']))
+
+ def onLinkDestroy(self, link, topology_id, client_id):
+ device_map = dict(Device.objects
+ .filter(topology_id=topology_id, cid__in=[link['from_device_id'], link['to_device_id']])
+ .values_list('cid', 'pk'))
+ if link['from_device_id'] not in device_map:
+ return
+ if link['to_device_id'] not in device_map:
+ return
+ Link.objects.filter(cid=link['id'],
+ from_device_id=device_map[link['from_device_id']],
+ to_device_id=device_map[link['to_device_id']],
+ from_interface_id=Interface.objects.get(device_id=device_map[link['from_device_id']],
+ cid=link['from_interface_id']).pk,
+ to_interface_id=Interface.objects.get(device_id=device_map[link['to_device_id']],
+ cid=link['to_interface_id']).pk).delete()
+
+ def onDeviceSelected(self, message_value, topology_id, client_id):
+ 'Ignore DeviceSelected messages'
+ pass
+
+ def onDeviceUnSelected(self, message_value, topology_id, client_id):
+ 'Ignore DeviceSelected messages'
+ pass
+
+ def onLinkSelected(self, message_value, topology_id, client_id):
+ 'Ignore LinkSelected messages'
+ pass
+
+ def onLinkUnSelected(self, message_value, topology_id, client_id):
+ 'Ignore LinkSelected messages'
+ pass
+
+ def onMultipleMessage(self, message_value, topology_id, client_id):
+ for message in message_value['messages']:
+ handler = self.get_handler(message['msg_type'])
+ if handler is not None:
+ handler(message, topology_id, client_id)
+ else:
+ logger.warning("Unsupported message %s", message['msg_type'])
+
+
+networking_events_dispatcher = NetworkingEvents()
+
+
+@channel_session
+def ws_connect(message):
+ data = urlparse.parse_qs(message.content['query_string'])
+ inventory_id = parse_inventory_id(data)
+ topology_ids = list(TopologyInventory.objects.filter(inventory_id=inventory_id).values_list('pk', flat=True))
+ topology_id = None
+ if len(topology_ids) > 0:
+ topology_id = topology_ids[0]
+ if topology_id is not None:
+ topology = Topology.objects.get(pk=topology_id)
+ else:
+ topology = Topology(name="topology", scale=1.0, panX=0, panY=0)
+ topology.save()
+ TopologyInventory(inventory_id=inventory_id, topology_id=topology.pk).save()
+ topology_id = topology.pk
+ message.channel_session['topology_id'] = topology_id
+ Group("topology-%s" % topology_id).add(message.reply_channel)
+ client = Client()
+ client.save()
+ message.channel_session['client_id'] = client.pk
+ Group("client-%s" % client.pk).add(message.reply_channel)
+ message.reply_channel.send({"text": json.dumps(["id", client.pk])})
+ message.reply_channel.send({"text": json.dumps(["topology_id", topology_id])})
+ topology_data = transform_dict(dict(id='topology_id',
+ name='name',
+ panX='panX',
+ panY='panY',
+ scale='scale',
+ link_id_seq='link_id_seq',
+ device_id_seq='device_id_seq'), topology.__dict__)
+
+ message.reply_channel.send({"text": json.dumps(["Topology", topology_data])})
+ send_snapshot(message.reply_channel, topology_id)
+
+
+def send_snapshot(channel, topology_id):
+ interfaces = defaultdict(list)
+
+ for i in (Interface.objects
+ .filter(device__topology_id=topology_id)
+ .values()):
+ i = transform_dict(dict(cid='id',
+ device_id='device_id',
+ id='interface_id',
+ name='name'), i)
+ interfaces[i['device_id']].append(i)
+ devices = list(Device.objects.filter(topology_id=topology_id).values())
+ devices = [transform_dict(dict(cid='id',
+ id='device_id',
+ device_type='device_type',
+ host_id='host_id',
+ name='name',
+ x='x',
+ y='y',
+ interface_id_seq='interface_id_seq'), x) for x in devices]
+ for device in devices:
+ device['interfaces'] = interfaces[device['device_id']]
+
+ links = [dict(id=x['cid'],
+ name=x['name'],
+ from_device_id=x['from_device__cid'],
+ to_device_id=x['to_device__cid'],
+ from_interface_id=x['from_interface__cid'],
+ to_interface_id=x['to_interface__cid'])
+ for x in list(Link.objects
+ .filter(Q(from_device__topology_id=topology_id) |
+ Q(to_device__topology_id=topology_id))
+ .values('cid',
+ 'name',
+ 'from_device__cid',
+ 'to_device__cid',
+ 'from_interface__cid',
+ 'to_interface__cid'))]
+ snapshot = dict(sender=0,
+ devices=devices,
+ links=links)
+ channel.send({"text": json.dumps(["Snapshot", snapshot])})
+
+
+@channel_session
+def ws_message(message):
+ # Send to all clients editing the topology
+ Group("topology-%s" % message.channel_session['topology_id']).send({"text": message['text']})
+ # Send to networking_events handler
+ networking_events_dispatcher.handle({"text": message['text'],
+ "topology": message.channel_session['topology_id'],
+ "client": message.channel_session['client_id']})
+
+
+@channel_session
+def ws_disconnect(message):
+ if 'topology_id' in message.channel_session:
+ Group("topology-%s" % message.channel_session['topology_id']).discard(message.reply_channel)
+
diff --git a/awx/network_ui/docs/README.md b/awx/network_ui/docs/README.md
new file mode 100644
index 0000000000..2c8ff94bda
--- /dev/null
+++ b/awx/network_ui/docs/README.md
@@ -0,0 +1,8 @@
+
+
+The design files in this directory are used in the database schema designer tool.
+
+* [models.png](models.png) - An image of the database schema design for network UI.
+* [models.yml](models.yml) - Provides the main schema design for the network UI project.
+
+
diff --git a/awx/network_ui/docs/messages.yml b/awx/network_ui/docs/messages.yml
new file mode 100644
index 0000000000..06ee3b9b75
--- /dev/null
+++ b/awx/network_ui/docs/messages.yml
@@ -0,0 +1,19 @@
+messages:
+ - {msg_type: DeviceMove, fields: [msg_type, sender, id, x, y, previous_x, previous_y]}
+ - {msg_type: DeviceCreate, fields: [msg_type, sender, id, x, y, name, type, host_id]}
+ - {msg_type: DeviceDestroy, fields: [msg_type, sender, id, previous_x, previous_y, previous_name, previous_type, previous_host_id]}
+ - {msg_type: DeviceLabelEdit, fields: [msg_type, sender, id, name, previous_name]}
+ - {msg_type: DeviceSelected, fields: [msg_type, sender, id]}
+ - {msg_type: DeviceUnSelected, fields: [msg_type, sender, id]}
+ - {msg_type: InterfaceCreate, fields: [msg_type, sender, device_id, id, name]}
+ - {msg_type: InterfaceLabelEdit, fields: [msg_type, sender, id, device_id, name, previous_name]}
+ - {msg_type: LinkLabelEdit, fields: [msg_type, sender, id, name, previous_name]}
+ - {msg_type: LinkCreate, fields: [msg_type, id, sender, name, from_device_id, to_device_id, from_interface_id, to_interface_id]}
+ - {msg_type: LinkDestroy, fields: [msg_type, id, sender, name, from_device_id, to_device_id, from_interface_id, to_interface_id]}
+ - {msg_type: LinkSelected, fields: [msg_type, sender, id]}
+ - {msg_type: LinkUnSelected, fields: [msg_type, sender, id]}
+ - {msg_type: MultipleMessage, fields: [msg_type, sender, messages]}
+ - {msg_type: Snapshot, fields: [msg_type, sender, devices, links, order, trace_id]}
+ - {msg_type: id, type: int}
+ - {msg_type: topology_id, type: int}
+ - {msg_type: Topology, fields: [topology_id, name, panX, panY, scale, link_id_seq, device_id_seq]}
diff --git a/awx/network_ui/docs/models.png b/awx/network_ui/docs/models.png
new file mode 100644
index 0000000000..c6b22910d8
Binary files /dev/null and b/awx/network_ui/docs/models.png differ
diff --git a/awx/network_ui/docs/models.yml b/awx/network_ui/docs/models.yml
new file mode 100644
index 0000000000..683dde4bfd
--- /dev/null
+++ b/awx/network_ui/docs/models.yml
@@ -0,0 +1,130 @@
+app: awx.network_ui
+external_models: []
+models:
+- display: name
+ fields:
+ - name: device_id
+ pk: true
+ type: AutoField
+ - name: topology
+ ref: Topology
+ ref_field: topology_id
+ type: ForeignKey
+ - len: 200
+ name: name
+ type: CharField
+ - name: x
+ type: IntegerField
+ - name: y
+ type: IntegerField
+ - name: id
+ type: IntegerField
+ - len: 200
+ name: device_type
+ type: CharField
+ - default: 0
+ name: interface_id_seq
+ type: IntegerField
+ - default: 0
+ name: host_id
+ type: IntegerField
+ name: Device
+ x: 348
+ y: 124
+- fields:
+ - name: link_id
+ pk: true
+ type: AutoField
+ - name: from_device
+ ref: Device
+ ref_field: device_id
+ related_name: from_link
+ type: ForeignKey
+ - name: to_device
+ ref: Device
+ ref_field: device_id
+ related_name: to_link
+ type: ForeignKey
+ - name: from_interface
+ ref: Interface
+ ref_field: interface_id
+ related_name: from_link
+ type: ForeignKey
+ - name: to_interface
+ ref: Interface
+ ref_field: interface_id
+ related_name: to_link
+ type: ForeignKey
+ - name: id
+ type: IntegerField
+ - len: 200
+ name: name
+ type: CharField
+ name: Link
+ x: 731
+ y: -33
+- display: name
+ fields:
+ - name: topology_id
+ pk: true
+ type: AutoField
+ - len: 200
+ name: name
+ type: CharField
+ - name: scale
+ type: FloatField
+ - name: panX
+ type: FloatField
+ - name: panY
+ type: FloatField
+ - default: 0
+ name: device_id_seq
+ type: IntegerField
+ - default: 0
+ name: link_id_seq
+ type: IntegerField
+ name: Topology
+ x: 111
+ y: 127
+- fields:
+ - name: client_id
+ pk: true
+ type: AutoField
+ name: Client
+ x: -162
+ y: 282
+- display: name
+ fields:
+ - name: interface_id
+ pk: true
+ type: AutoField
+ - name: device
+ ref: Device
+ ref_field: device_id
+ type: ForeignKey
+ - len: 200
+ name: name
+ type: CharField
+ - name: id
+ type: IntegerField
+ name: Interface
+ x: 977
+ y: 312
+- fields:
+ - name: topology_inventory_id
+ pk: true
+ type: AutoField
+ - name: topology
+ ref: Topology
+ ref_field: topology_id
+ type: ForeignKey
+ - name: inventory_id
+ type: IntegerField
+ name: TopologyInventory
+ x: -204
+ y: 12
+modules: []
+view:
+ panX: 213.729555519212
+ panY: 189.446959094643
+ scaleXY: 0.69
diff --git a/awx/network_ui/migrations/0001_initial.py b/awx/network_ui/migrations/0001_initial.py
new file mode 100644
index 0000000000..9b81e82455
--- /dev/null
+++ b/awx/network_ui/migrations/0001_initial.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.11 on 2018-03-23 20:43
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('main', '0026_v330_emitted_events'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Client',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Device',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('name', models.CharField(blank=True, max_length=200)),
+ ('x', models.IntegerField()),
+ ('y', models.IntegerField()),
+ ('cid', models.IntegerField()),
+ ('device_type', models.CharField(blank=True, max_length=200)),
+ ('interface_id_seq', models.IntegerField(default=0)),
+ ('host', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.Host')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Interface',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('name', models.CharField(blank=True, max_length=200)),
+ ('cid', models.IntegerField()),
+ ('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui.Device')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Link',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('cid', models.IntegerField()),
+ ('name', models.CharField(blank=True, max_length=200)),
+ ('from_device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='from_link', to='network_ui.Device')),
+ ('from_interface', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='from_link', to='network_ui.Interface')),
+ ('to_device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='to_link', to='network_ui.Device')),
+ ('to_interface', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='to_link', to='network_ui.Interface')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Topology',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('name', models.CharField(blank=True, max_length=200)),
+ ('scale', models.FloatField()),
+ ('panX', models.FloatField()),
+ ('panY', models.FloatField()),
+ ('device_id_seq', models.IntegerField(default=0)),
+ ('link_id_seq', models.IntegerField(default=0)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='TopologyInventory',
+ fields=[
+ ('id', models.AutoField(primary_key=True, serialize=False)),
+ ('inventory', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.Inventory')),
+ ('topology', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui.Topology')),
+ ],
+ ),
+ migrations.AddField(
+ model_name='device',
+ name='topology',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='network_ui.Topology'),
+ ),
+ ]
diff --git a/awx/network_ui/migrations/__init__.py b/awx/network_ui/migrations/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/awx/network_ui/models.py b/awx/network_ui/models.py
new file mode 100644
index 0000000000..07d87e26cc
--- /dev/null
+++ b/awx/network_ui/models.py
@@ -0,0 +1,65 @@
+from django.db import models
+
+
+class Device(models.Model):
+
+ id = models.AutoField(primary_key=True,)
+ topology = models.ForeignKey('Topology',)
+ name = models.CharField(max_length=200, blank=True)
+ x = models.IntegerField()
+ y = models.IntegerField()
+ cid = models.IntegerField()
+ device_type = models.CharField(max_length=200, blank=True)
+ interface_id_seq = models.IntegerField(default=0,)
+ host = models.ForeignKey('main.Host', default=None, null=True, on_delete=models.SET_NULL)
+
+ def __unicode__(self):
+ return self.name
+
+
+class Link(models.Model):
+
+ id = models.AutoField(primary_key=True,)
+ from_device = models.ForeignKey('Device', related_name='from_link',)
+ to_device = models.ForeignKey('Device', related_name='to_link',)
+ from_interface = models.ForeignKey('Interface', related_name='from_link',)
+ to_interface = models.ForeignKey('Interface', related_name='to_link',)
+ cid = models.IntegerField()
+ name = models.CharField(max_length=200, blank=True)
+
+
+class Topology(models.Model):
+
+ id = models.AutoField(primary_key=True,)
+ name = models.CharField(max_length=200, blank=True)
+ scale = models.FloatField()
+ panX = models.FloatField()
+ panY = models.FloatField()
+ device_id_seq = models.IntegerField(default=0,)
+ link_id_seq = models.IntegerField(default=0,)
+
+ def __unicode__(self):
+ return self.name
+
+
+class Client(models.Model):
+
+ id = models.AutoField(primary_key=True,)
+
+
+class Interface(models.Model):
+
+ id = models.AutoField(primary_key=True,)
+ device = models.ForeignKey('Device',)
+ name = models.CharField(max_length=200, blank=True)
+ cid = models.IntegerField()
+
+ def __unicode__(self):
+ return self.name
+
+
+class TopologyInventory(models.Model):
+
+ id = models.AutoField(primary_key=True,)
+ topology = models.ForeignKey('Topology',)
+ inventory = models.ForeignKey('main.Inventory')
diff --git a/awx/network_ui/routing.py b/awx/network_ui/routing.py
new file mode 100644
index 0000000000..66553e23d2
--- /dev/null
+++ b/awx/network_ui/routing.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2017 Red Hat, Inc
+from channels.routing import route
+from awx.network_ui.consumers import ws_connect, ws_message, ws_disconnect
+
+channel_routing = [
+ route("websocket.connect", ws_connect, path=r"^/network_ui/topology"),
+ route("websocket.receive", ws_message, path=r"^/network_ui/topology"),
+ route("websocket.disconnect", ws_disconnect, path=r"^/network_ui/topology"),
+]
diff --git a/awx/network_ui/urls.py b/awx/network_ui/urls.py
new file mode 100644
index 0000000000..d08b0a448a
--- /dev/null
+++ b/awx/network_ui/urls.py
@@ -0,0 +1,10 @@
+# Copyright (c) 2017 Red Hat, Inc
+from django.conf.urls import url
+
+from awx.network_ui import views
+
+app_name = 'network_ui'
+urlpatterns = [
+ url(r'^topology.json$', views.json_topology_data, name='json_topology_data'),
+ url(r'^topology.yaml$', views.yaml_topology_data, name='yaml_topology_data'),
+]
diff --git a/awx/network_ui/utils.py b/awx/network_ui/utils.py
new file mode 100644
index 0000000000..9b2eea6c10
--- /dev/null
+++ b/awx/network_ui/utils.py
@@ -0,0 +1,6 @@
+# Copyright (c) 2017 Red Hat, Inc
+
+
+def transform_dict(dict_map, d):
+ return {to_key: d[from_key] for from_key, to_key in dict_map.iteritems()}
+
diff --git a/awx/network_ui/views.py b/awx/network_ui/views.py
new file mode 100644
index 0000000000..e80f03c320
--- /dev/null
+++ b/awx/network_ui/views.py
@@ -0,0 +1,98 @@
+# Copyright (c) 2017 Red Hat, Inc
+from django.shortcuts import render
+from django import forms
+from django.http import JsonResponse, HttpResponseBadRequest, HttpResponse
+from awx.network_ui.models import Topology, Device, Link, Interface
+from django.db.models import Q
+import yaml
+import json
+
+NetworkAnnotatedInterface = Interface.objects.values('name',
+ 'cid',
+ 'from_link__pk',
+ 'to_link__pk',
+ 'from_link__to_device__name',
+ 'to_link__from_device__name',
+ 'from_link__to_interface__name',
+ 'to_link__from_interface__name')
+
+
+def topology_data(topology_id):
+
+ data = dict(devices=[],
+ links=[])
+
+ topology = Topology.objects.get(pk=topology_id)
+
+ data['name'] = topology.name
+ data['topology_id'] = topology_id
+
+ links = list(Link.objects
+ .filter(Q(from_device__topology_id=topology_id) |
+ Q(to_device__topology_id=topology_id)))
+
+ interfaces = Interface.objects.filter(device__topology_id=topology_id)
+
+ for device in Device.objects.filter(topology_id=topology_id).order_by('name'):
+ interfaces = list(NetworkAnnotatedInterface.filter(device_id=device.pk).order_by('name'))
+ interfaces = [dict(name=x['name'],
+ network=x['from_link__pk'] or x['to_link__pk'],
+ remote_device_name=x['from_link__to_device__name'] or x['to_link__from_device__name'],
+ remote_interface_name=x['from_link__to_interface__name'] or x['to_link__from_interface__name'],
+ id=x['cid'],
+ ) for x in interfaces]
+ data['devices'].append(dict(name=device.name,
+ type=device.device_type,
+ x=device.x,
+ y=device.y,
+ id=device.cid,
+ interfaces=interfaces))
+
+ for link in links:
+ data['links'].append(dict(from_device=link.from_device.name,
+ to_device=link.to_device.name,
+ from_interface=link.from_interface.name,
+ to_interface=link.to_interface.name,
+ from_device_id=link.from_device.cid,
+ to_device_id=link.to_device.cid,
+ from_interface_id=link.from_interface.cid,
+ to_interface_id=link.to_interface.cid,
+ name=link.name,
+ network=link.pk))
+
+ return data
+
+
+def yaml_serialize_topology(topology_id):
+ return yaml.safe_dump(topology_data(topology_id), default_flow_style=False)
+
+
+def json_serialize_topology(topology_id):
+ return json.dumps(topology_data(topology_id))
+
+
+def index(request):
+ return render(request, "network_ui/index.html", dict(topologies=Topology.objects.all().order_by('-pk')))
+
+
+class TopologyForm(forms.Form):
+ topology_id = forms.IntegerField()
+
+
+def json_topology_data(request):
+ form = TopologyForm(request.GET)
+ if form.is_valid():
+ return JsonResponse(topology_data(form.cleaned_data['topology_id']))
+ else:
+ return HttpResponseBadRequest(form.errors)
+
+
+def yaml_topology_data(request):
+ form = TopologyForm(request.GET)
+ if form.is_valid():
+ return HttpResponse(yaml.safe_dump(topology_data(form.cleaned_data['topology_id']),
+ default_flow_style=False),
+ content_type='application/yaml')
+ else:
+ return HttpResponseBadRequest(form.errors)
+
diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py
index 7adde14d26..0cbc257ce1 100644
--- a/awx/settings/defaults.py
+++ b/awx/settings/defaults.py
@@ -279,6 +279,7 @@ INSTALLED_APPS = (
'awx.ui',
'awx.sso',
'solo',
+ 'awx.network_ui'
)
INTERNAL_IPS = ('127.0.0.1',)
diff --git a/awx/ui/build/webpack.base.js b/awx/ui/build/webpack.base.js
index e7e5d54676..633987dde0 100644
--- a/awx/ui/build/webpack.base.js
+++ b/awx/ui/build/webpack.base.js
@@ -22,6 +22,7 @@ const SRC_PATH = path.join(CLIENT_PATH, 'src');
const STATIC_PATH = path.join(UI_PATH, 'static');
const TEST_PATH = path.join(UI_PATH, 'test');
const THEME_PATH = path.join(LIB_PATH, 'theme');
+const NETWORK_UI_PATH = path.join(SRC_PATH, 'network-ui');
const APP_ENTRY = path.join(SRC_PATH, 'app.js');
const VENDOR_ENTRY = path.join(SRC_PATH, 'vendor.js');
@@ -57,6 +58,17 @@ const base = {
},
module: {
rules: [
+ {
+ test: /\.js$/,
+ use: {
+ loader: 'istanbul-instrumenter-loader',
+ options: { esModules: true }
+ },
+ enforce: 'pre',
+ include: [
+ /src\/network-ui\//
+ ]
+ },
{
test: /\.js$/,
loader: 'babel-loader',
@@ -97,6 +109,15 @@ const base = {
/src\//
]
},
+ {
+ test: /\.svg$/,
+ use: ['ngtemplate-loader', 'html-loader'],
+ include: [
+ /lib\/components\//,
+ /features\//,
+ /src\//
+ ]
+ },
{
test: /\.json$/,
loader: 'json-loader',
@@ -187,6 +208,7 @@ const base = {
'~test': TEST_PATH,
'~theme': THEME_PATH,
'~ui': UI_PATH,
+ '~network-ui': NETWORK_UI_PATH,
d3$: '~node_modules/d3/d3.min.js',
'codemirror.jsonlint$': '~node_modules/codemirror/addon/lint/json-lint.js',
jquery: '~node_modules/jquery/dist/jquery.js',
diff --git a/awx/ui/build/webpack.watch.js b/awx/ui/build/webpack.watch.js
index 53fb881a9d..d653707847 100644
--- a/awx/ui/build/webpack.watch.js
+++ b/awx/ui/build/webpack.watch.js
@@ -51,6 +51,7 @@ const watch = {
stats: 'minimal',
publicPath: '/static/',
host: '127.0.0.1',
+ https: true,
port: 3000,
https: true,
proxy: {
@@ -64,6 +65,11 @@ const watch = {
target: TARGET,
secure: false,
ws: true
+ },
+ '/network_ui': {
+ target: TARGET,
+ secure: false,
+ ws: true
}
}
}
diff --git a/awx/ui/client/index.template.ejs b/awx/ui/client/index.template.ejs
index 0301d42da0..58ee4d85a0 100644
--- a/awx/ui/client/index.template.ejs
+++ b/awx/ui/client/index.template.ejs
@@ -16,6 +16,7 @@
+
diff --git a/awx/ui/client/lib/theme/index.less b/awx/ui/client/lib/theme/index.less
index b5ac04dfca..9a9f564840 100644
--- a/awx/ui/client/lib/theme/index.less
+++ b/awx/ui/client/lib/theme/index.less
@@ -170,3 +170,9 @@
* the transition.
*/
@import '_resets';
+
+/**
+ * Network Visualization Style
+ *
+ */
+@import '../../src/network-ui/style.less';
diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js
index 5e67b3a381..9a2e6ccc7d 100644
--- a/awx/ui/client/src/app.js
+++ b/awx/ui/client/src/app.js
@@ -10,7 +10,6 @@ if ($basePath) {
}
import start from './app.start';
-
import portalMode from './portal-mode/main';
import systemTracking from './system-tracking/main';
import inventoriesHosts from './inventories-hosts/main';
@@ -46,6 +45,8 @@ import atLibComponents from '~components';
import atLibModels from '~models';
import atLibServices from '~services';
+import networkUI from '~network-ui/main';
+
start.bootstrap(() => {
angular.bootstrap(document.body, ['awApp']);
});
@@ -95,6 +96,7 @@ angular
users.name,
projects.name,
scheduler.name,
+ networkUI.tower.name,
'Utilities',
'templates',
diff --git a/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js b/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js
index f90b953ab6..c075662883 100644
--- a/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js
+++ b/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js
@@ -93,6 +93,13 @@ export default ['i18n', function(i18n) {
fieldActions: {
columnClass: 'col-md-2 col-sm-3 col-xs-4',
+ network: {
+ label: i18n._('Network Visualization'),
+ ngClick: 'goToGraph(inventory)',
+ awToolTip: i18n._('Network Visualization'),
+ dataPlacement: 'top',
+ ngShow: '!inventory.pending_deletion'
+ },
edit: {
label: i18n._('Edit'),
ngClick: 'editInventory(inventory)',
diff --git a/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js b/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js
index 7b22a22dba..9f3ba01557 100644
--- a/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js
+++ b/awx/ui/client/src/inventories-hosts/inventories/list/inventory-list.controller.js
@@ -85,6 +85,10 @@ function InventoriesList($scope,
.finally(() => Wait('stop'));
};
+ $scope.goToGraph = function(inventory){
+ $state.go('inventories.edit.networking', {inventory_id: inventory.id, inventory_name: inventory.name});
+ };
+
$scope.editInventory = function (inventory) {
if(inventory.kind && inventory.kind === 'smart') {
$state.go('inventories.editSmartInventory', {smartinventory_id: inventory.id});
diff --git a/awx/ui/client/src/network-ui/.gitignore b/awx/ui/client/src/network-ui/.gitignore
new file mode 100644
index 0000000000..bc3cac3dd2
--- /dev/null
+++ b/awx/ui/client/src/network-ui/.gitignore
@@ -0,0 +1 @@
+/extracted
diff --git a/awx/ui/client/src/network-ui/CONTRIBUTING.md b/awx/ui/client/src/network-ui/CONTRIBUTING.md
new file mode 100644
index 0000000000..bf3c2b3a28
--- /dev/null
+++ b/awx/ui/client/src/network-ui/CONTRIBUTING.md
@@ -0,0 +1,951 @@
+
+
+Getting Started With Network UI Development
+===========================================
+
+
+**Introduction**
+
+The Networking UI component of AWX works differently from the rest of the AWX
+web UI to support high-scale interactive graphical design of networking
+topologies.
+
+The Networking UI is a virtual graphical canvas where graphical elements are
+drawn upon. This canvas supports panning (scrolling horizontally and
+vertically) and scaling (zooming in and out), dynamic changing of modes, and
+other features that would be very difficult or impossible to implement with
+standard HTML events and rendering.
+
+This interface is more like computer graphics than it is building a styled text
+document with interactive components. A good grasp of Cartesian coordinates,
+trigonometry, and analytic geometry are useful when working with this code.
+
+* See:
+
+**Design choices**
+
+Certain design choices were made to make the UI performant and scale to a large
+number of nodes in a diagram. These include the use of simple ES5 functions for
+better performance over more advanced functions. For instance C-style for-loops
+were many times faster than implementations of `forEach` or iterators which make
+function calls during each iteration. This basic ES5 style should be followed
+throughout the implementation of the Network UI.
+
+**AngularJS**
+
+The Networking UI component uses AngularJS 1.6.x for part of the rendering pipeline
+but it is not a normal AngularJS web application. AngularJS makes use of
+data-binding and watchers which I found do not scale to the number of elements
+we are trying to support in the Networking UI. The Networking UI only uses
+AngularJS for SVG rendering (using AngularJS templates) which does scale
+sufficiently.
+
+
+**AngularJS Controllers**
+
+Instead of creating many AngularJS controllers and directives the networking UI
+uses one big controller to hold the state of the entire UI. Normally this is
+an anti-pattern in AngularJS. Here is was necessary to scale to a large number
+of on-screen elements.
+
+**AngularJS Directives**
+
+* See:
+
+AngularJS directives are used in the networking UI application using the element
+matching style and the `templateUrl` option to include a template. A majority of
+the directives are defined in `network.ui.app.js`.
+
+* See: [network.ui.app.js](network.ui.app.js)
+```
+ .directive('awxNetDeviceDetail', deviceDetail.deviceDetail)
+```
+
+* See: [device.detail.directive.js](device.detail.directive.js)
+```
+function deviceDetail () {
+ return { restrict: 'A', templateUrl: '/static/network_ui/widgets/device_detail.html' };
+}
+```
+
+**AngularJS Templates**
+
+* See:
+
+Normal AngularJS templates are used with the networking UI controller.
+The templates can be found in `/widgets`. Child
+scopes are created for sub-templates using the `ng-repeat` directive.
+
+In this example the `awx-net-link` directive expects a Link model to be
+passed to it. The Link model is defined in the `models.js` file.
+
+* See: [link.directive.js](link.directive.js)
+* See: [link.partial.svg](link.partial.svg)
+
+* See: [network_ui.html](network_ui.partial.svg)
+```
+
+
+
+```
+
+* See: [models.js](models.js)
+```
+function Link(id, from_device, to_device, from_interface, to_interface) {
+ this.id = id;
+ this.from_device = from_device;
+ this.to_device = to_device;
+ this.from_interface = from_interface;
+ this.to_interface = to_interface;
+ this.selected = false;
+ this.remote_selected = false;
+ this.status = null;
+ this.edit_label = false;
+ this.name = "";
+}
+```
+
+The following example sets the toolbox.selected_item value to the variable
+item which the directives used in the child scope expect to be set.
+
+* See: [inventory_toolbox.html](inventory_toolbox.partial.svg)
+```
+
+```
+
+
+**DOM (Document Object Model)**
+
+No state is stored in or attached to the DOM. All state is stored in
+javascript objects attached to the network ui controller.
+
+Direct DOM manipulation should not be used in the network UI unless absolutely
+necessary. JQuery should not be used. The DOM is generated through the use of
+AngularJS templates.
+
+**SVG (Scalable Vector Graphics)**
+
+* See:
+
+The network UI is built as one large SVG element (the SVG canvas) with other
+graphical elements (lines, circles, rectangles, paths, and text) absolutely
+positioned within the outer most SVG element. The browser is not involved with
+layout of the elements within the SVG. Each "widget" in the network UI needs
+to track or calculate its own position on the SVG canvas. The z-level of the
+elements are determined by the draw order on the canvas which is defined
+in `network_ui.partial.svg`. Elements drawn first will be hidden behind
+elements drawn later.
+
+
+
+**Rendering Pipeline**
+
+Event -> Javscript objects -> AngularJS templates -> SVG
+
+AngularJS is used to render the SVG inside the SVG canvas using directives
+and templates. AngularJS is also used to schedule when the SVG canvas will
+be updated. When an input event comes from the user, or an event is received
+over the websocket, javascript objects will be updated according the the network
+UI code. Then AngularJS will be notified that it needs to update the templates
+either automatically for some events or explicitly using `$scope.$apply();` if
+not handled automatically by AngularJS. The templates will render to SVG and be
+included in the DOM for the rest of the AWX UI.
+
+Because the networking UI does not use watchers nor data-binding features of
+AngularJS events flow in one way from event to javascript to angular to SVG.
+Events do not flow backwards through this pipeline.
+
+Clicking on an SVG element will not send the event to that SVG element directly
+from the browser. It must be routed through the network UI code first.
+
+
+**SVG Primer**
+
+SVG uses tags to define graphical elements just like HTML uses tags to define
+text documents. Commonly use tags include g, circle, rect, path, and text.
+SVG elements are absolutely positioned within an SVG canvas. The group tag, g,
+is similar to the div tag in HTML. Text in SVG must be contained in the text
+tag and cannot be outside tags as in HTML.
+
+* See:
+
+Each tag that describes a visual element requires X and Y coordinates as input
+to position that element. These coordinates are relative to position of the SVG
+canvas. The network UI uses the entire page height and width for the SVG canvas
+so that the position on the SVG on the canvas is the same as the position on
+the page.
+
+
+SVG supports graphical transformations on several tags to allow relative
+positioning of sub-elements which makes calculating the X and Y positions
+easier. The network UI uses transformations often for this purpose.
+Transformations that are often used here are the translate, scale, and rotate
+transforms. Translate moves the origin of the coordinate system to a new point
+for the sub-elements. Scale multiplies the size of the units in a coordinate
+system by some factor. Rotate performs a rotation about the origin by some
+number of degrees. These functions are converted to a matrix operation on the
+coordinate system which can be efficiently applied. It is often useful to use
+the transforms to simplify the calculations of X and Y coordinates instead of
+calculating those values in Javascript. Also these transforms make developing
+widgets much easier since we only need to keep up with a single point for the
+widget and all other points can be relatively positioned from that point.
+Hard-coding positions in widget development is the normal case since transforms
+can change the size and position of the widget when the widget is applied to
+the canvas. Only when necessary should we calculate positions of parts of a
+widget in javascript.
+
+* See:
+
+
+SVG paths are a mini-language for defining graphics operations in one tag. It
+is often used to create shapes that are more complex than lines, rectangles,
+and circles. It is very useful for defining arcs.
+
+* See:
+
+**SVG and CSS**
+
+CSS and SVG work really nicely together for setting style, colors, and fonts in SVG.
+The SVG uses different attributes for setting colors than does HTML elements.
+Most SVG elements use `stroke` and `fill` to define the colors and `stroke-width`
+to define the width of lines and curves. The attributes `font-family` and `font-size`
+are used to set the font for text elements in SVG. The network UI uses the Less
+CSS compiler and BEM naming conventions to simplify and organize CSS.
+
+* See: [style.less](style.less)
+* See:
+* See:
+
+**Events**
+
+All mouse and keyboard events are captured by the outer most element of the
+network UI. Mouse movements, mouse clicks, and key presses are all routed by
+the network UI code and not by the browser. This is done to implement
+interactions with the virtual graphical canvas that are not supported by the
+browser. "Simple" things like buttons and text fields have to be handled by
+the network UI code instead of relying on the browser to route the mouse click
+to the appropriate object.
+
+
+The following code captures all the mouse movements, mouse clicks, mouse wheel,
+and touch events and sends them to the corresponding network UI controller functions.
+
+* See: [network_ui.partial.svg](network_ui.partial.svg#L3)
+
+```
+
+```
+
+
+Key events are captured by the following code:
+
+* See: [network.ui.controller.js](network.ui.controller.js)
+
+```
+ $document.bind("keydown", $scope.onKeyDown);
+```
+
+**Event Processing**
+
+This code works as an event processing pipeline where the source of the events
+may be mouse clicks, keystrokes, or messages from the server over the
+websocket. This allows the appropriate processor to handle each event in turn
+or delegate the message to another processor.
+
+The following diagram documents the pipeline processors that handle the events.
+Events are injected into to the pipeline at `Start` and travel through the
+pipeline along the arrows. Events may be handled at a node in the pipeline,
+passed along to the next node, discarded, or transformed into another message
+and sent along the pipeline. For instance `hotkeys_fsm` generates new and
+different type of events based on key presses that are injected at the
+beginning of the pipeline.
+
+
+
+
+**Describing Behavior with Finite State Machines**
+
+To implement complex UI interactions predictably and correctly is a tough
+problem. Often the problem is solved by creating a large library of generic
+reusable components that are rigorously tested and hardened by a large group of
+developers over a period of several years. Eventually the myriad bugs are
+hammered out at great expense. Only then can the UI components be reliably
+used. This code does not follow that approach.
+
+The workflows this code supports require very specific UI components that are
+not found in generic libraries. The interactions we want to support are not
+available in generic libraries. This code develops from scratch only the
+components that are necessary to implement the workflows of designing and
+operating networks of devices.
+
+This code defines those elements using finite state machines to process the
+events from user input and other software components. Programming with finite
+state machines allows us to define formally complex behavior that would
+normally be informally defined by branches, functions, object interactions, and
+object inheritance. Formal definition eliminates much of the unexpected
+behavior that causes defects in the software.
+
+* See:
+
+Finite state machines can be represented as a directed graph of labeled nodes and labeled edges
+which can be both be represented visually and in machine readable code.
+
+The network UI uses finite state machines to describe what happens when the software receives
+an input.
+
+**Link FSM**
+
+
+
+For example the link FSM describes how to connect devices with links. The FSM
+diagram above maps out the states and events that will select a device to connect and another
+device to connect to. FSMs traditionally start in the `Start` state. We get a free transition
+to the `Ready` state by handling the special event called `start` and changing state to `Ready`.
+Then when the `NewLink` event is received from a hot key or button click the FSM changes
+state to the `Selecting` state. On a `MouseUp` event the FSM changes to the the `Connecting` state
+if the mouse is over a device icon otherwise it stays in the `Selecting` state. In the `Connecting`
+state the FSM changes to the `Connected` state when it receives a `MouseUp` event and the
+mouse is over another device otherwise it goes back to the `Ready` state since the user cancelled
+the connecting operation. Finally in the `Connected` state the FSM changes to the `Ready` state
+for free using the `start` event so that the user can connect another set of devices.
+
+* See: [designs/link.yml](designs/link.yml)
+* See: [link.js](link.js)
+
+The link FSM diagram has an equivalent machine readable representation in `designs/link.yml`. This
+representation is useful for comparing the current implementation in `link.js` to the design to see if they
+are out-of-sync. If they are out-of-sync either the design or the implementation can be updated depending
+on if you are changing the design or implementation first.
+
+Tools are provided to facilitate the design-first and the implementation-first workflows.
+
+**Design-First Workflow**
+
+In the design-first workflow, first change the design and then update the
+implementation to match. In this workflow we use the
+[fsm-designer-svg](https://github.com/benthomasson/fsm-designer-svg) tool to
+change the FSM diagram, then export the FSM to a file, then generate a skeleton
+of the javascript code that implements the FSM. Then development of the logic
+inside the event handlers can begin with a clear understanding of the state of
+the system and what that event handler should do.
+
+Use `tools/fsm_generate_diffs.py` to generate the new skeleton code:
+
+```
+ ./tools/fsm_generate_diffs.py designs/link.yml ./link.js
+```
+
+This will print out code for additional states or transitions needed in the implementation.
+Copy those lines into the implementation code and fill out the event handler functions.
+
+
+**Implementation-First Workflow**
+
+In the implementation-first workflow, first change the code and then update the
+design to reflect the changes. This workflow is useful when the design doesn't
+survive its impact with reality and the code adds additional requirements to
+the design. Often in usabilty testing we find that we forgot to consider
+handling a certain interaction or user input in a state. We can quickly add
+that transition to the code and test it out. Once confirmed that the interaction
+is correct we can update the design and run `./tools/fsm-diff` to make sure the two
+are in sync.
+
+
+Use `./extract.js` and `tools/fsm-diff` to compare the implementation to the design
+and add any additional transitions to the FSM design.
+
+```
+ ./extract.js link.js > ./extracted/link.yml
+ ./tools/fsm-diff designs/link.yml extracted/link.yml
+```
+
+
+**Validating That Design Matches Implementation**
+
+Use the `make extract` and `make diff` Makefile targets to do a mass extact of the
+FSMs from the implementation and a mass comparison against the designs. Take
+note of any differences between design and implementation and update the appropriate
+files as outlined in the workflows above.
+
+```
+ make extract; make diff
+```
+
+
+**Finite State Machine Implementation**
+
+The implementation of a finite state machine in the network UI is split into
+two parts: the declaration of the states and the event-handlers which may cause
+FSM transitions using `controller.changeState`.
+
+**FSM States**
+
+* See:
+* See:
+
+States are implemented using an object-oriented style in ES5 using the
+flyweight and singleton patterns. This means that the state objects store no
+information on themselves and that there is only one instance of each state
+class. All states should provide a `start` and `end` function which will be
+called when a FSM state is entered and exited respectively. Subclassing
+[fsm.State](fsm.js#L36) will provide empty `start` and `end` functions that
+can be overridden as necessary.
+
+* See: [fsm.js](fsm.js#L2)
+
+The state variable is stored on another object called an FSMController (which
+should not be confused with an AngularJS controller). The FSMController holds
+all the state for each FSM instance. If you need more than one copy of an FSM
+(for buttons for instance) use more than one instance of FSMController and
+pass the same FSM starting state to their constructor e.g. `button.Start`.
+Variables other than `state` should not be stored on the FSMController. A
+special variable named `scope` is useful for that. The scope can be used
+to hold arbitrary data that the FSM code will use in addition to the messages
+in the event handlers. In the network UI often the `scope` is a reference
+to the network UI AngularJS controller's scope. In the case of a button
+the scope is a reference to the `Button` model.
+
+* See: [models.js](models.js#302)
+
+The following code creates a new instance of `FSMController` using the
+`Button` model as the scope and the `button.Start` state as the initial
+state.
+
+```
+ this.fsm = new fsm.FSMController(this, button.Start, null);
+```
+
+* See: [link.js](link.js#L40)
+
+This code block defines the `_Selecting` class in ES5 style and uses the
+`inherits` NPM module to define that the class is a subclass of `_State`. We
+also create a single instance (a singleton) of this class named `Selecting`.
+
+```
+ function _Selecting () {
+ this.name = 'Selecting';
+ }
+ inherits(_Selecting, _State);
+ var Selecting = new _Selecting();
+ exports.Selecting = Selecting;
+```
+
+**FSM Event Handlers and Transitions**
+
+After all the states are defined the event handlers for those state classes can be defined.
+We do this to prevent forward references in the file.
+
+* See: [link.js](link.js#L134)
+
+In this code we define an event handler for the `MouseUp` event on the `Selecting` state. This
+code should select a single device if the mouse is over that device. It should store
+that device somewhere and change to the `Connecting` state. The code below creates a new
+`Link` model and stores the `selected_device` in that object. The `new_link` object is
+stored in the `controller.scope` for later use in the FSM. Finally the event handler changes
+state using `controller.changeState` to change the state of the FSM to `Connecting`.
+
+Event handlers must start with the prefix of `on` and a suffix of the name of the messsage
+type. The special functions `start` and `end` do not follow this rule nor do
+they receive a message.
+
+The event handler must also define its `transitions` as a list so that `./extract.js` can
+find them.
+
+```
+ _Selecting.prototype.onMouseUp = function (controller) {
+
+ var selected_device = controller.scope.select_items(false).last_selected_device;
+ if (selected_device !== null) {
+ controller.scope.new_link = new models.Link(controller.scope.link_id_seq(), selected_device, null, null, null, true);
+ controller.scope.links.push(controller.scope.new_link);
+ controller.changeState(Connecting);
+ }
+ };
+ _Selecting.prototype.onMouseUp.transitions = ['Connecting'];
+
+```
+
+**FSM Designs**
+
+All the finite state machines for the network UI have been designed using the
+[fsm-designer-svg](https://github.com/benthomasson/fsm-designer-svg) tool
+and their designs are stored in the `designs` directory.
+
+* See: [designs/README.md](designs/README.md)
+
+
+**Data Models**
+
+There are two types of data structures used in the network UI: messages and
+models. Models are used for long-lived data that is used to render the UI
+whereas messages are used for ephemeral data that is passed from one part of
+the system to another. Models may be unpacked or serialized into messages that
+are sent to other FSMControllers in the client or sent over a websocket to the
+server.
+
+* See: [models.js](models.js)
+
+The models defined in [models.js](models.js) are:
+
+* Device - a networking device i.e. a router, a switch, or a host
+* Interface - a networking interface
+* Link - a connection between interfaces
+* Button - a UI button
+* ToggleButton - a togglable UI button
+* Task - a playbook task
+* Group - a grouping of devices
+* ToolBox - a UI element for holding things that can be placed on the virtual canvas
+* Configuration - a configuration for a device
+* Process - an application running on a device
+* Stream - a flow of data between applications
+
+
+**Message Types**
+
+Message types define the structure of the data that is passed between the server
+and the client and between different parts of the client. This provides a known and
+well defined data structure that can be counted up on the code.
+
+* See: [messages.js](messages.js)
+
+The messages defined are [messages.js](messages.js):
+
+* DeviceMove - Device has changed x,y position
+* DeviceCreate - A device was created
+* DeviceDestroy - A device was destroyed
+* DeviceLabelEdit - The label of a device was changed
+* DeviceSelected - A device was selected
+* DeviceUnSelected - A device was unselected
+* InterfaceCreate - An interface was created
+* InterfaceLabelEdit - The label of an interface was changed
+* LinkLabelEdit - The label of a link was changed
+* LinkCreate - A link was created
+* LinkDestroy - A link was destroyed
+* LinkSelected - A link was selected
+* LinkUnSelected - A link was unselected
+* Undo - Undo the last operation
+* Redo - Redo the last undone operation
+* Deploy - Call the deploy playbook
+* Destroy - Call the destroy playbook
+* Discover - Call the discover playbook
+* Layout - Call the layout function
+* MultipleMessage - A collection of messages that should be handled together
+* Coverage - A coverage report
+* MouseEvent - A generic mouse event
+* MouseWheelEvent - A mouse wheel event
+* KeyEvent - A key press event
+* TouchEvent - A touch screen event
+* StartRecording - Start recording user interactions
+* StopRecording - Stop recording user interactions
+* ViewPort - Update the view port onto the virtual canvas
+* NewDevice - Request for a new device
+* PasteDevice - Paste a device from a toolbox
+* PasteProcess - Paste a process from a toolbox
+* NewGroup - Request for a new group
+* PasteGroup - Paste a group from a toolbox
+* PasteRack - Paste a rack from a toolbox
+* PasteSite - Paste a site from a toolbox
+* CopySite - Copy a stie to a toolbox
+* GroupMove - A group has changed its x, y coordinates
+* GroupCreate - A new group was created
+* GroupDestroy - A group was destroyed
+* GroupLabelEdit - The label for a group was changed
+* GroupSelected - A group was selected
+* GroupUnSelected - A group was unselected
+* GroupMembership - The device membership of a group changed
+* TableCellEdit - A table cell was chaged
+* ProcessCreate - A new process was created
+* StreamCreate - A new stream was created
+* StreamDestroy - A stream was destroyed
+* StreamLabelEdit - The label of a stream was changed
+* StreamSelected - A stream was selected
+* StreamUnSelected - A stream was unselected
+
+
+Widget Development
+==================
+
+When developing a new UI widget follow this process:
+
+For a widget named `new widget` do this:
+
+* Add a template in `widgets` for the new widget with name `new_widget.html`
+* Add a directive that loads that template in `src` with name `new.widget.directive.js`
+* Register the directive with the network UI application in `src/network.ui.app.js` using name `awxNetNewWidget`
+* Add a tag that loads the directive into an existing template in `widgets`. If you are not sure add it to `widgets/network_ui.html`.
+* Test that the directive is loaded when the page renders in a browser
+* Iterate on the template for the new widget until the UI look matches the mockup
+* Design the interaction behavior using [fsm-designer-svg](https://github.com/benthomasson/fsm-designer-svg)
+* Export the FSM design to `designs` in a file named `designs/new_widget.yml`
+* Create a new empty FSM implementation file in `src` named `src/new.wiget.fsm.js`
+* Use the `./tools/fsm_generate_diffs.py` tool to generate the skeleton for the new FSM implementation
+* Decide if you need any new data structures for your UI widget. If so, add them to `src/models.js`.
+* Decide if you need any new messages to communicate between the UI and the server or between pieces of the UI.
+ If so, add them to `src/messages.js`
+* Add the FSM implementation to a FSMController in `src/network.ui.controller.js`
+* Write the logic in the event handlers to update the models, send any messages, and change states according to the design.
+* Test the interaction manually in a browser
+* Iterate on changing the event handlers until the desired interaction is acheived
+* Update the design to match the implementation
+
+**Widget Development Example**
+
+This example follows development of the inventory toolbox widget.
+
+* Add a template in `widgets` for the new widget with name [inventory_toolbox.partial.svg](inventory_toolbox.partial.svg)
+
+```
+
+
+
+
+ ...
+
+```
+
+* Add a directive that loads that template in `src/network-ui` with name [inventory.toolbox.directive.js](inventory.toolbox.directive.js)
+
+```
+ /* Copyright (c) 2017 Red Hat, Inc. */
+
+ function inventoryToolbox () {
+ return { restrict: 'A', templateUrl: '/static/network_ui/widgets/inventory_toolbox.html' };
+ }
+ exports.inventoryToolbox = inventoryToolbox;
+```
+
+
+* Register the directive with the network UI application in [network.ui.app.js](network.ui.app.js#L61) using name `awxNetInventoryToolbox`
+
+```
+...
+ var inventoryToolbox = require('./inventory.toolbox.directive.js');
+...
+ .directive('awxNetInventoryToolbox', inventoryToolbox.inventoryToolbox)
+...
+```
+
+* Add a tag that loads the directive into an existing template in `src/network-ui` in [network_ui.partial.svg](network_ui.partial.svg#L94)
+
+```
+
+```
+
+* Test that the directive is loaded when the page renders in a browser
+* Iterate on the template for the new widget until the UI look matches the mockup
+* Design the interaction behavior using [fsm-designer-svg](https://github.com/benthomasson/fsm-designer-svg)
+
+
+
+* Export the FSM design to `designs` in a file named `designs/toolbox.yml`
+
+```
+ finite_state_machine_id: 14
+ name: toolbox
+ states:
+ - id: 2
+ label: Selected
+ x: 1180
+ y: 959
+ - id: 6
+ label: Move
+ x: 1409
+ y: 741
+ - id: 3
+ label: Ready
+ x: 892
+ y: 429
+ - id: 4
+ label: Scrolling
+ x: 567
+ y: 431
+ - id: 5
+ label: Start
+ x: 892
+ y: 216
+ - id: 7
+ label: Selecting
+ x: 888
+ y: 710
+ - id: 1
+ label: Dropping
+ x: 1358
+ y: 431
+ transitions:
+ - from_state: Selecting
+ label: onMouseDown
+ to_state: Selected
+ - from_state: Selected
+ label: onMouseMove
+ to_state: Move
+ - from_state: Selecting
+ label: onMouseDown
+ to_state: Ready
+ - from_state: Selected
+ label: onMouseUp
+ to_state: Ready
+ - from_state: Dropping
+ label: start
+ to_state: Ready
+ - from_state: Start
+ label: start
+ to_state: Ready
+ - from_state: Scrolling
+ label: onMouseWheel
+ to_state: Ready
+ - from_state: Ready
+ label: onMouseWheel
+ to_state: Scrolling
+ - from_state: Ready
+ label: onMouseDown
+ to_state: Selecting
+ - from_state: Move
+ label: onMouseUp
+ to_state: Dropping
+```
+
+* Create a new empty FSM implementation file in `src/network-ui` named `toolbox.fsm.js`
+
+```
+ touch toolbox.fsm.js
+```
+
+* Use the `./tools/fsm_generate_diffs.py` tool to generate the skeleton for the new FSM implementation
+
+```
+ ./tools/fsm_generate_diffs.py designs/toolbox.yml src/toolbox.fsm.js --append
+```
+
+
+```
+ var inherits = require('inherits');
+ var fsm = require('./fsm.js');
+
+ function _State () {
+ }
+ inherits(_State, fsm._State);
+
+
+ function _Start () {
+ this.name = 'Start';
+ }
+ inherits(_Start, _State);
+ var Start = new _Start();
+ exports.Start = Start;
+
+ function _Selected () {
+ this.name = 'Selected';
+ }
+ inherits(_Selected, _State);
+ var Selected = new _Selected();
+ exports.Selected = Selected;
+
+ function _Dropping () {
+ this.name = 'Dropping';
+ }
+ inherits(_Dropping, _State);
+ var Dropping = new _Dropping();
+ exports.Dropping = Dropping;
+
+ function _Ready () {
+ this.name = 'Ready';
+ }
+ inherits(_Ready, _State);
+ var Ready = new _Ready();
+ exports.Ready = Ready;
+
+ function _Selecting () {
+ this.name = 'Selecting';
+ }
+ inherits(_Selecting, _State);
+ var Selecting = new _Selecting();
+ exports.Selecting = Selecting;
+
+ function _Move () {
+ this.name = 'Move';
+ }
+ inherits(_Move, _State);
+ var Move = new _Move();
+ exports.Move = Move;
+
+ function _Scrolling () {
+ this.name = 'Scrolling';
+ }
+ inherits(_Scrolling, _State);
+ var Scrolling = new _Scrolling();
+ exports.Scrolling = Scrolling;
+
+
+
+
+ _Start.prototype.start = function (controller) {
+
+ controller.changeState(Ready);
+
+ };
+ _Start.prototype.start.transitions = ['Ready'];
+
+
+
+ _Selected.prototype.onMouseMove = function (controller) {
+
+ controller.changeState(Move);
+
+ };
+ _Selected.prototype.onMouseMove.transitions = ['Move'];
+
+ _Selected.prototype.onMouseUp = function (controller) {
+
+ controller.changeState(Ready);
+
+ };
+ _Selected.prototype.onMouseUp.transitions = ['Ready'];
+
+
+
+ _Dropping.prototype.start = function (controller) {
+
+ controller.changeState(Ready);
+
+ };
+ _Dropping.prototype.start.transitions = ['Ready'];
+
+
+
+ _Ready.prototype.onMouseDown = function (controller) {
+
+ controller.changeState(Selecting);
+
+ };
+ _Ready.prototype.onMouseDown.transitions = ['Selecting'];
+
+ _Ready.prototype.onMouseWheel = function (controller) {
+
+ controller.changeState(Scrolling);
+
+ };
+ _Ready.prototype.onMouseWheel.transitions = ['Scrolling'];
+
+
+
+ _Selecting.prototype.onMouseDown = function (controller) {
+
+ controller.changeState(Ready);
+
+ controller.changeState(Selected);
+
+ };
+ _Selecting.prototype.onMouseDown.transitions = ['Ready', 'Selected'];
+
+
+
+ _Move.prototype.onMouseUp = function (controller) {
+
+ controller.changeState(Dropping);
+
+ };
+ _Move.prototype.onMouseUp.transitions = ['Dropping'];
+
+
+
+ _Scrolling.prototype.onMouseWheel = function (controller) {
+
+ controller.changeState(Ready);
+
+ };
+ _Scrolling.prototype.onMouseWheel.transitions = ['Ready'];
+ };
+ _Ready.prototype.onMouseWheel.transitions = ['Scrolling'];
+
+
+
+ _Selecting.prototype.onMouseDown = function (controller) {
+
+ controller.changeState(Ready);
+
+ controller.changeState(Selected);
+
+ };
+ _Selecting.prototype.onMouseDown.transitions = ['Ready', 'Selected'];
+
+
+
+ _Move.prototype.onMouseUp = function (controller) {
+
+ controller.changeState(Dropping);
+
+ };
+ _Move.prototype.onMouseUp.transitions = ['Dropping'];
+
+
+
+ _Scrolling.prototype.onMouseWheel = function (controller) {
+
+ controller.changeState(Ready);
+
+ };
+ _Scrolling.prototype.onMouseWheel.transitions = ['Ready'];
+
+```
+
+* Decide if you need any new data structures for your UI widget. If so, add them to [src/models.js](src/models.js#L608).
+
+```
+ function ToolBox(id, name, type, x, y, width, height) {
+ this.id = id;
+ this.name = name;
+ this.type = type;
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ this.items = [];
+ this.spacing = 200;
+ this.scroll_offset = 0;
+ this.selected_item = null;
+ this.enabled = true;
+ }
+ exports.ToolBox = ToolBox;
+```
+
+* Decide if you need any new messages to communicate between the UI and the server or between pieces of the UI.
+ If so, add them to [messages.js](messages.js#L251)
+
+```
+ function PasteDevice(device) {
+ this.device = device;
+ }
+ exports.PasteDevice = PasteDevice;
+```
+
+* Write the logic in the event handlers to update the models, send any messages, and change states according to the design.
+
+See: [toolbox.fsm.js](toolbox.fsm.js)
+
+* Add the FSM implementation to a FSMController in [network.ui.controller.js](network.ui.controller.js#L145)
+
+```
+ $scope.inventory_toolbox_controller = new fsm.FSMController($scope, toolbox_fsm.Start, $scope.app_toolbox_controller);
+```
+
+* Test the interaction manually in a browser
+* Iterate on changing the event handlers until the desired interaction is achieved
+* Update the design to match the implementation
diff --git a/awx/ui/client/src/network-ui/Makefile b/awx/ui/client/src/network-ui/Makefile
new file mode 100644
index 0000000000..58991a7919
--- /dev/null
+++ b/awx/ui/client/src/network-ui/Makefile
@@ -0,0 +1,19 @@
+
+
+.PHONY: check extract
+
+FSMS = animation time test mode buttons button toolbox site rack group stream link details.panel move device.detail view keybindings hotkeys null
+
+
+extract:
+ mkdir -p extracted
+ for fsm in $(FSMS); do \
+ ./extract.js ./$${fsm}.fsm.js > extracted/$${fsm}.yml; \
+ done
+
+
+check: extract
+ for fsm in $(FSMS); do \
+ ./tools/fsm-diff ../../../../network_ui/designs/$$fsm.yml extracted/$$fsm.yml; \
+ ./tools/copy-layout.py ../../../../network_ui/designs/$$fsm.yml extracted/$$fsm.yml; \
+ done
diff --git a/awx/ui/client/src/network-ui/animation.fsm.js b/awx/ui/client/src/network-ui/animation.fsm.js
new file mode 100644
index 0000000000..dc1e737d50
--- /dev/null
+++ b/awx/ui/client/src/network-ui/animation.fsm.js
@@ -0,0 +1,81 @@
+/* Copyright (c) 2018 Benjamin Thomasson */
+/* Copyright (c) 2018 Red Hat, Inc. */
+
+var inherits = require('inherits');
+var fsm = require('./fsm.js');
+
+
+function _Start () {
+ this.name = 'Start';
+}
+inherits(_Start, fsm._State);
+var Start = new _Start();
+exports.Start = Start;
+
+function _Completed () {
+ this.name = 'Completed';
+}
+inherits(_Completed, fsm._State);
+var Completed = new _Completed();
+exports.Completed = Completed;
+
+function _Cancelled () {
+ this.name = 'Cancelled';
+}
+inherits(_Cancelled, fsm._State);
+var Cancelled = new _Cancelled();
+exports.Cancelled = Cancelled;
+
+function _Running () {
+ this.name = 'Running';
+}
+inherits(_Running, fsm._State);
+var Running = new _Running();
+exports.Running = Running;
+
+
+_Start.prototype.start = function (controller) {
+
+ controller.changeState(Running);
+};
+_Start.prototype.start.transitions = ['Running'];
+
+_Running.prototype.start = function (controller) {
+
+ controller.scope.interval = setInterval(function () {
+ controller.scope.frame_number = controller.scope.frame_number_seq();
+ if (!controller.scope.active) {
+ return;
+ }
+ if (controller.scope.frame_number > controller.scope.steps) {
+ controller.scope.fsm.handle_message('AnimationCompleted');
+ return;
+ }
+ controller.scope.callback(controller.scope);
+ controller.scope.scope.$apply();
+ }, controller.scope.frame_delay);
+};
+
+_Running.prototype.onAnimationCancelled = function (controller) {
+
+ controller.changeState(Cancelled);
+
+};
+_Running.prototype.onAnimationCancelled.transitions = ['Cancelled'];
+
+_Running.prototype.onAnimationCompleted = function (controller) {
+
+ controller.changeState(Completed);
+
+};
+_Running.prototype.onAnimationCompleted.transitions = ['Completed'];
+
+_Completed.prototype.start = function (controller) {
+ controller.scope.active = false;
+ clearInterval(controller.scope.interval);
+};
+
+_Cancelled.prototype.start = function (controller) {
+ controller.scope.active = false;
+ clearInterval(controller.scope.interval);
+};
diff --git a/awx/ui/client/src/network-ui/animations.js b/awx/ui/client/src/network-ui/animations.js
new file mode 100644
index 0000000000..74ead1a423
--- /dev/null
+++ b/awx/ui/client/src/network-ui/animations.js
@@ -0,0 +1,23 @@
+
+function scale_animation (scope) {
+
+ var initial_height = ((1 / scope.data.current_scale) - 1);
+ var height = (scope.data.end_height - initial_height) * (scope.frame_number / scope.steps) + initial_height;
+ scope.data.scope.current_scale = 1 / (1 + height);
+ scope.data.scope.updatePanAndScale();
+ scope.data.scope.$emit('awxNet-UpdateZoomWidget', scope.data.scope.current_scale, scope.data.updateZoomBoolean);
+}
+exports.scale_animation = scale_animation;
+
+function pan_animation (scope) {
+ var incr_x = (scope.data.x2 - scope.data.x1) / scope.steps;
+ var incr_y = (scope.data.y2 - scope.data.y1) / scope.steps;
+ var v_x = incr_x * scope.frame_number + scope.data.x1;
+ var v_y = incr_y * scope.frame_number + scope.data.y1;
+ var p = scope.data.scope.to_pan(v_x, v_y);
+ scope.data.scope.panX = p.x + scope.data.scope.graph.width/2;
+ scope.data.scope.panY = p.y + scope.data.scope.graph.height/2;
+ scope.data.scope.first_channel.send("PanChanged", {});
+ scope.data.scope.updatePanAndScale();
+}
+exports.pan_animation = pan_animation;
diff --git a/awx/ui/client/src/network-ui/button.fsm.js b/awx/ui/client/src/network-ui/button.fsm.js
new file mode 100644
index 0000000000..f32839ecdc
--- /dev/null
+++ b/awx/ui/client/src/network-ui/button.fsm.js
@@ -0,0 +1,107 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+var inherits = require('inherits');
+var fsm = require('./fsm.js');
+
+function _State () {
+}
+inherits(_State, fsm._State);
+
+function _Ready () {
+ this.name = 'Ready';
+}
+inherits(_Ready, _State);
+var Ready = new _Ready();
+exports.Ready = Ready;
+
+function _Start () {
+ this.name = 'Start';
+}
+inherits(_Start, _State);
+var Start = new _Start();
+exports.Start = Start;
+
+function _Clicked () {
+ this.name = 'Clicked';
+}
+inherits(_Clicked, _State);
+var Clicked = new _Clicked();
+exports.Clicked = Clicked;
+
+function _Pressed () {
+ this.name = 'Pressed';
+}
+inherits(_Pressed, _State);
+var Pressed = new _Pressed();
+exports.Pressed = Pressed;
+function _Disabled () {
+ this.name = 'Disabled';
+}
+inherits(_Disabled, _State);
+var Disabled = new _Disabled();
+exports.Disabled = Disabled;
+
+
+
+// Begin ready state
+_Ready.prototype.onMouseDown = function (controller) {
+
+ controller.changeState(Pressed);
+
+};
+_Ready.prototype.onMouseDown.transitions = ['Pressed'];
+
+_Ready.prototype.start = function (controller) {
+
+ controller.scope.enabled = true;
+
+};
+
+_Ready.prototype.onDisable = function (controller) {
+
+ controller.changeState(Disabled);
+
+};
+_Ready.prototype.onDisable.transitions = ['Disabled'];
+// end ready state
+
+
+_Start.prototype.start = function (controller) {
+
+ controller.changeState(Ready);
+
+};
+_Start.prototype.start.transitions = ['Ready'];
+
+
+_Clicked.prototype.start = function (controller) {
+
+ controller.scope.is_pressed = false;
+ controller.changeState(Ready);
+ controller.scope.callback(controller.scope);
+};
+_Clicked.prototype.start.transitions = ['Ready'];
+
+
+_Pressed.prototype.start = function (controller) {
+ controller.scope.is_pressed = true;
+};
+
+_Pressed.prototype.onMouseUp = function (controller) {
+
+ controller.changeState(Clicked);
+
+};
+_Pressed.prototype.onMouseUp.transitions = ['Clicked'];
+
+_Disabled.prototype.onEnable = function (controller) {
+
+ controller.changeState(Ready);
+
+};
+_Disabled.prototype.onEnable.transitions = ['Ready'];
+
+_Disabled.prototype.start = function (controller) {
+
+ controller.scope.enabled = false;
+
+};
diff --git a/awx/ui/client/src/network-ui/buttons.fsm.js b/awx/ui/client/src/network-ui/buttons.fsm.js
new file mode 100644
index 0000000000..2a01a0bd3a
--- /dev/null
+++ b/awx/ui/client/src/network-ui/buttons.fsm.js
@@ -0,0 +1,96 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+var inherits = require('inherits');
+var fsm = require('./fsm.js');
+
+function _State () {
+}
+inherits(_State, fsm._State);
+
+
+function _Ready () {
+ this.name = 'Ready';
+}
+inherits(_Ready, _State);
+var Ready = new _Ready();
+exports.Ready = Ready;
+
+function _Start () {
+ this.name = 'Start';
+}
+inherits(_Start, _State);
+var Start = new _Start();
+exports.Start = Start;
+
+function _ButtonPressed () {
+ this.name = 'ButtonPressed';
+}
+inherits(_ButtonPressed, _State);
+var ButtonPressed = new _ButtonPressed();
+exports.ButtonPressed = ButtonPressed;
+
+
+
+
+_Ready.prototype.onMouseDown = function (controller, msg_type, $event) {
+
+ var i = 0;
+ var buttons = controller.scope.all_buttons;
+ var button = null;
+ for (i = 0; i < buttons.length; i++) {
+ button = buttons[i];
+ if (button.is_selected(controller.scope.mouseX, controller.scope.mouseY)) {
+ button.fsm.handle_message(msg_type, $event);
+ controller.changeState(ButtonPressed);
+ break;
+ }
+ button = null;
+ }
+ if (button === null) {
+ controller.delegate_channel.send(msg_type, $event);
+ }
+
+};
+_Ready.prototype.onMouseDown.transitions = ['ButtonPressed'];
+
+_Ready.prototype.onMouseMove = function (controller, msg_type, $event) {
+
+ if (!controller.scope.hide_buttons) {
+
+ var i = 0;
+ var buttons = controller.scope.all_buttons;
+ var button = null;
+ for (i = 0; i < buttons.length; i++) {
+ button = buttons[i];
+ button.mouse_over = false;
+ if (button.is_selected(controller.scope.mouseX, controller.scope.mouseY)) {
+ button.mouse_over = true;
+ }
+ }
+ }
+
+ controller.delegate_channel.send(msg_type, $event);
+};
+
+
+_Start.prototype.start = function (controller) {
+
+ controller.changeState(Ready);
+
+};
+_Start.prototype.start.transitions = ['Ready'];
+
+
+
+_ButtonPressed.prototype.onMouseUp = function (controller, msg_type, $event) {
+
+ var i = 0;
+ var buttons = controller.scope.all_buttons;
+ var button = null;
+ for (i = 0; i < buttons.length; i++) {
+ button = buttons[i];
+ button.fsm.handle_message(msg_type, $event);
+ }
+ controller.changeState(Ready);
+
+};
+_ButtonPressed.prototype.onMouseUp.transitions = ['Ready'];
diff --git a/awx/ui/client/src/network-ui/context.menu.button.directive.js b/awx/ui/client/src/network-ui/context.menu.button.directive.js
new file mode 100644
index 0000000000..8e984109cf
--- /dev/null
+++ b/awx/ui/client/src/network-ui/context.menu.button.directive.js
@@ -0,0 +1,15 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+
+const templateUrl = require('~network-ui/context_menu_button.partial.svg');
+
+function contextMenuButton () {
+ return {
+ restrict: 'A',
+ templateUrl,
+ scope: {
+ contextMenuButton: '=',
+ contextMenu: '='
+ }
+ };
+}
+exports.contextMenuButton = contextMenuButton;
diff --git a/awx/ui/client/src/network-ui/context.menu.directive.js b/awx/ui/client/src/network-ui/context.menu.directive.js
new file mode 100644
index 0000000000..bf92043d34
--- /dev/null
+++ b/awx/ui/client/src/network-ui/context.menu.directive.js
@@ -0,0 +1,14 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+
+const templateUrl = require('~network-ui/context_menu.partial.svg');
+
+function contextMenu () {
+ return {
+ restrict: 'A',
+ templateUrl,
+ scope: {
+ contextMenu: '='
+ }
+ };
+}
+exports.contextMenu = contextMenu;
diff --git a/awx/ui/client/src/network-ui/context_menu.partial.svg b/awx/ui/client/src/network-ui/context_menu.partial.svg
new file mode 100644
index 0000000000..883b532c04
--- /dev/null
+++ b/awx/ui/client/src/network-ui/context_menu.partial.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/src/network-ui/context_menu_button.partial.svg b/awx/ui/client/src/network-ui/context_menu_button.partial.svg
new file mode 100644
index 0000000000..2c619cfb4d
--- /dev/null
+++ b/awx/ui/client/src/network-ui/context_menu_button.partial.svg
@@ -0,0 +1,19 @@
+
+
+
+{{contextMenuButton.name}}
+
+{{contextMenuButton.name}}
+
diff --git a/awx/ui/client/src/network-ui/cursor.directive.js b/awx/ui/client/src/network-ui/cursor.directive.js
new file mode 100644
index 0000000000..340640efb0
--- /dev/null
+++ b/awx/ui/client/src/network-ui/cursor.directive.js
@@ -0,0 +1,8 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+
+const templateUrl = require('~network-ui/cursor.partial.svg');
+
+function cursor () {
+ return { restrict: 'A', templateUrl};
+}
+exports.cursor = cursor;
diff --git a/awx/ui/client/src/network-ui/cursor.partial.svg b/awx/ui/client/src/network-ui/cursor.partial.svg
new file mode 100644
index 0000000000..a0f7233bd7
--- /dev/null
+++ b/awx/ui/client/src/network-ui/cursor.partial.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/awx/ui/client/src/network-ui/debug.directive.js b/awx/ui/client/src/network-ui/debug.directive.js
new file mode 100644
index 0000000000..c593f0be5b
--- /dev/null
+++ b/awx/ui/client/src/network-ui/debug.directive.js
@@ -0,0 +1,20 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+
+const templateUrl = require('~network-ui/debug.partial.svg');
+
+function debug () {
+ return {
+ restrict: 'A',
+ templateUrl,
+ link: function(){
+ $('.NetworkUI__debug-text').each(function(index, option){
+ let startingY = 15;
+ let offset = 20;
+ let y = startingY + (index * offset);
+ option.setAttribute('y', y);
+ });
+ }
+ };
+}
+
+exports.debug = debug;
diff --git a/awx/ui/client/src/network-ui/debug.partial.svg b/awx/ui/client/src/network-ui/debug.partial.svg
new file mode 100644
index 0000000000..8a852a9bd3
--- /dev/null
+++ b/awx/ui/client/src/network-ui/debug.partial.svg
@@ -0,0 +1,60 @@
+
+
+
+
+
+ view_port.x: {{view_port.x}}
+ view_port.y: {{view_port.y}}
+ view_port.width: {{view_port.width}}
+ view_port.height: {{view_port.height}}
+ width: {{graph.width}}
+ height: {{graph.height}}
+ rc: {{graph.right_column}}
+ Mouse down: {{onMouseDownResult}}
+ Mouse up: {{onMouseUpResult}}
+ Mouse move: {{onMouseMoveResult}}
+ Mouse over: {{onMouseOverResult}}
+ Mouse enter: {{onMouseEnterResult}}
+ Mouse leave: {{onMouseLeaveResult}}
+ Current scale: {{current_scale.toFixed(4)}}
+ Pan X: {{panX.toFixed(2)}}
+ Pan Y: {{panY.toFixed(2)}}
+ View State: {{view_controller.state.name}}
+ Mouse X: {{mouseX.toFixed(2)}}
+ Mouse Y: {{mouseY.toFixed(2)}}
+ Scaled X: {{scaledX.toFixed(2)}}
+ Scaled Y: {{scaledY.toFixed(2)}}
+ Key: {{last_key}}
+ Key Code: {{last_key_code}}
+ Move State: {{move_controller.state.name}}
+ Move Readonly State: {{move_readonly_controller.state.name}}
+ Selected devices: {{selected_devices.length}}
+ Selected links: {{selected_links.length}}
+ Link State: {{link_controller.state.name}}
+ Buttons State: {{buttons_controller.state.name}}
+ Time State: {{time_controller.state.name}}
+ Time Pointer: {{time_pointer}}
+ Group State: {{group_controller.state.name}}
+ Hotkeys State: {{hotkeys_controller.state.name}}
+ Mode State: {{mode_controller.state.name}}
+ Device Detail State: {{device_detail_controller.state.name}}
+ Site State: {{site_controller.state.name}}
+ Rack State: {{rack_controller.state.name}}
+ Stream State: {{stream_controller.state.name}}
+ App Toolbox State: {{app_toolbox_controller.state.name}}
+ Inventory Toolbox State: {{inventory_toolbox_controller.state.name}}
+ Rack Toolbox State: {{rack_toolbox_controller.state.name}}
+ Site Toolbox State: {{site_toolbox_controller.state.name}}
+
+
+
+
+
diff --git a/awx/ui/client/src/network-ui/default.directive.js b/awx/ui/client/src/network-ui/default.directive.js
new file mode 100644
index 0000000000..16c469f188
--- /dev/null
+++ b/awx/ui/client/src/network-ui/default.directive.js
@@ -0,0 +1,8 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+
+const templateUrl = require('~network-ui/default.partial.svg');
+
+function defaultd () {
+ return { restrict: 'A', templateUrl};
+}
+exports.defaultd = defaultd;
diff --git a/awx/ui/client/src/network-ui/default.partial.svg b/awx/ui/client/src/network-ui/default.partial.svg
new file mode 100644
index 0000000000..380fdd32a0
--- /dev/null
+++ b/awx/ui/client/src/network-ui/default.partial.svg
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.name}}
+
+ {{item.name}}{{item.edit_label?'_':''}}
+
+
diff --git a/awx/ui/client/src/network-ui/designs/README.md b/awx/ui/client/src/network-ui/designs/README.md
new file mode 100644
index 0000000000..3a20f73bcd
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/README.md
@@ -0,0 +1,161 @@
+
+Finite State Machine Designs
+============================
+
+This directory contains the finite state machine designs that were used to
+generate the skeleton of the javascript implementations and can be used to
+check that the implementations still match the designs.
+
+
+**Machine Readable FSM Schema**
+
+The machine readable FSM schema contains three top-level elements: `name`, `states`, and `transitions`.
+* The `name` element is a string.
+* The `states` element contains a list of `state` elements which have attributes `id`, `label`, and `x`, and `y`.
+* The `transitions` element contains a list of `transition` elements which have attributes `from_state`, `to_state`, and `label`.
+
+
+**Design Diagrams**
+
+The diagrams below are visual representations of the finite state machine designs in this directory.
+The equivalent machine readable representations are linked as well.
+
+---
+
+**Null FSM**
+* See: null.yml
+
+The null FSM is an FSM that ignores all events.
+
+
+
+---
+
+**Button FSM**
+* See: button.yml
+
+The button FSM describes how a button works. The key insight here is that a button is not
+clicked if the mouse is not over the button on both the `MouseDown` and `MouseUp` events. Moving
+the mouse off the button before `MouseUp` is not a click.
+
+
+
+---
+
+**Buttons FSM**
+* See: buttons.yml
+
+The buttons FSM distributes events to the buttons which each have their own FSM.
+
+
+
+---
+
+**Device Detail FSM**
+* See: device_detail.yml
+
+The device detail FSM describes interactions when zoomed into a device.
+
+
+
+---
+
+**Group FSM**
+* See: group.yml
+
+The group FSM describes how to organize multiple devices together in a group.
+
+
+
+---
+
+**Hot Keys FSM**
+* See: hotkeys.yml
+
+The hot keys FSM handles key events and generates new events like `NewLink` to implement
+hot keys.
+
+
+
+---
+
+**Link FSM**
+* See: link.yml
+
+The link FSM connects two devices together with a link.
+
+
+
+---
+
+**Mode FSM**
+* See: mode.yml
+
+The mode FSM controls the overall mode of the network UI application.
+
+
+
+---
+
+**Move FSM**
+* See: move.yml
+
+The move FSM controls placement of devices as well as editing the device labels.
+
+
+
+---
+
+**Rack FSM**
+* See: rack.yml
+
+The rack FSM controls organizing devices into a special group called a rack.
+
+
+
+---
+
+**Site FSM**
+* See: site.yml
+
+The site FSM controls organizing devices into a special group called a site.
+
+
+
+---
+
+**Stream FSM**
+* See: stream.yml
+
+The stream FSM controls how streams are defined between devices.
+
+
+
+---
+
+**Time FSM**
+* See: time.yml
+
+The time FSM controls undo/redo functionality of the network UI.
+
+
+
+---
+
+**Toolbox FSM**
+* See: toolbox.yml
+
+The toolbox FSM controls the drag-and-drop toolboxes and allow placement of new devices, applications,
+racks, and sites onto the canvas.
+
+
+
+---
+
+**View FSM**
+* See: view.yml
+
+The view FSM controls the panning and scaling of the the virtual canvas through clicking-and-dragging
+of the background and scrolling the mousewheel.
+
+
diff --git a/awx/ui/client/src/network-ui/designs/animation.yml b/awx/ui/client/src/network-ui/designs/animation.yml
new file mode 100644
index 0000000000..c99b99dd1c
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/animation.yml
@@ -0,0 +1,29 @@
+diagram_id: 58
+name: animation_fsm
+states:
+- id: 4
+ label: Cancelled
+ x: 590
+ y: 602
+- id: 3
+ label: Completed
+ x: 225
+ y: 604
+- id: 2
+ label: Running
+ x: 418
+ y: 362
+- id: 1
+ label: Start
+ x: 454
+ y: 158
+transitions:
+- from_state: Running
+ label: onAnimationCancelled
+ to_state: Cancelled
+- from_state: Running
+ label: onAnimationCompleted
+ to_state: Completed
+- from_state: Start
+ label: start
+ to_state: Running
diff --git a/awx/ui/client/src/network-ui/designs/button.png b/awx/ui/client/src/network-ui/designs/button.png
new file mode 100644
index 0000000000..5a1bc7aaa1
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/button.png differ
diff --git a/awx/ui/client/src/network-ui/designs/button.yml b/awx/ui/client/src/network-ui/designs/button.yml
new file mode 100644
index 0000000000..f037edb8f8
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/button.yml
@@ -0,0 +1,43 @@
+diagram_id: 66
+name: 'button_fsm'
+finite_state_machine_id: 12
+states:
+- id: 3
+ label: Clicked
+ x: 331
+ y: 568
+- id: 5
+ label: Disabled
+ x: 719
+ y: 283
+- id: 4
+ label: Pressed
+ x: 606
+ y: 563
+- id: 1
+ label: Ready
+ x: 471
+ y: 376
+- id: 2
+ label: Start
+ x: 468
+ y: 170
+transitions:
+- from_state: Clicked
+ label: start
+ to_state: Ready
+- from_state: Disabled
+ label: onEnable
+ to_state: Ready
+- from_state: Pressed
+ label: onMouseUp
+ to_state: Clicked
+- from_state: Ready
+ label: onDisable
+ to_state: Disabled
+- from_state: Ready
+ label: onMouseDown
+ to_state: Pressed
+- from_state: Start
+ label: start
+ to_state: Ready
diff --git a/awx/ui/client/src/network-ui/designs/buttons.png b/awx/ui/client/src/network-ui/designs/buttons.png
new file mode 100644
index 0000000000..8538b06614
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/buttons.png differ
diff --git a/awx/ui/client/src/network-ui/designs/buttons.yml b/awx/ui/client/src/network-ui/designs/buttons.yml
new file mode 100644
index 0000000000..b1cc56a246
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/buttons.yml
@@ -0,0 +1,28 @@
+app: buttons_fsm
+finite_state_machine_id: 7
+panX: 133
+panY: 41
+scaleXY: 1
+states:
+- label: Start
+ size: 100
+ x: 392
+ y: 88
+- label: Ready
+ size: 100
+ x: 392
+ y: 281
+- label: ButtonPressed
+ size: 100
+ x: 394
+ y: 491
+transitions:
+- from_state: Start
+ label: start
+ to_state: Ready
+- from_state: Ready
+ label: onMouseDown
+ to_state: ButtonPressed
+- from_state: ButtonPressed
+ label: onMouseUp
+ to_state: Ready
diff --git a/awx/ui/client/src/network-ui/designs/details.panel.png b/awx/ui/client/src/network-ui/designs/details.panel.png
new file mode 100644
index 0000000000..d060f3bd2f
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/details.panel.png differ
diff --git a/awx/ui/client/src/network-ui/designs/details.panel.yml b/awx/ui/client/src/network-ui/designs/details.panel.yml
new file mode 100644
index 0000000000..555d21c8be
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/details.panel.yml
@@ -0,0 +1,26 @@
+diagram_id: 70
+finite_state_machine_id: 21
+name: diagram
+states:
+- id: 1
+ label: Start
+ x: 590
+ y: 233
+- id: 2
+ label: Collapsed
+ x: 594
+ y: 490
+- id: 3
+ label: Expanded
+ x: 919
+ y: 491
+transitions:
+- from_state: Start
+ label: start
+ to_state: Collapsed
+- from_state: Expanded
+ label: onDetailsPanelClose
+ to_state: Collapsed
+- from_state: Collapsed
+ label: onDetailsPanel
+ to_state: Expanded
diff --git a/awx/ui/client/src/network-ui/designs/device.detail.png b/awx/ui/client/src/network-ui/designs/device.detail.png
new file mode 100644
index 0000000000..0f11ac8ffe
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/device.detail.png differ
diff --git a/awx/ui/client/src/network-ui/designs/device.detail.yml b/awx/ui/client/src/network-ui/designs/device.detail.yml
new file mode 100644
index 0000000000..fcb2f52eb6
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/device.detail.yml
@@ -0,0 +1,19 @@
+finite_state_machine_id: 19
+name: device_detail_fsm
+states:
+- id: 2
+ label: Ready
+ x: 517
+ y: 588
+- id: 3
+ label: Disable
+ x: 770
+ y: 455
+- id: 1
+ label: Start
+ x: 507
+ y: 336
+transitions:
+- from_state: Start
+ label: start
+ to_state: Ready
diff --git a/awx/ui/client/src/network-ui/designs/group.png b/awx/ui/client/src/network-ui/designs/group.png
new file mode 100644
index 0000000000..5ca5ead538
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/group.png differ
diff --git a/awx/ui/client/src/network-ui/designs/group.yml b/awx/ui/client/src/network-ui/designs/group.yml
new file mode 100644
index 0000000000..f618170553
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/group.yml
@@ -0,0 +1,119 @@
+diagram_id: 61
+finite_state_machine_id: 5
+name: group_fsm
+states:
+- id: 12
+ label: ContextMenu
+ x: 1228
+ y: -74
+- id: 3
+ label: CornerSelected
+ x: 526
+ y: 554
+- id: 8
+ label: Disable
+ x: 497
+ y: 84
+- id: 9
+ label: EditLabel
+ x: 1130
+ y: 112
+- id: 6
+ label: Move
+ x: 1297
+ y: 786
+- id: 11
+ label: Placing
+ x: 299
+ y: 300
+- id: 7
+ label: Ready
+ x: 733
+ y: 304
+- id: 1
+ label: Resize
+ x: 571
+ y: 911
+- id: 4
+ label: Selected1
+ x: 839
+ y: 640
+- id: 10
+ label: Selected2
+ x: 1179
+ y: 435
+- id: 5
+ label: Selected3
+ x: 1528
+ y: 360
+- id: 2
+ label: Start
+ x: 744
+ y: 69
+transitions:
+- from_state: ContextMenu
+ label: onLabelEdit
+ to_state: EditLabel
+- from_state: ContextMenu
+ label: onMouseDown
+ to_state: Ready
+- from_state: CornerSelected
+ label: onMouseMove
+ to_state: Resize
+- from_state: CornerSelected
+ label: onMouseUp
+ to_state: Selected1
+- from_state: EditLabel
+ label: onKeyDown
+ to_state: Selected2
+- from_state: EditLabel
+ label: onMouseDown
+ to_state: Ready
+- from_state: Move
+ label: onMouseDown
+ to_state: Selected1
+- from_state: Move
+ label: onMouseUp
+ to_state: Selected2
+- from_state: Placing
+ label: onMouseDown
+ to_state: Resize
+- from_state: Ready
+ label: onMouseDown
+ to_state: CornerSelected
+- from_state: Ready
+ label: onMouseDown
+ to_state: Selected1
+- from_state: Ready
+ label: onNewGroup
+ to_state: Placing
+- from_state: Resize
+ label: onMouseUp
+ to_state: Selected1
+- from_state: Selected1
+ label: onMouseMove
+ to_state: Move
+- from_state: Selected1
+ label: onMouseUp
+ to_state: Selected2
+- from_state: Selected2
+ label: onKeyDown
+ to_state: Ready
+- from_state: Selected2
+ label: onMouseDown
+ to_state: Ready
+- from_state: Selected2
+ label: onMouseDown
+ to_state: Selected3
+- from_state: Selected2
+ label: onNewGroup
+ to_state: Ready
+- from_state: Selected3
+ label: onMouseMove
+ to_state: Move
+- from_state: Selected3
+ label: onMouseUp
+ to_state: ContextMenu
+- from_state: Start
+ label: start
+ to_state: Ready
diff --git a/awx/ui/client/src/network-ui/designs/hotkeys.png b/awx/ui/client/src/network-ui/designs/hotkeys.png
new file mode 100644
index 0000000000..d21ff09cc6
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/hotkeys.png differ
diff --git a/awx/ui/client/src/network-ui/designs/hotkeys.yml b/awx/ui/client/src/network-ui/designs/hotkeys.yml
new file mode 100644
index 0000000000..6d237bda46
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/hotkeys.yml
@@ -0,0 +1,25 @@
+finite_state_machine_id: 1
+name: hotkeys_fsm
+states:
+- id: 2
+ label: Enabled
+ x: 585
+ y: 396
+- id: 1
+ label: Start
+ x: 585
+ y: 160
+- id: 3
+ label: Disabled
+ x: 331
+ y: 408
+transitions:
+- from_state: Enabled
+ label: onDisable
+ to_state: Disabled
+- from_state: Disabled
+ label: onEnable
+ to_state: Enabled
+- from_state: Start
+ label: start
+ to_state: Enabled
diff --git a/awx/ui/client/src/network-ui/designs/keybindings.png b/awx/ui/client/src/network-ui/designs/keybindings.png
new file mode 100644
index 0000000000..b6a0b89a19
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/keybindings.png differ
diff --git a/awx/ui/client/src/network-ui/designs/keybindings.yml b/awx/ui/client/src/network-ui/designs/keybindings.yml
new file mode 100644
index 0000000000..b98167e288
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/keybindings.yml
@@ -0,0 +1,38 @@
+diagram_id: 68
+finite_state_machine_id: 18
+name: diagram
+states:
+- id: 1
+ label: Enabled
+ x: 842
+ y: 533
+- id: 2
+ label: Start
+ x: 839
+ y: 270
+- id: 3
+ label: Disabled
+ x: 1412
+ y: 522
+transitions:
+- from_state: Start
+ label: start
+ to_state: Enabled
+- from_state: Disabled
+ label: onBindDocument
+ to_state: Enabled
+- from_state: Enabled
+ label: onUnbindDocument
+ to_state: Disabled
+- from_state: Disabled
+ label: onDetailsPanelClose
+ to_state: Enabled
+- from_state: Enabled
+ label: onDetailsPanel
+ to_state: Disabled
+- from_state: Enabled
+ label: onSearchDropdown
+ to_state: Disabled
+- from_state: Disabled
+ label: onSearchDropdownClose
+ to_state: Enabled
diff --git a/awx/ui/client/src/network-ui/designs/link.png b/awx/ui/client/src/network-ui/designs/link.png
new file mode 100644
index 0000000000..4058fd5519
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/link.png differ
diff --git a/awx/ui/client/src/network-ui/designs/link.yml b/awx/ui/client/src/network-ui/designs/link.yml
new file mode 100644
index 0000000000..7b3a7ea16c
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/link.yml
@@ -0,0 +1,42 @@
+finite_state_machine_id: 4
+name: link_fsm
+states:
+- id: 5
+ label: Selecting
+ x: -429
+ y: 63
+- id: 2
+ label: Start
+ x: 15
+ y: -221
+- id: 4
+ label: Connecting
+ x: -429
+ y: 466
+- id: 3
+ label: Connected
+ x: 47
+ y: 453
+- id: 1
+ label: Ready
+ x: 26
+ y: 61
+transitions:
+- from_state: Ready
+ label: onNewLink
+ to_state: Selecting
+- from_state: Selecting
+ label: onMouseUp
+ to_state: Connecting
+- from_state: Connecting
+ label: onMouseUp
+ to_state: Connected
+- from_state: Connecting
+ label: onMouseUp
+ to_state: Ready
+- from_state: Connected
+ label: start
+ to_state: Ready
+- from_state: Start
+ label: start
+ to_state: Ready
diff --git a/awx/ui/client/src/network-ui/designs/mode.png b/awx/ui/client/src/network-ui/designs/mode.png
new file mode 100644
index 0000000000..b8dff5610d
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/mode.png differ
diff --git a/awx/ui/client/src/network-ui/designs/mode.yml b/awx/ui/client/src/network-ui/designs/mode.yml
new file mode 100644
index 0000000000..b124a3d32e
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/mode.yml
@@ -0,0 +1,96 @@
+diagram_id: 68
+finite_state_machine_id: 9
+name: mode_fsm
+states:
+- id: 7
+ label: Device
+ x: 558
+ y: 821
+- id: 2
+ label: Interface
+ x: 340
+ y: 1053
+- id: 5
+ label: MultiSite
+ x: 569
+ y: -88
+- id: 4
+ label: Process
+ x: 833
+ y: 1051
+- id: 6
+ label: Rack
+ x: 571
+ y: 486
+- id: 3
+ label: Site
+ x: 564
+ y: 201
+- id: 1
+ label: Start
+ x: 568
+ y: -379
+transitions:
+- from_state: Device
+ label: onMouseWheel
+ to_state: Process
+- from_state: Device
+ label: onMouseWheel
+ to_state: Rack
+- from_state: Device
+ label: onMouseWheel
+ to_state: Interface
+- from_state: Device
+ label: onScaleChanged
+ to_state: Interface
+- from_state: Device
+ label: onScaleChanged
+ to_state: Rack
+- from_state: Device
+ label: onScaleChanged
+ to_state: Process
+- from_state: Interface
+ label: onMouseWheel
+ to_state: Device
+- from_state: Interface
+ label: onScaleChanged
+ to_state: Device
+- from_state: MultiSite
+ label: onMouseWheel
+ to_state: Site
+- from_state: MultiSite
+ label: onScaleChanged
+ to_state: Site
+- from_state: Process
+ label: onMouseWheel
+ to_state: Device
+- from_state: Process
+ label: onScaleChanged
+ to_state: Device
+- from_state: Rack
+ label: onMouseWheel
+ to_state: Site
+- from_state: Rack
+ label: onMouseWheel
+ to_state: Device
+- from_state: Rack
+ label: onScaleChanged
+ to_state: Site
+- from_state: Rack
+ label: onScaleChanged
+ to_state: Device
+- from_state: Site
+ label: onMouseWheel
+ to_state: MultiSite
+- from_state: Site
+ label: onMouseWheel
+ to_state: Rack
+- from_state: Site
+ label: onScaleChanged
+ to_state: MultiSite
+- from_state: Site
+ label: onScaleChanged
+ to_state: Rack
+- from_state: Start
+ label: start
+ to_state: MultiSite
diff --git a/awx/ui/client/src/network-ui/designs/move.png b/awx/ui/client/src/network-ui/designs/move.png
new file mode 100644
index 0000000000..e199991c76
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/move.png differ
diff --git a/awx/ui/client/src/network-ui/designs/move.readonly.png b/awx/ui/client/src/network-ui/designs/move.readonly.png
new file mode 100644
index 0000000000..95db906bcd
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/move.readonly.png differ
diff --git a/awx/ui/client/src/network-ui/designs/move.readonly.yml b/awx/ui/client/src/network-ui/designs/move.readonly.yml
new file mode 100644
index 0000000000..6d1245d772
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/move.readonly.yml
@@ -0,0 +1,65 @@
+diagram_id: 91
+name: diagram
+states:
+- id: 0
+ label: ContextMenu
+ x: 826
+ y: 1008
+- id: 1
+ label: Disable
+ x: 914
+ y: 115
+- id: 5
+ label: Ready
+ x: 702
+ y: 327
+- id: 6
+ label: Selected1
+ x: 397
+ y: 332
+- id: 7
+ label: Selected2
+ x: 268
+ y: 735
+- id: 8
+ label: Selected3
+ x: 225
+ y: 1021
+- id: 9
+ label: Start
+ x: 704
+ y: 128
+transitions:
+- from_state: ContextMenu
+ label: onDetailsPanel
+ to_state: Selected2
+- from_state: ContextMenu
+ label: onMouseDown
+ to_state: Selected2
+- from_state: Ready
+ label: onMouseDown
+ to_state: Selected1
+- from_state: Selected1
+ label: onMouseUp
+ to_state: Selected2
+- from_state: Selected2
+ label: onKeyDown
+ to_state: Ready
+- from_state: Selected2
+ label: onMouseDown
+ to_state: Selected3
+- from_state: Selected2
+ label: onMouseDown
+ to_state: Ready
+- from_state: Selected3
+ label: ''
+ to_state: Selected3
+- from_state: Selected3
+ label: onMouseMove
+ to_state: Selected2
+- from_state: Selected3
+ label: onMouseUp
+ to_state: ContextMenu
+- from_state: Start
+ label: start
+ to_state: Ready
diff --git a/awx/ui/client/src/network-ui/designs/move.yml b/awx/ui/client/src/network-ui/designs/move.yml
new file mode 100644
index 0000000000..42e42b6dfd
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/move.yml
@@ -0,0 +1,107 @@
+diagram_id: 87
+name: move
+states:
+- id: 8
+ label: ContextMenu
+ x: 826
+ y: 1008
+- id: 0
+ label: Disable
+ x: 914
+ y: 115
+- id: 6
+ label: EditLabel
+ x: 765
+ y: 684
+- id: 4
+ label: Move
+ x: 118
+ y: 594
+- id: 5
+ label: Placing
+ x: 263
+ y: 89
+- id: 2
+ label: Ready
+ x: 702
+ y: 327
+- id: 3
+ label: Selected1
+ x: 397
+ y: 332
+- id: 7
+ label: Selected2
+ x: 268
+ y: 735
+- id: 9
+ label: Selected3
+ x: 361
+ y: 961
+- id: 1
+ label: Start
+ x: 704
+ y: 128
+transitions:
+- from_state: ContextMenu
+ label: onDetailsPanel
+ to_state: Selected2
+- from_state: ContextMenu
+ label: onLabelEdit
+ to_state: EditLabel
+- from_state: ContextMenu
+ label: onMouseDown
+ to_state: Selected2
+- from_state: EditLabel
+ label: onKeyDown
+ to_state: Selected2
+- from_state: EditLabel
+ label: onMouseDown
+ to_state: Ready
+- from_state: Move
+ label: onMouseDown
+ to_state: Selected1
+- from_state: Move
+ label: onMouseUp
+ to_state: Selected1
+- from_state: Placing
+ label: onMouseDown
+ to_state: Selected1
+- from_state: Placing
+ label: onMouseMove
+ to_state: Move
+- from_state: Ready
+ label: onMouseDown
+ to_state: Selected1
+- from_state: Ready
+ label: onNewDevice
+ to_state: Placing
+- from_state: Ready
+ label: onPasteDevice
+ to_state: Selected2
+- from_state: Selected1
+ label: onMouseMove
+ to_state: Move
+- from_state: Selected1
+ label: onMouseUp
+ to_state: Selected2
+- from_state: Selected2
+ label: onKeyDown
+ to_state: Ready
+- from_state: Selected2
+ label: onMouseDown
+ to_state: Selected3
+- from_state: Selected2
+ label: onMouseDown
+ to_state: Ready
+- from_state: Selected2
+ label: onNewDevice
+ to_state: Ready
+- from_state: Selected3
+ label: onMouseMove
+ to_state: Move
+- from_state: Selected3
+ label: onMouseUp
+ to_state: ContextMenu
+- from_state: Start
+ label: start
+ to_state: Ready
diff --git a/awx/ui/client/src/network-ui/designs/null.png b/awx/ui/client/src/network-ui/designs/null.png
new file mode 100644
index 0000000000..f095a1cad4
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/null.png differ
diff --git a/awx/ui/client/src/network-ui/designs/null.yml b/awx/ui/client/src/network-ui/designs/null.yml
new file mode 100644
index 0000000000..5218cfe1f7
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/null.yml
@@ -0,0 +1,15 @@
+finite_state_machine_id: 17
+name: null_fsm
+states:
+- id: 1
+ label: Start
+ x: 391
+ y: 132
+- id: 2
+ label: Ready
+ x: 402
+ y: 346
+transitions:
+- from_state: Start
+ label: start
+ to_state: Ready
diff --git a/awx/ui/client/src/network-ui/designs/pipeline.png b/awx/ui/client/src/network-ui/designs/pipeline.png
new file mode 100644
index 0000000000..c5fb2b12f7
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/pipeline.png differ
diff --git a/awx/ui/client/src/network-ui/designs/pipeline.yml b/awx/ui/client/src/network-ui/designs/pipeline.yml
new file mode 100644
index 0000000000..43052f2b2d
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/pipeline.yml
@@ -0,0 +1,233 @@
+channels:
+- from_fsm: buttons_fsm
+ from_fsm_id: 7
+ inbox: ''
+ outbox: ''
+ to_fsm: button_fsm
+ to_fsm_id: 12
+ type: ''
+- from_fsm: buttons_fsm
+ from_fsm_id: 7
+ inbox: ''
+ outbox: ''
+ to_fsm: toolbox_fsm
+ to_fsm_id: 14
+ type: ''
+- from_fsm: details_panel_fsm
+ from_fsm_id: 21
+ inbox: ''
+ outbox: ''
+ to_fsm: move_fsm
+ to_fsm_id: 3
+ type: ''
+- from_fsm: device_detail_fsm
+ from_fsm_id: 19
+ inbox: ''
+ outbox: ''
+ to_fsm: view_fsm
+ to_fsm_id: 2
+ type: ''
+- from_fsm: group_fsm
+ from_fsm_id: 5
+ inbox: ''
+ outbox: ''
+ to_fsm: stream_fsm
+ to_fsm_id: 20
+ type: ''
+- from_fsm: hotkeys_fsm
+ from_fsm_id: 1
+ inbox: ''
+ outbox: ''
+ to_fsm: null_fsm
+ to_fsm_id: 17
+ type: ''
+- from_fsm: keybindings_fsm
+ from_fsm_id: 18
+ inbox: ''
+ outbox: ''
+ to_fsm: hotkeys_fsm
+ to_fsm_id: 1
+ type: ''
+- from_fsm: link_fsm
+ from_fsm_id: 4
+ inbox: ''
+ outbox: ''
+ to_fsm: details_panel_fsm
+ to_fsm_id: 21
+ type: ''
+- from_fsm: mode_fsm
+ from_fsm_id: 9
+ inbox: ''
+ outbox: ''
+ to_fsm: time_fsm
+ to_fsm_id: 8
+ type: ''
+- from_fsm: move_fsm
+ from_fsm_id: 3
+ inbox: ''
+ outbox: ''
+ to_fsm: device_detail_fsm
+ to_fsm_id: 19
+ type: ''
+- from_fsm: rack_fsm
+ from_fsm_id: 6
+ inbox: ''
+ outbox: ''
+ to_fsm: group_fsm
+ to_fsm_id: 5
+ type: ''
+- from_fsm: site_fsm
+ from_fsm_id: 22
+ inbox: ''
+ outbox: ''
+ to_fsm: rack_fsm
+ to_fsm_id: 6
+ type: ''
+- from_fsm: stream_fsm
+ from_fsm_id: 20
+ inbox: ''
+ outbox: ''
+ to_fsm: link_fsm
+ to_fsm_id: 4
+ type: ''
+- from_fsm: test_fsm
+ from_fsm_id: 23
+ inbox: ''
+ outbox: ''
+ to_fsm: mode_fsm
+ to_fsm_id: 9
+ type: ''
+- from_fsm: time_fsm
+ from_fsm_id: 8
+ inbox: ''
+ outbox: ''
+ to_fsm: buttons_fsm
+ to_fsm_id: 7
+ type: ''
+- from_fsm: toolbox_fsm
+ from_fsm_id: 14
+ inbox: ''
+ outbox: ''
+ to_fsm: site_fsm
+ to_fsm_id: 22
+ type: ''
+- from_fsm: view_fsm
+ from_fsm_id: 2
+ inbox: ''
+ outbox: ''
+ to_fsm: keybindings_fsm
+ to_fsm_id: 18
+ type: ''
+diagram_id: 85
+fsms:
+- id: 12
+ name: button_fsm
+ x1: -2438
+ x2: -3026
+ y1: -934
+ y2: -1532
+- id: 7
+ name: buttons_fsm
+ x1: -2650
+ x2: -2850
+ y1: -16
+ y2: -619
+- id: 21
+ name: details_panel_fsm
+ x1: 5669
+ x2: 5140
+ y1: -64
+ y2: -521
+- id: 19
+ name: device_detail_fsm
+ x1: 7667
+ x2: 7214
+ y1: -110
+ y2: -562
+- id: 5
+ name: group_fsm
+ x1: 3685
+ x2: 2256
+ y1: 278
+ y2: -906
+- id: 1
+ name: hotkeys_fsm
+ x1: 9692
+ x2: 9281
+ y1: -124
+ y2: -549
+- id: 18
+ name: keybindings_fsm
+ x1: 9223
+ x2: 8370
+ y1: -71
+ y2: -614
+- id: 4
+ name: link_fsm
+ x1: 5080
+ x2: 4436
+ y1: 154
+ y2: -732
+- id: 9
+ name: mode_fsm
+ x1: -3760
+ x2: -4453
+ y1: 192
+ y2: -1439
+- id: 3
+ name: move_fsm
+ x1: 6968
+ x2: 5813
+ y1: 146
+ y2: -935
+- id: 17
+ name: null_fsm
+ x1: 10125
+ x2: 9925
+ y1: -129
+ y2: -543
+- id: 6
+ name: rack_fsm
+ x1: 2214
+ x2: 1047
+ y1: 127
+ y2: -753
+- id: 22
+ name: site_fsm
+ x1: 964
+ x2: -190
+ y1: 128
+ y2: -768
+- id: 20
+ name: stream_fsm
+ x1: 4376
+ x2: 3868
+ y1: 56
+ y2: -643
+- id: 23
+ name: test_fsm
+ x1: -4569
+ x2: -5140
+ y1: 72
+ y2: -863
+- id: 8
+ name: time_fsm
+ x1: -3122
+ x2: -3693
+ y1: -69
+ y2: -553
+- id: 14
+ name: toolbox_fsm
+ x1: -680
+ x2: -1722
+ y1: 265
+ y2: -904
+- id: 2
+ name: view_fsm
+ x1: 8311
+ x2: 7734
+ y1: -25
+ y2: -684
+name: diagram
+states: []
+transitions: []
diff --git a/awx/ui/client/src/network-ui/designs/rack.png b/awx/ui/client/src/network-ui/designs/rack.png
new file mode 100644
index 0000000000..505cfdda6b
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/rack.png differ
diff --git a/awx/ui/client/src/network-ui/designs/rack.yml b/awx/ui/client/src/network-ui/designs/rack.yml
new file mode 100644
index 0000000000..3a5f7cae8c
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/rack.yml
@@ -0,0 +1,83 @@
+diagram_id: 65
+finite_state_machine_id: 6
+name: rack_fsm
+states:
+- id: 9
+ label: ContextMenu
+ x: 898
+ y: 1016
+- id: 2
+ label: Disable
+ x: 760
+ y: 468
+- id: 7
+ label: EditLabel
+ x: 600
+ y: 934
+- id: 8
+ label: Move
+ x: -69
+ y: 861
+- id: 1
+ label: Ready
+ x: 532
+ y: 560
+- id: 4
+ label: Selected1
+ x: 214
+ y: 566
+- id: 5
+ label: Selected2
+ x: 220
+ y: 810
+- id: 6
+ label: Selected3
+ x: 249
+ y: 1047
+- id: 3
+ label: Start
+ x: 582
+ y: 334
+transitions:
+- from_state: ContextMenu
+ label: onLabelEdit
+ to_state: EditLabel
+- from_state: ContextMenu
+ label: onMouseDown
+ to_state: Ready
+- from_state: EditLabel
+ label: onKeyDown
+ to_state: Selected2
+- from_state: EditLabel
+ label: onMouseDown
+ to_state: Ready
+- from_state: Move
+ label: onMouseUp
+ to_state: Selected2
+- from_state: Ready
+ label: onMouseDown
+ to_state: Selected1
+- from_state: Selected1
+ label: onMouseMove
+ to_state: Move
+- from_state: Selected1
+ label: onMouseUp
+ to_state: Selected2
+- from_state: Selected2
+ label: onKeyDown
+ to_state: Ready
+- from_state: Selected2
+ label: onMouseDown
+ to_state: Ready
+- from_state: Selected2
+ label: onMouseDown
+ to_state: Selected3
+- from_state: Selected3
+ label: onMouseMove
+ to_state: Move
+- from_state: Selected3
+ label: onMouseUp
+ to_state: ContextMenu
+- from_state: Start
+ label: start
+ to_state: Ready
diff --git a/awx/ui/client/src/network-ui/designs/site.png b/awx/ui/client/src/network-ui/designs/site.png
new file mode 100644
index 0000000000..72d256f552
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/site.png differ
diff --git a/awx/ui/client/src/network-ui/designs/site.yml b/awx/ui/client/src/network-ui/designs/site.yml
new file mode 100644
index 0000000000..ab807f28d1
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/site.yml
@@ -0,0 +1,83 @@
+diagram_id: 63
+finite_state_machine_id: 22
+name: site_fsm
+states:
+- id: 9
+ label: ContextMenu
+ x: 887
+ y: 1031
+- id: 2
+ label: Disable
+ x: 760
+ y: 468
+- id: 7
+ label: EditLabel
+ x: 600
+ y: 934
+- id: 8
+ label: Move
+ x: -69
+ y: 861
+- id: 1
+ label: Ready
+ x: 532
+ y: 560
+- id: 4
+ label: Selected1
+ x: 214
+ y: 566
+- id: 5
+ label: Selected2
+ x: 220
+ y: 810
+- id: 6
+ label: Selected3
+ x: 249
+ y: 1047
+- id: 3
+ label: Start
+ x: 582
+ y: 334
+transitions:
+- from_state: ContextMenu
+ label: onLabelEdit
+ to_state: EditLabel
+- from_state: ContextMenu
+ label: onMouseDown
+ to_state: Ready
+- from_state: EditLabel
+ label: onKeyDown
+ to_state: Selected2
+- from_state: EditLabel
+ label: onMouseDown
+ to_state: Ready
+- from_state: Move
+ label: onMouseUp
+ to_state: Selected2
+- from_state: Ready
+ label: onMouseDown
+ to_state: Selected1
+- from_state: Selected1
+ label: onMouseMove
+ to_state: Move
+- from_state: Selected1
+ label: onMouseUp
+ to_state: Selected2
+- from_state: Selected2
+ label: onKeyDown
+ to_state: Ready
+- from_state: Selected2
+ label: onMouseDown
+ to_state: Ready
+- from_state: Selected2
+ label: onMouseDown
+ to_state: Selected3
+- from_state: Selected3
+ label: onMouseMove
+ to_state: Move
+- from_state: Selected3
+ label: onMouseUp
+ to_state: ContextMenu
+- from_state: Start
+ label: start
+ to_state: Ready
diff --git a/awx/ui/client/src/network-ui/designs/stream.png b/awx/ui/client/src/network-ui/designs/stream.png
new file mode 100644
index 0000000000..3a00d21e97
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/stream.png differ
diff --git a/awx/ui/client/src/network-ui/designs/stream.yml b/awx/ui/client/src/network-ui/designs/stream.yml
new file mode 100644
index 0000000000..cf12e95312
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/stream.yml
@@ -0,0 +1,42 @@
+finite_state_machine_id: 20
+name: stream_fsm
+states:
+- id: 4
+ label: Connecting
+ x: 344
+ y: 312
+- id: 5
+ label: Selecting
+ x: 311
+ y: 23
+- id: 1
+ label: Ready
+ x: 36
+ y: 28
+- id: 3
+ label: Connected
+ x: 55
+ y: 317
+- id: 2
+ label: Start
+ x: 43
+ y: -188
+transitions:
+- from_state: Ready
+ label: onNewStream
+ to_state: Selecting
+- from_state: Start
+ label: start
+ to_state: Ready
+- from_state: Connected
+ label: start
+ to_state: Ready
+- from_state: Connecting
+ label: onMouseUp
+ to_state: Ready
+- from_state: Connecting
+ label: onMouseUp
+ to_state: Connected
+- from_state: Selecting
+ label: onMouseUp
+ to_state: Connecting
diff --git a/awx/ui/client/src/network-ui/designs/test.yml b/awx/ui/client/src/network-ui/designs/test.yml
new file mode 100644
index 0000000000..37092ab245
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/test.yml
@@ -0,0 +1,50 @@
+diagram_id: 69
+finite_state_machine_id: 23
+name: diagram
+states:
+- id: 1
+ label: Disabled
+ x: 895
+ y: 344
+- id: 4
+ label: Loading
+ x: 524
+ y: 710
+- id: 5
+ label: Ready
+ x: 722
+ y: 509
+- id: 6
+ label: Reporting
+ x: 926
+ y: 721
+- id: 3
+ label: Running
+ x: 720
+ y: 922
+- id: 2
+ label: Start
+ x: 702
+ y: 186
+transitions:
+- from_state: Disabled
+ label: onEnableTest
+ to_state: Ready
+- from_state: Loading
+ label: start
+ to_state: Running
+- from_state: Ready
+ label: onDisableTest
+ to_state: Disabled
+- from_state: Ready
+ label: start
+ to_state: Loading
+- from_state: Reporting
+ label: start
+ to_state: Loading
+- from_state: Running
+ label: onTestCompleted
+ to_state: Reporting
+- from_state: Start
+ label: start
+ to_state: Disabled
diff --git a/awx/ui/client/src/network-ui/designs/time.png b/awx/ui/client/src/network-ui/designs/time.png
new file mode 100644
index 0000000000..cec5fe69ee
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/time.png differ
diff --git a/awx/ui/client/src/network-ui/designs/time.yml b/awx/ui/client/src/network-ui/designs/time.yml
new file mode 100644
index 0000000000..6efaa5f38c
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/time.yml
@@ -0,0 +1,37 @@
+finite_state_machine_id: 8
+name: time_fsm
+states:
+- id: 1
+ label: Present
+ x: 256
+ y: 123
+- id: 2
+ label: Start
+ x: 245
+ y: -161
+- id: 3
+ label: Past
+ x: -115
+ y: 129
+transitions:
+- from_state: Past
+ label: onRedo
+ to_state: Present
+- from_state: Past
+ label: onMouseWheel
+ to_state: Present
+- from_state: Past
+ label: onKeyDown
+ to_state: Present
+- from_state: Start
+ label: start
+ to_state: Present
+- from_state: Present
+ label: onUndo
+ to_state: Past
+- from_state: Present
+ label: onMouseWheel
+ to_state: Past
+- from_state: Present
+ label: onKeyDown
+ to_state: Past
diff --git a/awx/ui/client/src/network-ui/designs/toolbox.png b/awx/ui/client/src/network-ui/designs/toolbox.png
new file mode 100644
index 0000000000..14f676bddc
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/toolbox.png differ
diff --git a/awx/ui/client/src/network-ui/designs/toolbox.yml b/awx/ui/client/src/network-ui/designs/toolbox.yml
new file mode 100644
index 0000000000..99fd550b54
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/toolbox.yml
@@ -0,0 +1,98 @@
+finite_state_machine_id: 14
+name: toolbox_fsm
+states:
+- id: 9
+ label: Disabled
+ x: 885
+ y: 141
+- id: 7
+ label: OffScreen
+ x: 1140
+ y: 217
+- id: 1
+ label: Selected
+ x: 1180
+ y: 959
+- id: 2
+ label: Move
+ x: 1409
+ y: 741
+- id: 3
+ label: Ready
+ x: 892
+ y: 429
+- id: 4
+ label: Scrolling
+ x: 567
+ y: 431
+- id: 5
+ label: Selecting
+ x: 888
+ y: 710
+- id: 6
+ label: Dropping
+ x: 1358
+ y: 431
+- id: 8
+ label: Start
+ x: 672
+ y: 196
+- id: 10
+ label: OffScreen2
+ x: 1115
+ y: -12
+transitions:
+- from_state: Ready
+ label: onDisable
+ to_state: Disabled
+- from_state: OffScreen2
+ label: onToggleToolbox
+ to_state: Disabled
+- from_state: OffScreen2
+ label: onEnable
+ to_state: OffScreen
+- from_state: Ready
+ label: onToggleToolbox
+ to_state: OffScreen
+- from_state: Selecting
+ label: onMouseDown
+ to_state: Selected
+- from_state: Selected
+ label: onMouseMove
+ to_state: Move
+- from_state: Selecting
+ label: onMouseDown
+ to_state: Ready
+- from_state: Selected
+ label: onMouseUp
+ to_state: Ready
+- from_state: Dropping
+ label: start
+ to_state: Ready
+- from_state: Start
+ label: start
+ to_state: Ready
+- from_state: Scrolling
+ label: onMouseWheel
+ to_state: Ready
+- from_state: OffScreen
+ label: onToggleToolbox
+ to_state: Ready
+- from_state: Disabled
+ label: onEnable
+ to_state: Ready
+- from_state: Ready
+ label: onMouseWheel
+ to_state: Scrolling
+- from_state: Ready
+ label: onMouseDown
+ to_state: Selecting
+- from_state: Move
+ label: onMouseUp
+ to_state: Dropping
+- from_state: OffScreen
+ label: onDisable
+ to_state: OffScreen2
+- from_state: Disabled
+ label: onToggleToolbox
+ to_state: OffScreen2
diff --git a/awx/ui/client/src/network-ui/designs/view.png b/awx/ui/client/src/network-ui/designs/view.png
new file mode 100644
index 0000000000..e0d7f261b2
Binary files /dev/null and b/awx/ui/client/src/network-ui/designs/view.png differ
diff --git a/awx/ui/client/src/network-ui/designs/view.yml b/awx/ui/client/src/network-ui/designs/view.yml
new file mode 100644
index 0000000000..75f3c5cb47
--- /dev/null
+++ b/awx/ui/client/src/network-ui/designs/view.yml
@@ -0,0 +1,45 @@
+finite_state_machine_id: 2
+name: view_fsm
+states:
+- id: 1
+ label: Start
+ x: 498
+ y: 175
+- id: 2
+ label: Ready
+ x: 506
+ y: 395
+- id: 3
+ label: Scale
+ x: 310
+ y: 626
+- id: 4
+ label: Pan
+ x: 741
+ y: 631
+- id: 5
+ label: Pressed
+ x: 739
+ y: 392
+transitions:
+- from_state: Scale
+ label: onMouseWheel
+ to_state: Ready
+- from_state: Start
+ label: start
+ to_state: Ready
+- from_state: Ready
+ label: onMouseWheel
+ to_state: Scale
+- from_state: Ready
+ label: onMouseDown
+ to_state: Pressed
+- from_state: Pressed
+ label: onMouseMove
+ to_state: Pan
+- from_state: Pressed
+ label: onMouseUp
+ to_state: Ready
+- from_state: Pan
+ label: onMouseUp
+ to_state: Ready
diff --git a/awx/ui/client/src/network-ui/details.panel.fsm.js b/awx/ui/client/src/network-ui/details.panel.fsm.js
new file mode 100644
index 0000000000..2d8560594d
--- /dev/null
+++ b/awx/ui/client/src/network-ui/details.panel.fsm.js
@@ -0,0 +1,62 @@
+var inherits = require('inherits');
+var fsm = require('./fsm.js');
+
+function _State () {
+}
+inherits(_State, fsm._State);
+
+
+function _Start () {
+ this.name = 'Start';
+}
+inherits(_Start, _State);
+var Start = new _Start();
+exports.Start = Start;
+
+function _Collapsed () {
+ this.name = 'Collapsed';
+}
+inherits(_Collapsed, _State);
+var Collapsed = new _Collapsed();
+exports.Collapsed = Collapsed;
+
+function _Expanded () {
+ this.name = 'Expanded';
+}
+inherits(_Expanded, _State);
+var Expanded = new _Expanded();
+exports.Expanded = Expanded;
+
+
+
+
+_Start.prototype.start = function (controller, msg_type, $event) {
+
+ controller.scope.$parent.vm.rightPanelIsExpanded = false;
+ controller.changeState(Collapsed);
+ controller.handle_message(msg_type, $event);
+
+};
+_Start.prototype.start.transitions = ['Collapsed'];
+
+
+
+_Collapsed.prototype.onDetailsPanel = function (controller, msg_type, $event) {
+
+ controller.scope.$parent.vm.rightPanelIsExpanded = true;
+ controller.changeState(Expanded);
+ controller.handle_message(msg_type, $event);
+
+};
+_Collapsed.prototype.onDetailsPanel.transitions = ['Expanded'];
+
+
+
+_Expanded.prototype.onDetailsPanelClose = function (controller, msg_type, $event) {
+
+ controller.scope.$parent.vm.rightPanelIsExpanded = false;
+ controller.scope.$parent.vm.keyPanelExpanded = false;
+ controller.changeState(Collapsed);
+ controller.handle_message(msg_type, $event);
+};
+_Expanded.prototype.onDetailsPanelClose.transitions = ['Collapsed'];
diff --git a/awx/ui/client/src/network-ui/extract.js b/awx/ui/client/src/network-ui/extract.js
new file mode 100755
index 0000000000..79edb17f60
--- /dev/null
+++ b/awx/ui/client/src/network-ui/extract.js
@@ -0,0 +1,47 @@
+#!/usr/bin/env node
+var YAML = require('yamljs');
+
+function Iterator(o){
+ var k=Object.keys(o);
+ return {
+ next:function(){
+ return k.shift();
+ }
+ };
+}
+
+var myArgs = process.argv.slice(2);
+var implementation = require(myArgs[0]);
+var states = [];
+var transitions = [];
+var data = {states: states,
+ transitions: transitions};
+
+var state_iter = Iterator(implementation);
+var transition_iter = null;
+var next_state = state_iter.next();
+var next_transition = null;
+var state = null;
+var transition = null;
+var i = 0;
+while(next_state !== undefined) {
+ state = implementation[next_state];
+ transition_iter = Iterator(state.constructor.prototype);
+ next_transition = transition_iter.next();
+ while (next_transition !== undefined) {
+ transition = state.constructor.prototype[next_transition];
+ if (transition.transitions !== undefined) {
+ for (i = 0; i < transition.transitions.length; i++) {
+ transitions.push({from_state: next_state,
+ to_state:transition.transitions[i],
+ label:next_transition});
+ }
+ }
+ next_transition = transition_iter.next();
+ }
+ states.push({label: state.name});
+ next_state = state_iter.next();
+}
+
+
+console.log(YAML.stringify(data));
diff --git a/awx/ui/client/src/network-ui/extract_messages.js b/awx/ui/client/src/network-ui/extract_messages.js
new file mode 100755
index 0000000000..aee32413de
--- /dev/null
+++ b/awx/ui/client/src/network-ui/extract_messages.js
@@ -0,0 +1,57 @@
+#!/usr/bin/env node
+var YAML = require('yamljs');
+
+function Iterator(o){
+ var k=Object.keys(o);
+ return {
+ next:function(){
+ return k.shift();
+ }
+ };
+}
+
+var myArgs = process.argv.slice(2);
+var implementation = require(myArgs[0]);
+var messages = [];
+var data = {messages: messages};
+var message_iter = Iterator(implementation);
+var field_iter = null;
+var next_message = message_iter.next();
+var next_field = null;
+var message = null;
+var message_instance = null;
+var fields = null;
+// var field = null;
+// var i = 0;
+while(next_message !== undefined) {
+ message = implementation[next_message];
+ try {
+ message_instance = new message();
+ } catch(err) {
+ next_message = message_iter.next();
+ continue;
+ }
+ fields = [];
+ field_iter = Iterator(message_instance);
+ next_field = field_iter.next();
+ while (next_field !== undefined) {
+ fields.push(next_field);
+ // field = message.constructor.prototype[next_field];
+ // if (field.transitions !== undefined) {
+ // for (i = 0; i < field.transitions.length; i++) {
+ // transitions.push({from_message: next_message,
+ // to_message:field.transitions[i],
+ // label:next_field});
+ // }
+ // }
+ next_field = field_iter.next();
+ }
+ if(message_instance.msg_type !== null && message_instance.msg_type !== undefined) {
+ messages.push({msg_type: message_instance.msg_type,
+ fields: fields});
+ }
+ next_message = message_iter.next();
+}
+
+
+console.log(YAML.stringify(data));
diff --git a/awx/ui/client/src/network-ui/fsm.js b/awx/ui/client/src/network-ui/fsm.js
new file mode 100644
index 0000000000..ffd833623d
--- /dev/null
+++ b/awx/ui/client/src/network-ui/fsm.js
@@ -0,0 +1,90 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+var messages = require('./messages.js');
+
+function Channel(from_controller, to_controller, tracer) {
+ this.tracer = tracer;
+ this.from_controller = from_controller;
+ this.to_controller = to_controller;
+ this.trace = false;
+}
+exports.Channel = Channel;
+
+Channel.prototype.send = function(msg_type, message) {
+ this.to_controller.handle_message(msg_type, message);
+};
+
+function NullChannel(from_controller, tracer) {
+ this.tracer = tracer;
+ this.from_controller = from_controller;
+ this.trace = false;
+}
+
+NullChannel.prototype.send = function() {
+};
+
+function FSMController (scope, name, initial_state, tracer) {
+ this.scope = scope;
+ this.name = name;
+ this.state = initial_state;
+ this.delegate_channel = new NullChannel(this, tracer);
+ this.tracer = tracer;
+ this.trace = true;
+ this.handling_message_type = 'start';
+ this.state.start(this);
+ this.handling_message_type = null;
+}
+exports.FSMController = FSMController;
+
+FSMController.prototype.changeState = function (state) {
+ var old_handling_message_type;
+ if(this.state !== null) {
+ old_handling_message_type = this.handling_message_type;
+ this.handling_message_type = 'end';
+ this.state.end(this);
+ this.handling_message_type = old_handling_message_type;
+ }
+ if (this.trace) {
+ this.tracer.send_trace_message(new messages.FSMTrace(this.tracer.trace_order_seq(),
+ this.name,
+ this.state.name,
+ state.name,
+ this.handling_message_type));
+ }
+ this.state = state;
+ if(state !== null) {
+ old_handling_message_type = this.handling_message_type;
+ this.handling_message_type = 'start';
+ state.start(this);
+ this.handling_message_type = old_handling_message_type;
+ }
+};
+
+FSMController.prototype.handle_message = function(msg_type, message) {
+
+ var old_handling_message_type = this.handling_message_type;
+ this.handling_message_type = msg_type;
+ var handler_name = 'on' + msg_type;
+ if (typeof(this.state[handler_name]) !== "undefined") {
+ this.state[handler_name](this, msg_type, message);
+ } else {
+ this.default_handler(msg_type, message);
+ }
+ this.handling_message_type = old_handling_message_type;
+};
+
+FSMController.prototype.default_handler = function(msg_type, message) {
+ this.delegate_channel.send(msg_type, message);
+};
+
+function _State () {
+}
+
+_State.prototype.start = function () {
+};
+
+_State.prototype.end = function () {
+};
+
+var State = new _State();
+exports.State = State;
+exports._State = _State;
diff --git a/awx/ui/client/src/network-ui/host.directive.js b/awx/ui/client/src/network-ui/host.directive.js
new file mode 100644
index 0000000000..aa0b13d3c4
--- /dev/null
+++ b/awx/ui/client/src/network-ui/host.directive.js
@@ -0,0 +1,8 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+
+const templateUrl = require('~network-ui/host.partial.svg');
+
+function host () {
+ return { restrict: 'A', templateUrl};
+}
+exports.host = host;
diff --git a/awx/ui/client/src/network-ui/host.partial.svg b/awx/ui/client/src/network-ui/host.partial.svg
new file mode 100644
index 0000000000..86e5c875c2
--- /dev/null
+++ b/awx/ui/client/src/network-ui/host.partial.svg
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.name}}
+
+ {{item.name}}{{item.edit_label?'_':''}}
+
+
diff --git a/awx/ui/client/src/network-ui/hotkeys.fsm.js b/awx/ui/client/src/network-ui/hotkeys.fsm.js
new file mode 100644
index 0000000000..abc3a3cc75
--- /dev/null
+++ b/awx/ui/client/src/network-ui/hotkeys.fsm.js
@@ -0,0 +1,87 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+var inherits = require('inherits');
+var fsm = require('./fsm.js');
+
+function _State () {
+}
+inherits(_State, fsm._State);
+
+
+function _Enabled () {
+ this.name = 'Enabled';
+}
+inherits(_Enabled, _State);
+var Enabled = new _Enabled();
+exports.Enabled = Enabled;
+
+function _Start () {
+ this.name = 'Start';
+}
+inherits(_Start, _State);
+var Start = new _Start();
+exports.Start = Start;
+
+function _Disabled () {
+ this.name = 'Disabled';
+}
+inherits(_Disabled, _State);
+var Disabled = new _Disabled();
+exports.Disabled = Disabled;
+
+
+
+
+_Enabled.prototype.onDisable = function (controller) {
+
+ controller.changeState(Disabled);
+
+};
+_Enabled.prototype.onDisable.transitions = ['Disabled'];
+
+
+_Enabled.prototype.onKeyDown = function(controller, msg_type, $event) {
+
+ var scope = controller.scope;
+
+ if ($event.key === 'r' && ($event.ctrlKey || $event.metaKey)) {
+ location.reload();
+ }
+
+ if ($event.key === 'd') {
+ scope.debug.hidden = !scope.debug.hidden;
+ return;
+ }
+ if ($event.key === 'i') {
+ scope.hide_interfaces = !scope.hide_interfaces;
+ return;
+ }
+ if($event.keyCode === 27){
+ // 27 is the escape key
+ scope.reset_fsm_state();
+ return;
+ }
+
+ if ($event.key === '0') {
+ scope.jump_to_animation(0, 0, 1.0);
+ }
+
+ controller.delegate_channel.send(msg_type, $event);
+};
+
+_Start.prototype.start = function (controller) {
+
+ controller.changeState(Enabled);
+
+};
+_Start.prototype.start.transitions = ['Enabled'];
+
+
+
+_Disabled.prototype.onEnable = function (controller) {
+
+ controller.changeState(Enabled);
+
+};
+_Disabled.prototype.onEnable.transitions = ['Enabled'];
+
+
diff --git a/awx/ui/client/src/network-ui/inventory.toolbox.directive.js b/awx/ui/client/src/network-ui/inventory.toolbox.directive.js
new file mode 100644
index 0000000000..d3cfa8b65e
--- /dev/null
+++ b/awx/ui/client/src/network-ui/inventory.toolbox.directive.js
@@ -0,0 +1,8 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+
+const templateUrl = require('~network-ui/inventory_toolbox.partial.svg');
+
+function inventoryToolbox () {
+ return { restrict: 'A', templateUrl};
+}
+exports.inventoryToolbox = inventoryToolbox;
diff --git a/awx/ui/client/src/network-ui/inventory_toolbox.partial.svg b/awx/ui/client/src/network-ui/inventory_toolbox.partial.svg
new file mode 100644
index 0000000000..7709b30228
--- /dev/null
+++ b/awx/ui/client/src/network-ui/inventory_toolbox.partial.svg
@@ -0,0 +1,95 @@
+
+
+
+
+ {{toolbox.name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/src/network-ui/keybindings.fsm.js b/awx/ui/client/src/network-ui/keybindings.fsm.js
new file mode 100644
index 0000000000..3e9fcd8ee4
--- /dev/null
+++ b/awx/ui/client/src/network-ui/keybindings.fsm.js
@@ -0,0 +1,93 @@
+var inherits = require('inherits');
+var fsm = require('./fsm.js');
+
+function _State () {
+}
+inherits(_State, fsm._State);
+
+
+function _Disabled () {
+ this.name = 'Disabled';
+}
+inherits(_Disabled, _State);
+var Disabled = new _Disabled();
+exports.Disabled = Disabled;
+
+function _Start () {
+ this.name = 'Start';
+}
+inherits(_Start, _State);
+var Start = new _Start();
+exports.Start = Start;
+
+function _Enabled () {
+ this.name = 'Enabled';
+}
+inherits(_Enabled, _State);
+var Enabled = new _Enabled();
+exports.Enabled = Enabled;
+
+
+
+
+_Disabled.prototype.onBindDocument = function (controller) {
+
+ $(document).bind("keydown", controller.scope.onKeyDown);
+ controller.changeState(Enabled);
+
+};
+_Disabled.prototype.onBindDocument.transitions = ['Enabled'];
+
+
+
+_Start.prototype.start = function (controller) {
+
+ $(document).bind("keydown", controller.scope.onKeyDown);
+ controller.changeState(Enabled);
+
+};
+_Start.prototype.start.transitions = ['Enabled'];
+
+
+
+_Enabled.prototype.onUnbindDocument = function (controller) {
+
+ $(document).unbind("keydown", controller.scope.onKeyDown);
+ controller.changeState(Disabled);
+
+};
+_Enabled.prototype.onUnbindDocument.transitions = ['Disabled'];
+
+_Disabled.prototype.onDetailsPanelClose = function (controller) {
+
+ $(document).bind("keydown", controller.scope.onKeyDown);
+ controller.changeState(Enabled);
+
+};
+_Disabled.prototype.onDetailsPanelClose.transitions = ['Enabled'];
+
+_Disabled.prototype.onSearchDropdownClose = function (controller) {
+
+ $(document).bind("keydown", controller.scope.onKeyDown);
+ controller.changeState(Enabled);
+
+};
+_Disabled.prototype.onSearchDropdownClose.transitions = ['Enabled'];
+
+
+
+_Enabled.prototype.onDetailsPanel = function (controller) {
+
+ $(document).unbind("keydown", controller.scope.onKeyDown);
+ controller.changeState(Disabled);
+
+};
+_Enabled.prototype.onDetailsPanel.transitions = ['Disabled'];
+
+_Enabled.prototype.onSearchDropdown = function (controller) {
+
+ $(document).unbind("keydown", controller.scope.onKeyDown);
+ controller.changeState(Disabled);
+
+};
+_Enabled.prototype.onSearchDropdown.transitions = ['Disabled'];
diff --git a/awx/ui/client/src/network-ui/link.directive.js b/awx/ui/client/src/network-ui/link.directive.js
new file mode 100644
index 0000000000..446f56b14f
--- /dev/null
+++ b/awx/ui/client/src/network-ui/link.directive.js
@@ -0,0 +1,8 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+
+const templateUrl = require('~network-ui/link.partial.svg');
+
+function link () {
+ return { restrict: 'A', templateUrl};
+}
+exports.link = link;
diff --git a/awx/ui/client/src/network-ui/link.partial.svg b/awx/ui/client/src/network-ui/link.partial.svg
new file mode 100644
index 0000000000..bd9f9f3641
--- /dev/null
+++ b/awx/ui/client/src/network-ui/link.partial.svg
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{link.name}}
+{{link.name}}{{link.edit_label?'_':''}}
+
+
+
+ {{link.from_interface.name}}
+{{link.from_interface.name}}{{link.from_interface.edit_label ?'_':''}}
+
+
+
+ {{link.to_interface.name}}
+{{link.to_interface.name}}{{link.to_interface.edit_label?'_':''}}
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/src/network-ui/main.js b/awx/ui/client/src/network-ui/main.js
new file mode 100644
index 0000000000..0a9e8d5120
--- /dev/null
+++ b/awx/ui/client/src/network-ui/main.js
@@ -0,0 +1,5 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+var networkUI = require('./network.ui.app.js');
+var tower = require('./tower.app.js');
+exports.networkUI = networkUI.networkUI;
+exports.tower = tower.tower;
diff --git a/awx/ui/client/src/network-ui/messages.js b/awx/ui/client/src/network-ui/messages.js
new file mode 100644
index 0000000000..88950cee74
--- /dev/null
+++ b/awx/ui/client/src/network-ui/messages.js
@@ -0,0 +1,240 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+
+
+function serialize(message) {
+ return JSON.stringify([message.constructor.name, message]);
+}
+exports.serialize = serialize;
+
+function DeviceMove(sender, id, x, y, previous_x, previous_y) {
+ this.msg_type = "DeviceMove";
+ this.sender = sender;
+ this.id = id;
+ this.x = x;
+ this.y = y;
+ this.previous_x = previous_x;
+ this.previous_y = previous_y;
+}
+exports.DeviceMove = DeviceMove;
+
+function DeviceCreate(sender, id, x, y, name, type, host_id) {
+ this.msg_type = "DeviceCreate";
+ this.sender = sender;
+ this.id = id;
+ this.x = x;
+ this.y = y;
+ this.name = name;
+ this.type = type;
+ this.host_id = host_id;
+}
+exports.DeviceCreate = DeviceCreate;
+
+function DeviceDestroy(sender, id, previous_x, previous_y, previous_name, previous_type, previous_host_id) {
+ this.msg_type = "DeviceDestroy";
+ this.sender = sender;
+ this.id = id;
+ this.previous_x = previous_x;
+ this.previous_y = previous_y;
+ this.previous_name = previous_name;
+ this.previous_type = previous_type;
+ this.previous_host_id = previous_host_id;
+}
+exports.DeviceDestroy = DeviceDestroy;
+
+function DeviceSelected(sender, id) {
+ this.msg_type = "DeviceSelected";
+ this.sender = sender;
+ this.id = id;
+}
+exports.DeviceSelected = DeviceSelected;
+
+function DeviceUnSelected(sender, id) {
+ this.msg_type = "DeviceUnSelected";
+ this.sender = sender;
+ this.id = id;
+}
+exports.DeviceUnSelected = DeviceUnSelected;
+
+function InterfaceCreate(sender, device_id, id, name) {
+ this.msg_type = "InterfaceCreate";
+ this.sender = sender;
+ this.device_id = device_id;
+ this.id = id;
+ this.name = name;
+}
+exports.InterfaceCreate = InterfaceCreate;
+
+function LinkCreate(sender, id, from_device_id, to_device_id, from_interface_id, to_interface_id) {
+ this.msg_type = "LinkCreate";
+ this.id = id;
+ this.sender = sender;
+ this.name = '';
+ this.from_device_id = from_device_id;
+ this.to_device_id = to_device_id;
+ this.from_interface_id = from_interface_id;
+ this.to_interface_id = to_interface_id;
+}
+exports.LinkCreate = LinkCreate;
+
+function LinkDestroy(sender, id, from_device_id, to_device_id, from_interface_id, to_interface_id, name) {
+ this.msg_type = "LinkDestroy";
+ this.id = id;
+ this.sender = sender;
+ this.name = name;
+ this.from_device_id = from_device_id;
+ this.to_device_id = to_device_id;
+ this.from_interface_id = from_interface_id;
+ this.to_interface_id = to_interface_id;
+}
+exports.LinkDestroy = LinkDestroy;
+
+function LinkSelected(sender, id) {
+ this.msg_type = "LinkSelected";
+ this.sender = sender;
+ this.id = id;
+}
+exports.LinkSelected = LinkSelected;
+
+function LinkUnSelected(sender, id) {
+ this.msg_type = "LinkUnSelected";
+ this.sender = sender;
+ this.id = id;
+}
+exports.LinkUnSelected = LinkUnSelected;
+
+function MultipleMessage(sender, messages) {
+ this.msg_type = "MultipleMessage";
+ this.sender = sender;
+ this.messages = messages;
+}
+exports.MultipleMessage = MultipleMessage;
+
+
+function MouseEvent(sender, x, y, type, trace_id) {
+ this.msg_type = "MouseEvent";
+ this.sender = sender;
+ this.x = x;
+ this.y = y;
+ this.type = type;
+ this.trace_id = trace_id;
+}
+exports.MouseEvent = MouseEvent;
+
+function MouseWheelEvent(sender, delta, deltaX, deltaY, type, metaKey, trace_id) {
+ this.msg_type = "MouseWheelEvent";
+ this.sender = sender;
+ this.delta = delta;
+ this.deltaX = deltaX;
+ this.deltaY = deltaY;
+ this.type = type;
+ this.originalEvent = {metaKey: metaKey};
+ this.trace_id = trace_id;
+}
+exports.MouseWheelEvent = MouseWheelEvent;
+
+function KeyEvent(sender, key, keyCode, type, altKey, shiftKey, ctrlKey, metaKey, trace_id) {
+ this.msg_type = "KeyEvent";
+ this.sender = sender;
+ this.key = key;
+ this.keyCode = keyCode;
+ this.type = type;
+ this.altKey = altKey;
+ this.shiftKey = shiftKey;
+ this.ctrlKey = ctrlKey;
+ this.metaKey = metaKey;
+ this.trace_id = trace_id;
+}
+exports.KeyEvent = KeyEvent;
+
+function StartRecording(sender, trace_id) {
+ this.msg_type = "StartRecording";
+ this.sender = sender;
+ this.trace_id = trace_id;
+}
+exports.StartRecording = StartRecording;
+
+function StopRecording(sender, trace_id) {
+ this.msg_type = "StopRecording";
+ this.sender = sender;
+ this.trace_id = trace_id;
+}
+exports.StopRecording = StopRecording;
+
+function ViewPort(sender, scale, panX, panY, graph_width, graph_height, trace_id) {
+ this.msg_type = "ViewPort";
+ this.sender = sender;
+ this.scale = scale;
+ this.panX = panX;
+ this.panY = panY;
+ this.graph_width = graph_width;
+ this.graph_height = graph_height;
+ this.trace_id = trace_id;
+}
+exports.ViewPort = ViewPort;
+
+function PasteDevice(device) {
+ this.device = device;
+}
+exports.PasteDevice = PasteDevice;
+
+function FSMTrace(order, fsm_name, from_state, to_state, recv_message_type) {
+ this.msg_type = 'FSMTrace';
+ this.order = order;
+ this.sender = 0;
+ this.trace_id = 0;
+ this.fsm_name = fsm_name;
+ this.from_state = from_state;
+ this.to_state = to_state;
+ this.recv_message_type = recv_message_type;
+}
+exports.FSMTrace = FSMTrace;
+
+function Snapshot(sender, devices, links, inventory_toolbox, order, trace_id) {
+ this.msg_type = 'Snapshot';
+ this.sender = 0;
+ this.devices = devices;
+ this.links = links;
+ this.inventory_toolbox = inventory_toolbox;
+ this.order = order;
+ this.trace_id = trace_id;
+}
+exports.Snapshot = Snapshot;
+
+function EnableTest() {
+ this.msg_type = "EnableTest";
+}
+exports.EnableTest = EnableTest;
+
+function DisableTest() {
+ this.msg_type = "DisableTest";
+}
+exports.DisableTest = DisableTest;
+
+function StartTest() {
+ this.msg_type = "StartTest";
+}
+exports.StartTest = StartTest;
+
+function TestCompleted() {
+ this.msg_type = "TestCompleted";
+}
+exports.TestCompleted = TestCompleted;
+
+function TestResult(sender, id, name, result, date, code_under_test) {
+ this.msg_type = "TestResult";
+ this.sender = sender;
+ this.id = id;
+ this.name = name;
+ this.result = result;
+ this.date = date;
+ this.code_under_test = code_under_test;
+}
+exports.TestResult = TestResult;
+
+function Coverage(sender, coverage, result_id) {
+ this.msg_type = "Coverage";
+ this.sender = sender;
+ this.coverage = coverage;
+ this.result_id = result_id;
+}
+exports.Coverage = Coverage;
diff --git a/awx/ui/client/src/network-ui/mode.fsm.js b/awx/ui/client/src/network-ui/mode.fsm.js
new file mode 100644
index 0000000000..22cfeec656
--- /dev/null
+++ b/awx/ui/client/src/network-ui/mode.fsm.js
@@ -0,0 +1,38 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+var inherits = require('inherits');
+var fsm = require('./fsm.js');
+var move = require('./move.fsm.js');
+
+function _State () {
+}
+inherits(_State, fsm._State);
+
+
+function _Start () {
+ this.name = 'Start';
+}
+inherits(_Start, _State);
+var Start = new _Start();
+exports.Start = Start;
+
+function _Rack () {
+ this.name = 'Rack';
+}
+inherits(_Rack, _State);
+var Rack = new _Rack();
+exports.Rack = Rack;
+
+
+_Start.prototype.start = function (controller) {
+
+ controller.scope.inventory_toolbox_controller.handle_message('Disable', {});
+ controller.changeState(Rack);
+};
+_Start.prototype.start.transitions = ['MultiSite'];
+
+
+_Rack.prototype.start = function (controller) {
+ controller.scope.current_mode = controller.state.name;
+ controller.scope.inventory_toolbox_controller.handle_message('Enable', {});
+ controller.scope.move_controller.changeState(move.Ready);
+};
diff --git a/awx/ui/client/src/network-ui/models.js b/awx/ui/client/src/network-ui/models.js
new file mode 100644
index 0000000000..2a25033ed5
--- /dev/null
+++ b/awx/ui/client/src/network-ui/models.js
@@ -0,0 +1,380 @@
+/* Copyright (c) 2017-2018 Red Hat, Inc. */
+var fsm = require('./fsm.js');
+var button = require('./button.fsm.js');
+var util = require('./util.js');
+var animation_fsm = require('./animation.fsm.js');
+
+function Device(id, name, x, y, type, host_id) {
+ this.id = id;
+ this.host_id = host_id ? host_id: 0;
+ this.name = name;
+ this.x = x;
+ this.y = y;
+ this.height = type === "host" ? 20 : 37.5;
+ this.width = 37.5;
+ this.size = 37.5;
+ this.type = type;
+ this.selected = false;
+ this.remote_selected = false;
+ this.moving = false;
+ this.icon = false;
+ this.tasks = [];
+ this.shape = type === "router" ? "circular" : "rectangular";
+ this.interface_seq = util.natural_numbers(0);
+ this.interfaces = [];
+ this.interfaces_by_name = {};
+ this.variables = {};
+}
+exports.Device = Device;
+
+Device.prototype.toJSON = function () {
+ return {id: this.id,
+ name: this.name,
+ x: this.x,
+ y: this.y,
+ type: this.type,
+ interfaces: this.interfaces.map(function (x) {
+ return x.toJSON();
+ }),
+ variables: this.variables
+ };
+};
+
+Device.prototype.is_selected = function (x, y) {
+
+ return (x > this.x - this.width &&
+ x < this.x + this.width &&
+ y > this.y - this.height &&
+ y < this.y + this.height);
+
+};
+
+Device.prototype.describeArc = util.describeArc;
+
+
+function Interface(id, name) {
+ this.id = id;
+ this.name = name;
+ this.link = null;
+ this.device = null;
+ this.edit_label = false;
+ this.dot_x = null;
+ this.dot_y = null;
+}
+exports.Interface = Interface;
+
+Interface.prototype.toJSON = function () {
+
+ return {id: this.id,
+ name: this.name};
+};
+
+Interface.prototype.is_selected = function (x, y) {
+
+ if (this.link === null || this.device === null) {
+ return false;
+ }
+
+ var d = Math.sqrt(Math.pow(x - this.device.x, 2) + Math.pow(y - this.device.y, 2));
+ return this.link.is_selected(x, y) && (d < this.dot_d + 30);
+};
+
+Interface.prototype.dot_distance = function () {
+ this.dot_d = Math.sqrt(Math.pow(this.device.x - this.dot_x, 2) + Math.pow(this.device.y - this.dot_y, 2));
+};
+
+Interface.prototype.dot = function () {
+ if (this.link === null || this.device === null) {
+ return;
+ }
+ if (this.link.to_device === null || this.link.from_device === null) {
+ return;
+ }
+ var p;
+ if (this.device.shape === "circular") {
+
+ var theta = this.link.slope_rads();
+ if (this.link.from_interface === this) {
+ theta = theta + Math.PI;
+ }
+ p = {x: this.device.x - this.device.size * Math.cos(theta),
+ y: this.device.y - this.device.size * Math.sin(theta)};
+ this.dot_x = p.x;
+ this.dot_y = p.y;
+ this.dot_distance();
+ return;
+ }
+
+ var x1;
+ var y1;
+ var x2;
+ var y2;
+ var x3;
+ var y3;
+ var x4;
+ var y4;
+ var param1;
+ var param2;
+
+ x3 = this.link.to_device.x;
+ y3 = this.link.to_device.y;
+ x4 = this.link.from_device.x;
+ y4 = this.link.from_device.y;
+
+ x1 = this.device.x - this.device.width;
+ y1 = this.device.y - this.device.height;
+ x2 = this.device.x + this.device.width;
+ y2 = this.device.y - this.device.height;
+
+ p = util.intersection(x3, y3, x4, y4, x1, y1, x2, y2);
+ param1 = util.pCase(p.x, p.y, x1, y1, x2, y2);
+ param2 = util.pCase(p.x, p.y, x3, y3, x4, y4);
+ if (param1 >= 0 && param1 <= 1 && param2 >= 0 && param2 <= 1) {
+ this.dot_x = p.x;
+ this.dot_y = p.y;
+ this.dot_distance();
+ return;
+ }
+
+
+ x1 = this.device.x - this.device.width;
+ y1 = this.device.y + this.device.height;
+ x2 = this.device.x + this.device.width;
+ y2 = this.device.y + this.device.height;
+
+ p = util.intersection(x3, y3, x4, y4, x1, y1, x2, y2);
+ param1 = util.pCase(p.x, p.y, x1, y1, x2, y2);
+ param2 = util.pCase(p.x, p.y, x3, y3, x4, y4);
+ if (param1 >= 0 && param1 <= 1 && param2 >= 0 && param2 <= 1) {
+ this.dot_x = p.x;
+ this.dot_y = p.y;
+ this.dot_distance();
+ return;
+ }
+
+ x1 = this.device.x + this.device.width;
+ y1 = this.device.y - this.device.height;
+ x2 = this.device.x + this.device.width;
+ y2 = this.device.y + this.device.height;
+
+ p = util.intersection(x3, y3, x4, y4, x1, y1, x2, y2);
+ param1 = util.pCase(p.x, p.y, x1, y1, x2, y2);
+ param2 = util.pCase(p.x, p.y, x3, y3, x4, y4);
+ if (param1 >= 0 && param1 <= 1 && param2 >= 0 && param2 <= 1) {
+ this.dot_x = p.x;
+ this.dot_y = p.y;
+ this.dot_distance();
+ return;
+ }
+
+ x1 = this.device.x - this.device.width;
+ y1 = this.device.y - this.device.height;
+ x2 = this.device.x - this.device.width;
+ y2 = this.device.y + this.device.height;
+
+ p = util.intersection(x3, y3, x4, y4, x1, y1, x2, y2);
+ param1 = util.pCase(p.x, p.y, x1, y1, x2, y2);
+ param2 = util.pCase(p.x, p.y, x3, y3, x4, y4);
+ if (param1 >= 0 && param1 <= 1 && param2 >= 0 && param2 <= 1) {
+ this.dot_x = p.x;
+ this.dot_y = p.y;
+ this.dot_distance();
+ return;
+ }
+
+};
+
+function Link(id, from_device, to_device, from_interface, to_interface) {
+ this.id = id;
+ this.from_device = from_device;
+ this.to_device = to_device;
+ this.from_interface = from_interface;
+ this.to_interface = to_interface;
+ this.selected = false;
+ this.remote_selected = false;
+ this.status = null;
+ this.edit_label = false;
+ this.name = "";
+}
+exports.Link = Link;
+
+Link.prototype.toJSON = function () {
+
+ return {from_device_id: this.from_device.id,
+ to_device_id: this.to_device.id,
+ from_interface_id: this.from_interface.id,
+ to_interface_id: this.to_interface.id,
+ name: this.name};
+};
+
+Link.prototype.is_selected = function (x, y) {
+ // Is the distance to the mouse location less than 25 if on the label side
+ // or 5 on the other from the shortest line to the link?
+
+ if (this.to_device === null) {
+ return false;
+ }
+ var d = util.pDistance(x,
+ y,
+ this.from_device.x,
+ this.from_device.y,
+ this.to_device.x,
+ this.to_device.y);
+ if (util.cross_z_pos(x,
+ y,
+ this.from_device.x,
+ this.from_device.y,
+ this.to_device.x,
+ this.to_device.y)) {
+ return d < 10;
+ } else {
+ return d < 10;
+ }
+};
+
+Link.prototype.slope_rads = function () {
+ //Return the slope in degrees for this link.
+ var x1 = this.from_device.x;
+ var y1 = this.from_device.y;
+ var x2 = this.to_device.x;
+ var y2 = this.to_device.y;
+ return Math.atan2(y2 - y1, x2 - x1);
+};
+
+Link.prototype.slope = function () {
+ //Return the slope in degrees for this link.
+ var x1 = this.from_device.x;
+ var y1 = this.from_device.y;
+ var x2 = this.to_device.x;
+ var y2 = this.to_device.y;
+ return Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI + 180;
+};
+
+Link.prototype.pDistanceLine = function (x, y) {
+
+ var x1 = this.from_device.x;
+ var y1 = this.from_device.y;
+ var x2 = this.to_device.x;
+ var y2 = this.to_device.y;
+ return util.pDistanceLine(x, y, x1, y1, x2, y2);
+};
+
+
+Link.prototype.length = function () {
+ //Return the length of this link.
+ var x1 = this.from_device.x;
+ var y1 = this.from_device.y;
+ var x2 = this.to_device.x;
+ var y2 = this.to_device.y;
+ return Math.sqrt(Math.pow(x1-x2, 2) + Math.pow(y1-y2, 2));
+};
+
+Link.prototype.plength = function (x, y) {
+ //Return the length of this link.
+ var x1 = this.from_device.x;
+ var y1 = this.from_device.y;
+ var x2 = this.to_device.x;
+ var y2 = this.to_device.y;
+ return util.pDistance(x, y, x1, y1, x2, y2);
+};
+
+function ContextMenu(name, x, y, width, height, callback, enabled, buttons, tracer) {
+ this.name = name;
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ this.callback = callback;
+ this.is_pressed = false;
+ this.mouse_over = false;
+ this.enabled = false;
+ this.buttons = buttons;
+ this.fsm = new fsm.FSMController(this, "button_fsm", enabled ? button.Start : button.Disabled, tracer);
+}
+exports.ContextMenu = ContextMenu;
+
+
+ContextMenu.prototype.is_selected = function (x, y) {
+
+ return (x > this.x &&
+ x < this.x + this.width &&
+ y > this.y &&
+ y < this.y + this.height);
+
+};
+
+function ContextMenuButton(name, x, y, width, height, callback, tracer) {
+ this.name = name;
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ this.callback = callback;
+ this.is_pressed = false;
+ this.mouse_over = false;
+ this.enabled = true;
+ this.fsm = new fsm.FSMController(this, "button_fsm", button.Start, tracer);
+}
+exports.ContextMenuButton = ContextMenuButton;
+
+
+ContextMenuButton.prototype.is_selected = function (x, y) {
+
+ return (x > this.x &&
+ x < this.x + this.width &&
+ y > this.y &&
+ y < this.y + this.height);
+
+};
+
+
+function ToolBox(id, name, type, x, y, width, height) {
+ this.id = id;
+ this.name = name;
+ this.type = type;
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ this.items = [];
+ this.spacing = 200;
+ this.scroll_offset = 0;
+ this.selected_item = null;
+ this.enabled = true;
+}
+exports.ToolBox = ToolBox;
+
+function Test(name, event_trace, fsm_trace, pre_test_snapshot, post_test_snapshot) {
+ this.name = name;
+ this.event_trace = event_trace;
+ this.fsm_trace = fsm_trace;
+ this.pre_test_snapshot = pre_test_snapshot;
+ this.post_test_snapshot = post_test_snapshot;
+}
+exports.Test = Test;
+
+function TestResult(id, name, result, date, code_under_test) {
+ this.id = id;
+ this.name = name;
+ this.result = result;
+ this.date = date;
+ this.code_under_test = code_under_test;
+}
+exports.TestResult = TestResult;
+
+function Animation(id, steps, data, scope, tracer, callback) {
+
+ this.id = id;
+ this.steps = steps;
+ this.active = true;
+ this.frame_number_seq = util.natural_numbers(-1);
+ this.frame_number = 0;
+ this.data = data;
+ this.data.updateZoomBoolean = data.updateZoomBoolean !== undefined ? data.updateZoomBoolean : true;
+ this.callback = callback;
+ this.scope = scope;
+ this.interval = null;
+ this.frame_delay = 17;
+ this.fsm = new fsm.FSMController(this, "animation_fsm", animation_fsm.Start, tracer);
+}
+exports.Animation = Animation;
diff --git a/awx/ui/client/src/network-ui/move.fsm.js b/awx/ui/client/src/network-ui/move.fsm.js
new file mode 100644
index 0000000000..6744d8a91e
--- /dev/null
+++ b/awx/ui/client/src/network-ui/move.fsm.js
@@ -0,0 +1,397 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+var inherits = require('inherits');
+var fsm = require('./fsm.js');
+var models = require('./models.js');
+var messages = require('./messages.js');
+var util = require('./util.js');
+
+function _State () {
+}
+inherits(_State, fsm._State);
+
+function _Ready () {
+ this.name = 'Ready';
+}
+inherits(_Ready, _State);
+var Ready = new _Ready();
+exports.Ready = Ready;
+
+function _Disable () {
+ this.name = 'Disable';
+}
+inherits(_Disable, _State);
+var Disable = new _Disable();
+exports.Disable = Disable;
+
+function _Start () {
+ this.name = 'Start';
+}
+inherits(_Start, _State);
+var Start = new _Start();
+exports.Start = Start;
+
+function _Selected2 () {
+ this.name = 'Selected2';
+}
+inherits(_Selected2, _State);
+var Selected2 = new _Selected2();
+exports.Selected2 = Selected2;
+
+function _Selected3 () {
+ this.name = 'Selected3';
+}
+inherits(_Selected3, _State);
+var Selected3 = new _Selected3();
+exports.Selected3 = Selected3;
+
+function _Move () {
+ this.name = 'Move';
+}
+inherits(_Move, _State);
+var Move = new _Move();
+exports.Move = Move;
+
+function _Selected1 () {
+ this.name = 'Selected1';
+}
+inherits(_Selected1, _State);
+var Selected1 = new _Selected1();
+exports.Selected1 = Selected1;
+
+function _Placing () {
+ this.name = 'Placing';
+}
+inherits(_Placing, _State);
+var Placing = new _Placing();
+exports.Placing = Placing;
+
+
+function _ContextMenu () {
+ this.name = 'ContextMenu';
+}
+inherits(_ContextMenu, _State);
+var ContextMenu = new _ContextMenu();
+exports.ContextMenu = ContextMenu;
+
+
+_State.prototype.onUnselectAll = function (controller, msg_type, $event) {
+
+ controller.changeState(Ready);
+ controller.delegate_channel.send(msg_type, $event);
+};
+
+_Ready.prototype.onPasteDevice = function (controller, msg_type, message) {
+
+ var scope = controller.scope;
+ var device = null;
+ var remote_device = null;
+ var intf = null;
+ var link = null;
+ var new_link = null;
+ var i = 0;
+ var c_messages = [];
+
+ scope.pressedX = scope.mouseX;
+ scope.pressedY = scope.mouseY;
+ scope.pressedScaledX = scope.scaledX;
+ scope.pressedScaledY = scope.scaledY;
+
+ device = new models.Device(controller.scope.device_id_seq(),
+ message.device.name,
+ scope.scaledX,
+ scope.scaledY,
+ message.device.type,
+ message.device.host_id);
+ device.variables = message.device.variables;
+ scope.update_links_in_vars_by_device(device.name, device.variables);
+ scope.devices.push(device);
+ scope.devices_by_name[message.device.name] = device;
+ c_messages.push(new messages.DeviceCreate(scope.client_id,
+ device.id,
+ device.x,
+ device.y,
+ device.name,
+ device.type,
+ device.host_id));
+ for (i=0; i < message.device.interfaces.length; i++) {
+ intf = new models.Interface(message.device.interfaces[i].id, message.device.interfaces[i].name);
+ device.interfaces.push(intf);
+ device.interfaces_by_name[message.device.interfaces[i].name] = intf;
+ intf.device = device;
+ c_messages.push(new messages.InterfaceCreate(controller.scope.client_id,
+ device.id,
+ intf.id,
+ intf.name));
+ }
+ if (scope.links_in_vars_by_device[device.name] !== undefined) {
+ for (i=0; i < scope.links_in_vars_by_device[device.name].length; i++) {
+ link = scope.links_in_vars_by_device[device.name][i];
+ if (device.interfaces_by_name[link.from_interface] === undefined) {
+ intf = new models.Interface(device.interface_seq(), link.from_interface);
+ device.interfaces.push(intf);
+ device.interfaces_by_name[link.from_interface] = intf;
+ intf.device = device;
+ c_messages.push(new messages.InterfaceCreate(controller.scope.client_id,
+ device.id,
+ intf.id,
+ intf.name));
+ }
+ if (scope.devices_by_name[link.to_device] !== undefined) {
+ remote_device = scope.devices_by_name[link.to_device];
+ if (remote_device.interfaces_by_name[link.to_interface] === undefined) {
+ intf = new models.Interface(remote_device.interface_seq(), link.to_interface);
+ remote_device.interfaces.push(intf);
+ remote_device.interfaces_by_name[link.to_interface] = intf;
+ intf.device = remote_device;
+ c_messages.push(new messages.InterfaceCreate(controller.scope.client_id,
+ remote_device.id,
+ intf.id,
+ intf.name));
+ }
+ }
+ if (scope.devices_by_name[link.to_device] === undefined) {
+ continue;
+ }
+ if (scope.devices_by_name[link.to_device].interfaces_by_name[link.to_interface] === undefined) {
+ continue;
+ }
+ new_link = new models.Link(scope.link_id_seq(),
+ device,
+ scope.devices_by_name[link.to_device],
+ device.interfaces_by_name[link.from_interface],
+ scope.devices_by_name[link.to_device].interfaces_by_name[link.to_interface]);
+ c_messages.push(new messages.LinkCreate(controller.scope.client_id,
+ new_link.id,
+ new_link.from_device.id,
+ new_link.to_device.id,
+ new_link.from_interface.id,
+ new_link.to_interface.id));
+ device.interfaces_by_name[link.from_interface].link = new_link;
+ scope.devices_by_name[link.to_device].interfaces_by_name[link.to_interface].link = new_link;
+ scope.links.push(new_link);
+ scope.updateInterfaceDots();
+ }
+ }
+ scope.selected_devices.push(device);
+ device.selected = true;
+ console.log(c_messages);
+ scope.$emit('awxNet-addSearchOption', device);
+ scope.send_control_message(new messages.MultipleMessage(controller.scope.client_id, c_messages));
+ controller.changeState(Selected2);
+};
+_Ready.prototype.onPasteDevice.transitions = ['Selected2'];
+
+_Ready.prototype.onMouseDown = function (controller, msg_type, $event) {
+
+ var last_selected = controller.scope.select_items($event.shiftKey);
+
+ if (last_selected.last_selected_device !== null) {
+ controller.changeState(Selected1);
+ } else if (last_selected.last_selected_link !== null) {
+ controller.changeState(Selected1);
+ } else if (last_selected.last_selected_interface !== null) {
+ controller.changeState(Selected1);
+ } else {
+ controller.delegate_channel.send(msg_type, $event);
+ }
+};
+_Ready.prototype.onMouseDown.transitions = ['Selected1'];
+
+_Start.prototype.start = function (controller) {
+
+ controller.changeState(Ready);
+
+};
+_Start.prototype.start.transitions = ['Ready'];
+
+
+_Selected2.prototype.onMouseDown = function (controller, msg_type, $event) {
+
+ var last_selected = null;
+
+ if (controller.scope.selected_devices.length === 1) {
+ var current_selected_device = controller.scope.selected_devices[0];
+ var last_selected_device = controller.scope.select_items($event.shiftKey).last_selected_device;
+ if (current_selected_device === last_selected_device) {
+ controller.changeState(Selected3);
+ return;
+ }
+ }
+
+ if (controller.scope.selected_links.length === 1) {
+ var current_selected_link = controller.scope.selected_links[0];
+ last_selected = controller.scope.select_items($event.shiftKey);
+ if (current_selected_link === last_selected.last_selected_link) {
+ controller.changeState(Selected3);
+ return;
+ }
+ }
+
+ if (controller.scope.selected_interfaces.length === 1) {
+ var current_selected_interface = controller.scope.selected_interfaces[0];
+ last_selected = controller.scope.select_items($event.shiftKey);
+ if (current_selected_interface === last_selected.last_selected_interface) {
+ controller.changeState(Selected3);
+ return;
+ }
+ }
+ controller.scope.first_channel.send('BindDocument', {});
+ controller.changeState(Ready);
+ controller.handle_message(msg_type, $event);
+};
+_Selected2.prototype.onMouseDown.transitions = ['Ready', 'Selected3'];
+
+_Selected2.prototype.onKeyDown = function (controller, msg_type, $event) {
+
+ if ($event.keyCode === 8) {
+ //Delete
+ controller.scope.deleteDevice();
+ }
+
+ controller.delegate_channel.send(msg_type, $event);
+};
+_Selected2.prototype.onKeyDown.transitions = ['Ready'];
+
+_Selected1.prototype.onMouseMove = function (controller) {
+
+ controller.changeState(Move);
+
+};
+_Selected1.prototype.onMouseMove.transitions = ['Move'];
+
+_Selected1.prototype.onMouseUp = function (controller) {
+
+ if(controller.scope.$parent.vm.rightPanelIsExpanded){
+ controller.scope.onDetailsContextButton();
+ }
+ controller.changeState(Selected2);
+
+};
+_Selected1.prototype.onMouseUp.transitions = ['Selected2'];
+
+_Selected1.prototype.onMouseDown = util.noop;
+
+_Move.prototype.start = function (controller) {
+
+ var devices = controller.scope.selected_devices;
+ var i = 0;
+ var j = 0;
+ for (i = 0; i < devices.length; i++) {
+ devices[i].moving = true;
+ for (j = 0; j < controller.scope.devices.length; j++) {
+ if ((Math.pow(devices[i].x - controller.scope.devices[j].x, 2) +
+ Math.pow(devices[i].y - controller.scope.devices[j].y, 2)) < 160000) {
+ controller.scope.devices[j].moving = true;
+ }
+ }
+ }
+};
+
+_Move.prototype.end = function (controller) {
+
+ var devices = controller.scope.devices;
+ var i = 0;
+ for (i = 0; i < devices.length; i++) {
+ devices[i].moving = false;
+ }
+};
+
+_Move.prototype.onMouseMove = function (controller) {
+
+ var devices = controller.scope.selected_devices;
+
+ var diffX = controller.scope.scaledX - controller.scope.pressedScaledX;
+ var diffY = controller.scope.scaledY - controller.scope.pressedScaledY;
+ var i = 0;
+ var j = 0;
+ var previous_x, previous_y;
+ for (i = 0; i < devices.length; i++) {
+ previous_x = devices[i].x;
+ previous_y = devices[i].y;
+ devices[i].x = devices[i].x + diffX;
+ devices[i].y = devices[i].y + diffY;
+ for (j = 0; j < devices[i].interfaces.length; j++) {
+ devices[i].interfaces[j].dot();
+ if (devices[i].interfaces[j].link !== null) {
+ devices[i].interfaces[j].link.to_interface.dot();
+ devices[i].interfaces[j].link.from_interface.dot();
+ }
+ }
+ controller.scope.send_control_message(new messages.DeviceMove(controller.scope.client_id,
+ devices[i].id,
+ devices[i].x,
+ devices[i].y,
+ previous_x,
+ previous_y));
+ }
+ controller.scope.pressedScaledX = controller.scope.scaledX;
+ controller.scope.pressedScaledY = controller.scope.scaledY;
+
+};
+
+_Move.prototype.onMouseUp = function (controller, msg_type, $event) {
+
+ controller.changeState(Selected1);
+ controller.handle_message(msg_type, $event);
+};
+_Move.prototype.onMouseUp.transitions = ['Selected1'];
+
+_Move.prototype.onMouseDown = function (controller) {
+
+ controller.changeState(Selected1);
+};
+_Move.prototype.onMouseDown.transitions = ['Selected1'];
+
+_Selected3.prototype.onMouseUp = function (controller, msg_type, $event) {
+ let context_menu = controller.scope.context_menus[0];
+ context_menu.enabled = true;
+ context_menu.x = $event.x;
+ context_menu.y = $event.y;
+ context_menu.buttons.forEach(function(button, index){
+ button.x = $event.x;
+ let menuPaddingTop = 5;
+ button.y = $event.y + menuPaddingTop + (button.height * index);
+ });
+
+ controller.changeState(ContextMenu);
+};
+_Selected3.prototype.onMouseUp.transitions = ['ContextMenu'];
+
+_Selected3.prototype.onMouseMove = function (controller) {
+ controller.changeState(Move);
+};
+_Selected3.prototype.onMouseMove.transitions = ['Move'];
+
+_Placing.prototype.onMouseDown = function (controller) {
+
+ controller.changeState(Selected1);
+
+};
+_Placing.prototype.onMouseDown.transitions = ['Selected1'];
+
+_Placing.prototype.onMouseMove = function (controller) {
+
+ controller.changeState(Move);
+
+};
+_Placing.prototype.onMouseMove.transitions = ['Move'];
+
+
+_ContextMenu.prototype.end = function (controller) {
+
+ controller.scope.removeContextMenu();
+};
+
+_ContextMenu.prototype.onMouseDown = function (controller) {
+
+ controller.changeState(Selected2);
+
+};
+_ContextMenu.prototype.onMouseDown.transitions = ['Selected2'];
+
+_ContextMenu.prototype.onDetailsPanel = function (controller, msg_type, $event) {
+
+ controller.changeState(Selected2);
+ controller.handle_message(msg_type, $event);
+};
+_ContextMenu.prototype.onDetailsPanel.transitions = ['Selected2'];
diff --git a/awx/ui/client/src/network-ui/move.readonly.fsm.js b/awx/ui/client/src/network-ui/move.readonly.fsm.js
new file mode 100644
index 0000000000..aa0e1958e0
--- /dev/null
+++ b/awx/ui/client/src/network-ui/move.readonly.fsm.js
@@ -0,0 +1,177 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+var inherits = require('inherits');
+var fsm = require('./fsm.js');
+var util = require('./util.js');
+
+function _State () {
+}
+inherits(_State, fsm._State);
+
+function _Ready () {
+ this.name = 'Ready';
+}
+inherits(_Ready, _State);
+var Ready = new _Ready();
+exports.Ready = Ready;
+
+function _Disable () {
+ this.name = 'Disable';
+}
+inherits(_Disable, _State);
+var Disable = new _Disable();
+exports.Disable = Disable;
+
+function _Start () {
+ this.name = 'Start';
+}
+inherits(_Start, _State);
+var Start = new _Start();
+exports.Start = Start;
+
+function _Selected2 () {
+ this.name = 'Selected2';
+}
+inherits(_Selected2, _State);
+var Selected2 = new _Selected2();
+exports.Selected2 = Selected2;
+
+function _Selected3 () {
+ this.name = 'Selected3';
+}
+inherits(_Selected3, _State);
+var Selected3 = new _Selected3();
+exports.Selected3 = Selected3;
+
+function _Selected1 () {
+ this.name = 'Selected1';
+}
+inherits(_Selected1, _State);
+var Selected1 = new _Selected1();
+exports.Selected1 = Selected1;
+
+function _ContextMenu () {
+ this.name = 'ContextMenu';
+}
+inherits(_ContextMenu, _State);
+var ContextMenu = new _ContextMenu();
+exports.ContextMenu = ContextMenu;
+
+
+_State.prototype.onUnselectAll = function (controller, msg_type, $event) {
+
+ controller.changeState(Ready);
+ controller.delegate_channel.send(msg_type, $event);
+};
+
+
+_Ready.prototype.onMouseDown = function (controller, msg_type, $event) {
+
+ var last_selected = controller.scope.select_items($event.shiftKey);
+
+ if (last_selected.last_selected_device !== null) {
+ controller.changeState(Selected1);
+ } else if (last_selected.last_selected_link !== null) {
+ controller.changeState(Selected1);
+ } else if (last_selected.last_selected_interface !== null) {
+ controller.changeState(Selected1);
+ } else {
+ controller.delegate_channel.send(msg_type, $event);
+ }
+};
+_Ready.prototype.onMouseDown.transitions = ['Selected1'];
+
+_Start.prototype.start = function (controller) {
+
+ controller.changeState(Ready);
+
+};
+_Start.prototype.start.transitions = ['Ready'];
+
+
+_Selected2.prototype.onMouseDown = function (controller, msg_type, $event) {
+
+ var last_selected = null;
+
+ if (controller.scope.selected_devices.length === 1) {
+ var current_selected_device = controller.scope.selected_devices[0];
+ var last_selected_device = controller.scope.select_items($event.shiftKey).last_selected_device;
+ if (current_selected_device === last_selected_device) {
+ controller.changeState(Selected3);
+ return;
+ }
+ }
+
+ if (controller.scope.selected_links.length === 1) {
+ var current_selected_link = controller.scope.selected_links[0];
+ last_selected = controller.scope.select_items($event.shiftKey);
+ if (current_selected_link === last_selected.last_selected_link) {
+ controller.changeState(Selected3);
+ return;
+ }
+ }
+
+ if (controller.scope.selected_interfaces.length === 1) {
+ var current_selected_interface = controller.scope.selected_interfaces[0];
+ last_selected = controller.scope.select_items($event.shiftKey);
+ if (current_selected_interface === last_selected.last_selected_interface) {
+ controller.changeState(Selected3);
+ return;
+ }
+ }
+ controller.scope.first_channel.send('BindDocument', {});
+ controller.changeState(Ready);
+ controller.handle_message(msg_type, $event);
+};
+_Selected2.prototype.onMouseDown.transitions = ['Ready', 'Selected3'];
+
+
+_Selected1.prototype.onMouseUp = function (controller) {
+
+ if(controller.scope.$parent.vm.rightPanelIsExpanded){
+ controller.scope.onDetailsContextButton();
+ }
+ controller.changeState(Selected2);
+
+};
+_Selected1.prototype.onMouseUp.transitions = ['Selected2'];
+
+_Selected1.prototype.onMouseDown = util.noop;
+
+_Selected3.prototype.onMouseUp = function (controller, msg_type, $event) {
+ let context_menu = controller.scope.context_menus[0];
+ context_menu.enabled = true;
+ context_menu.x = $event.x;
+ context_menu.y = $event.y;
+ context_menu.buttons.forEach(function(button, index){
+ button.x = $event.x;
+ let menuPaddingTop = 5;
+ button.y = $event.y + menuPaddingTop + (button.height * index);
+ });
+
+ controller.changeState(ContextMenu);
+};
+_Selected3.prototype.onMouseUp.transitions = ['ContextMenu'];
+
+_Selected3.prototype.onMouseMove = function (controller) {
+ controller.changeState(Selected2);
+};
+_Selected3.prototype.onMouseMove.transitions = ['Selected2'];
+
+_ContextMenu.prototype.end = function (controller) {
+
+ controller.scope.removeContextMenu();
+};
+
+_ContextMenu.prototype.onMouseDown = function (controller) {
+
+ controller.changeState(Selected2);
+
+};
+_ContextMenu.prototype.onMouseDown.transitions = ['Selected2'];
+
+_ContextMenu.prototype.onDetailsPanel = function (controller, msg_type, $event) {
+
+ controller.changeState(Selected2);
+ controller.handle_message(msg_type, $event);
+};
+_ContextMenu.prototype.onDetailsPanel.transitions = ['Selected2'];
diff --git a/awx/ui/client/src/network-ui/network-details/details.block.less b/awx/ui/client/src/network-ui/network-details/details.block.less
new file mode 100644
index 0000000000..802e67fd3e
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network-details/details.block.less
@@ -0,0 +1,29 @@
+.Networking-panelHeader {
+ display: flex;
+ height: 30px;
+ width:100%;
+}
+
+.Networking-panelHeaderText {
+ color: @default-interface-txt;
+ flex: 1 0 auto;
+ font-size: 14px;
+ font-weight: bold;
+ margin-right: 10px;
+ text-transform: uppercase;
+}
+
+.Networking-noItems{
+ margin-top: 0px;
+}
+
+.Networking-form{
+ font-weight: normal;
+}
+
+.Networking-saveConfirmation{
+ font-weight: normal;
+ color: @default-succ;
+ text-align: right;
+ margin-top:15px;
+}
diff --git a/awx/ui/client/src/network-ui/network-details/details.controller.js b/awx/ui/client/src/network-ui/network-details/details.controller.js
new file mode 100644
index 0000000000..cf629ffa62
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network-details/details.controller.js
@@ -0,0 +1,44 @@
+/*************************************************
+ * Copyright (c) 2016 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+ export default
+ ['$scope', '$state', '$stateParams', 'GenerateForm', 'ParseTypeChange', 'HostsService',
+ function($scope, $state, $stateParams, GenerateForm, ParseTypeChange, HostsService){
+
+ $scope.parseType = 'yaml';
+ $scope.formCancel = function(){
+ $scope.$parent.$broadcast('awxNet-closeDetailsPanel');
+ };
+
+ $scope.formSave = function(){
+ var host = {
+ id: $scope.item.id,
+ variables: $scope.variables === '---' || $scope.variables === '{}' ? null : $scope.variables,
+ name: $scope.item.name,
+ description: $scope.item.description,
+ enabled: $scope.item.enabled
+ };
+ HostsService.put(host).then(function(response){
+ $scope.saveConfirmed = true;
+ if(_.has(response, "data")){
+ $scope.$parent.$broadcast('awxNet-hostUpdateSaved', response.data);
+ }
+ setTimeout(function(){
+ $scope.saveConfirmed = false;
+ }, 3000);
+ });
+
+ };
+
+ $scope.$parent.$on('awxNet-showDetails', (e, data, canAdd) => {
+ if (!_.has(data, 'host_id')) {
+ $scope.item = data;
+ $scope.canAdd = canAdd;
+ } else {
+ $scope.item = data;
+ }
+ });
+ }];
diff --git a/awx/ui/client/src/network-ui/network-details/details.directive.js b/awx/ui/client/src/network-ui/network-details/details.directive.js
new file mode 100644
index 0000000000..d05f364390
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network-details/details.directive.js
@@ -0,0 +1,22 @@
+/*************************************************
+ * Copyright (c) 2018 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+import detailsController from './details.controller';
+
+const templateUrl = require('~network-ui/network-details/details.partial.html');
+
+export default [
+ function() {
+ return {
+ scope:{
+ item: "=",
+ canAdd: '@'
+ },
+ templateUrl,
+ controller: detailsController,
+ restrict: 'E',
+ };
+}];
diff --git a/awx/ui/client/src/network-ui/network-details/details.partial.html b/awx/ui/client/src/network-ui/network-details/details.partial.html
new file mode 100644
index 0000000000..5b52ae1e6a
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network-details/details.partial.html
@@ -0,0 +1,43 @@
+
+
+
+ {{item.type}} DETAILS NOT AVAILABLE
+
+
+
+
+
+ Cancel
+
+
+ Save Complete
+
+
diff --git a/awx/ui/client/src/network-ui/network-details/main.js b/awx/ui/client/src/network-ui/network-details/main.js
new file mode 100644
index 0000000000..1827ce7297
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network-details/main.js
@@ -0,0 +1,13 @@
+/*************************************************
+ * Copyright (c) 2018 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+import awxNetDetailsPanel from './details.directive';
+import awxNetExtraVars from './network-extra-vars/network-extra-vars.directive';
+
+export default
+ angular.module('networkDetailsDirective', [])
+ .directive('awxNetDetailsPanel', awxNetDetailsPanel)
+ .directive('awxNetExtraVars', awxNetExtraVars);
diff --git a/awx/ui/client/src/network-ui/network-details/network-extra-vars/network-extra-vars.block.less b/awx/ui/client/src/network-ui/network-details/network-extra-vars/network-extra-vars.block.less
new file mode 100644
index 0000000000..9e340cccec
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network-details/network-extra-vars/network-extra-vars.block.less
@@ -0,0 +1,168 @@
+.NetworkingExtraVarsLabel{
+ display: flex;
+ width: 100%;
+}
+
+.NetworkingExtraVars-extraVarsLabelContainer{
+ flex: 1 0 auto;
+}
+
+.NetworkingExtraVars-expandTextContainer{
+ flex: 1 0 auto;
+ text-align: right;
+ font-weight: normal;
+ color: @default-link;
+ cursor: pointer;
+ font-size: 12px;
+}
+
+.noselect {
+ -webkit-touch-callout: none; /* iOS Safari */
+ -webkit-user-select: none; /* Chrome/Safari/Opera */
+ -khtml-user-select: none; /* Konqueror */
+ -moz-user-select: none; /* Firefox */
+ -ms-user-select: none; /* Internet Explorer/Edge */
+ user-select: none; /* Non-prefixed version, currently
+ not supported by any browser */
+}
+
+@media screen and (min-width: 768px){
+ .NetworkingExtraVars .modal-dialog{
+ width: 700px;
+ }
+}
+
+.NetworkingExtraVarsModal .modal-dialog{
+ width: calc(~"100% - 200px");
+ height: calc(~"100vh - 80px");
+}
+
+.NetworkingExtraVarsModal .modal-content{
+ height: 100%;
+}
+
+.NetworkingExtraVars .CodeMirror{
+ overflow-x: hidden;
+}
+
+.NetworkingExtraVars-close:hover{
+ color: @btn-txt;
+ background-color: @btn-bg-hov;
+}
+
+.NetworkingExtraVars-body{
+ margin-bottom: 20px;
+}
+
+.NetworkingExtraVars-tab:hover {
+ color: @btn-txt;
+ background-color: @btn-bg-hov;
+ cursor: pointer;
+}
+.NetworkingExtraVars-tab--selected{
+ color: @btn-txt-sel!important;
+ background-color: @default-icon!important;
+ border-color: @default-icon!important;
+}
+.NetworkingExtraVars-view--container{
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+}
+.NetworkingExtraVars .modal-footer{
+ border: 0;
+ margin-top: 0px;
+ padding-top: 5px;
+}
+.NetworkingExtraVars-controls{
+ float: right;
+ margin-top: 15px;
+ button {
+ margin-left: 10px;
+ }
+}
+
+.NetworkingExtraVars-header{
+ padding-bottom: 15px;
+}
+.NetworkingExtraVars-title{
+ color: @default-interface-txt;
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+.NetworkingExtraVarsModal .modal-body{
+ padding: 0px!important;
+ overflow-y: auto;
+}
+.NetworkingExtraVars-nav{
+ padding-top: 12px;
+ padding-bottom: 20px;
+}
+.NetworkingExtraVars-field{
+ margin-bottom: 8px;
+ flex: 0 1 12em;
+}
+.NetworkingExtraVars-field--label{
+ text-transform: uppercase;
+ flex: 0 1 80px;
+ max-width: 80px;
+ min-width: 80px;
+ font-size: 12px;
+ word-wrap: break-word;
+}
+.NetworkingExtraVars-field{
+ .OnePlusTwo-left--detailsRow;
+}
+.NetworkingExtraVars-field--content{
+ word-wrap: break-word;
+}
+.NetworkingExtraVars-field--monospaceContent{
+ font-family: monospace;
+}
+.NetworkingExtraVars-button:disabled {
+ pointer-events: all!important;
+}
+
+.NetworkingExtraVars-numberColumnPreload {
+ background-color: @default-list-header-bg;
+ height: 198px;
+ border-right: 1px solid #ccc;
+ width: 30px;
+ position: fixed;
+}
+
+.NetworkingExtraVars-numberColumn {
+ background-color: @default-list-header-bg;
+ border-right: 1px solid #ccc;
+ border-bottom-left-radius: 5px;
+ color: #999;
+ font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
+ position: fixed;
+ padding: 4px 3px 0 5px;
+ text-align: right;
+ white-space: nowrap;
+ width: 30px;
+}
+
+.NetworkingExtraVars-numberColumn--second{
+ padding-top:0px;
+}
+
+.NetworkingExtraVars-noJson{
+ align-items: center;
+ background-color: @default-no-items-bord;
+ border: 1px solid @default-icon-hov;
+ border-radius: 5px;
+ color: @b7grey;
+ display: flex;
+ height: 200px;
+ justify-content: center;
+ text-transform: uppercase;
+ width: 100%;
+}
+
+.NetworkingExtraVarsModal .CodeMirror{
+ max-height: none;
+}
diff --git a/awx/ui/client/src/network-ui/network-details/network-extra-vars/network-extra-vars.directive.js b/awx/ui/client/src/network-ui/network-details/network-extra-vars/network-extra-vars.directive.js
new file mode 100644
index 0000000000..35cf89539d
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network-details/network-extra-vars/network-extra-vars.directive.js
@@ -0,0 +1,70 @@
+/*************************************************
+ * Copyright (c) 2018 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+const templateUrl = require('~network-ui/network-details/network-extra-vars/network-extra-vars.partial.html');
+
+export default [ 'ParseTypeChange', 'ParseVariableString',
+ function(ParseTypeChange, ParseVariableString) {
+ return {
+ scope:{
+ item: "="
+ },
+ templateUrl,
+ restrict: 'E',
+ link(scope){
+ scope.networkingExtraVarsModalOpen = true;
+ function init(){
+ if(scope.item && scope.item.host_id){
+ scope.variables = ParseVariableString(scope.item.variables);
+ scope.parseType = 'yaml';
+ ParseTypeChange({
+ scope: scope,
+ field_id: 'network_host_variables',
+ variable: 'variables',
+ readOnly: true
+ });
+ }
+ }
+
+ scope.$watch('item', function(){
+ init();
+ });
+
+ scope.closeExtraVarModal = function() {
+ // Unbind the listener so it doesn't fire when we close the modal via navigation
+ $('.CodeMirror')[1].remove();
+ $('#NetworkingExtraVarsModal').off('hidden.bs.modal');
+ $('#NetworkingExtraVarsModal').modal('hide');
+ scope.networkingExtraVarsModalOpen = false;
+ };
+
+ scope.openExtraVarsModal = function(){
+ scope.networkingExtraVarsModalOpen = true;
+ $('#NetworkingExtraVarsModal').modal('show');
+
+ $('.modal-dialog').on('resize', function(){
+ resize();
+ });
+ scope.extra_variables = ParseVariableString(_.cloneDeep(scope.item.variables));
+ scope.parseType = 'yaml';
+ ParseTypeChange({
+ scope: scope,
+ field_id: 'NetworkingExtraVars-codemirror',
+ variable: 'extra_variables',
+ readOnly: true
+ });
+ resize();
+ };
+
+ function resize(){
+ let editor = $('.CodeMirror')[1].CodeMirror;
+ let height = $('#NetworkingExtraVarsModalDialog').height() - $('.NetworkingExtraVars-header').height() - $('.NetworkingExtraVars-controls').height() - 110;
+ editor.setSize("100%", height);
+ }
+
+ }
+ };
+}];
diff --git a/awx/ui/client/src/network-ui/network-details/network-extra-vars/network-extra-vars.partial.html b/awx/ui/client/src/network-ui/network-details/network-extra-vars/network-extra-vars.partial.html
new file mode 100644
index 0000000000..1ac026747e
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network-details/network-extra-vars/network-extra-vars.partial.html
@@ -0,0 +1,72 @@
+
+
+
+
+
diff --git a/awx/ui/client/src/network-ui/network-nav/main.js b/awx/ui/client/src/network-ui/network-nav/main.js
new file mode 100644
index 0000000000..666130a0af
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network-nav/main.js
@@ -0,0 +1,56 @@
+import NetworkingController from './network.nav.controller';
+import NetworkingStrings from './network.nav.strings';
+
+const MODULE_NAME = 'at.features.networking';
+
+const networkNavTemplate = require('~network-ui/network-nav/network.nav.view.html');
+
+function NetworkingResolve ($stateParams, resourceData) {
+ const resolve = {
+ inventory: {
+ id: $stateParams.inventory_id,
+ name: $stateParams.inventory_name
+ },
+ canEdit: resourceData.data.summary_fields.user_capabilities.edit
+ };
+ if (!resolve.inventory.name) {
+ resolve.inventory.name = resourceData.data.name;
+ }
+ return resolve;
+}
+
+NetworkingResolve.$inject = [
+ '$stateParams',
+ 'resourceData'
+];
+function NetworkingRun ($stateExtender, strings) {
+ $stateExtender.addState({
+ name: 'inventories.edit.networking',
+ route: '/networking',
+ ncyBreadcrumb: {
+ label: strings.get('state.BREADCRUMB_LABEL')
+ },
+ views: {
+ 'networking@': {
+ templateUrl: networkNavTemplate,
+ controller: NetworkingController,
+ controllerAs: 'vm'
+ }
+ },
+ resolve: {
+ resolvedModels: NetworkingResolve
+ }
+ });
+}
+
+NetworkingRun.$inject = [
+ '$stateExtender',
+ 'NetworkingStrings'
+];
+
+angular
+ .module(MODULE_NAME, [])
+ .service('NetworkingStrings', NetworkingStrings)
+ .run(NetworkingRun);
+
+export default MODULE_NAME;
diff --git a/awx/ui/client/src/network-ui/network-nav/network.nav.block.less b/awx/ui/client/src/network-ui/network-nav/network.nav.block.less
new file mode 100644
index 0000000000..8cb4006389
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network-nav/network.nav.block.less
@@ -0,0 +1,203 @@
+.Networking-shell{
+ display:flex;
+ flex-direction: column;
+ width:100%;
+ align-items: flex-end;
+ position:absolute;
+}
+
+.Networking-top{
+ width:100%;
+}
+
+.Networking-header{
+ border-top: 1px solid @at-color-panel-border;
+ border-bottom: 1px solid @at-color-panel-border;
+ display:flex;
+ height: 40px;
+ width:100%;
+ background-color: @default-bg;
+}
+
+.Networking-headerTitle{
+ color: @default-interface-txt;
+ flex: 1 0 auto;
+ font-size: 14px;
+ font-weight: bold;
+ padding-left: 20px;
+ align-items: center;
+ display: flex;
+}
+
+.Netowrking-headerActions{
+ align-items: center;
+ display: flex;
+ flex: 1 0 auto;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ max-width: 100%;
+}
+
+.Networking-headerActionItem{
+ justify-content: flex-end;
+ display: flex;
+ padding-right: 20px;
+ font-size: 20px;
+}
+
+.Networking-toolbarIcon{
+ font-size: 16px;
+ height: 30px;
+ min-width: 30px;
+ color: @default-icon;
+ background-color: inherit;
+ border: none;
+ border-radius: 5px;
+ margin-left: 5px;
+}
+
+.Networking-toolbarIcon:hover{
+ background-color:@default-link;
+ color: @default-bg;
+}
+
+.Networking-toolbarIcon--selected{
+ background-color:@default-link;
+ color: @default-bg;
+ border-bottom-right-radius: 0px;
+ border-bottom-left-radius: 0px;
+}
+
+.Networking-canvasPanel{
+ width:100%
+}
+
+.Networking-detailPanel{
+ border-left: 1px solid @at-color-panel-border;
+ border-bottom: 1px solid @at-color-panel-border;
+ width:400px;
+ height: calc(~"100vh - 115px");
+ padding: 20px;
+ color: @default-interface-txt;
+ font-size: 14px;
+ font-weight: bold;
+ background-color: @default-bg;
+ position: absolute;
+ top:115px;
+ right:0px;
+}
+
+.Networking-toolbar{
+ min-height: 40px;
+ width:100%;
+ background-color: @ebgrey;
+ display:flex;
+ flex: 1 0 auto;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ align-items: center;
+ border-bottom: 1px solid @at-color-panel-border;
+}
+
+.Networking-toolbarLeftSide{
+ display: flex;
+ flex-wrap: wrap;
+ min-width: 400px;
+ padding-left: 20px;
+ height: 40px;
+ align-items: center;
+}
+
+.Networking-toolbarRightSide{
+ align-items: center;
+ flex-wrap: wrap;
+ display: flex;
+ min-width: 500px;
+ padding-right: 20px;
+ height: 40px;
+ justify-content: flex-end;
+}
+
+.Networking-actionsDropDownContainer{
+ height: 30px;
+ flex: 1 0 auto;
+ display: flex;
+ margin-top:-5px;
+ margin-left: -2px;
+}
+
+.Networking-searchBarContainer{
+ height: 30px;
+ display: flex;
+ margin-top:-5px;
+ margin-left: 5px;
+}
+
+.Networking-dropDown{
+ left:-2px!important;
+}
+
+.Networking-searchButton{
+ padding: 4px 10px;
+}
+
+.Networking-searchButton i{
+ color:@default-icon;
+}
+
+
+.Networking-dropdownPanelTitle{
+ color: @default-stdout-txt;
+ padding-left:15px;
+ min-height: 30px;
+ font-size: 14px;
+ font-weight: bold;
+ display: flex;
+ align-items: center;
+}
+
+.Networking-keyContainer{
+ display: inline-block;
+ position: relative;
+}
+
+.Networking-keyDropDownPanel{
+ width: 180px;
+ padding: 10px 0px 10px 0px;
+ border: 1px solid @btn-bord;
+ background-color: white;
+ position: absolute;
+ right:0px;
+ z-index: 100;
+}
+
+.Networking-keyPanelOption{
+ color: @default-stdout-txt;
+ padding-left:15px;
+ min-height: 30px;
+ font-size: 12px;
+ display:flex;
+ align-items: center;
+}
+
+.Networking-keySymbol{
+ background-color: @default-icon;
+ color: white;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ font-size: 14px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.Networking-keySymbolLabel{
+ font-size: 12px;
+ padding-left: 15px;
+ color: @default-stdout-txt
+}
+
+.Networking-toolboxPanelToolbarIcon--selected{
+ border-radius: 5px;
+}
diff --git a/awx/ui/client/src/network-ui/network-nav/network.nav.controller.js b/awx/ui/client/src/network-ui/network-nav/network.nav.controller.js
new file mode 100644
index 0000000000..f05bee4dd7
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network-nav/network.nav.controller.js
@@ -0,0 +1,121 @@
+/* eslint-disable */
+function NetworkingController (models, $state, $scope, strings) {
+ const vm = this || {};
+
+ const {
+ inventory
+ } = models;
+
+ vm.strings = strings;
+ vm.panelTitle = `${strings.get('state.BREADCRUMB_LABEL')} | ${inventory.name}`;
+ vm.hostDetail = {};
+ vm.canEdit = models.canEdit;
+ vm.rightPanelIsExpanded = false;
+ vm.leftPanelIsExpanded = true;
+ vm.keyPanelExpanded = false;
+ vm.groups = [];
+ $scope.devices = [];
+ vm.close = () => {
+ $state.go('inventories');
+ };
+
+ vm.key = () => {
+ vm.keyPanelExpanded = !vm.keyPanelExpanded;
+ };
+
+ vm.hideToolbox = () => {
+ vm.leftPanelIsExpanded = !vm.leftPanelIsExpanded;
+ $scope.$broadcast('awxNet-hideToolbox', vm.leftPanelIsExpanded);
+ };
+
+ $scope.$on('awxNet-instatiateSelect', (e, devices) => {
+ for(var i = 0; i < devices.length; i++){
+ let device = devices[i];
+ $scope.devices.push({
+ value: device.id,
+ text: device.name,
+ label: device.name,
+ id: device.id,
+ type: device.type
+ });
+ }
+
+ $("#networking-search").select2({
+ width:'400px',
+ containerCssClass: 'Form-dropDown',
+ placeholder: 'SEARCH'
+ });
+ $("#networking-actionsDropdown").select2({
+ width:'400px',
+ containerCssClass: 'Form-dropDown',
+ minimumResultsForSearch: -1,
+ placeholder: 'ACTIONS'
+ });
+ });
+
+ $scope.$on('awxNet-addSearchOption', (e, device) => {
+ $scope.devices.push({
+ value: device.id,
+ text: device.name,
+ label: device.name,
+ id: device.id,
+ type: device.type
+ });
+ });
+
+ $scope.$on('awxNet-editSearchOption', (e, device) => {
+ for(var i = 0; i < $scope.devices.length; i++){
+ if(device.id === $scope.devices[i].id){
+ $scope.devices[i].text = device.name;
+ $scope.devices[i].label = device.name;
+ }
+ }
+ });
+
+ $scope.$on('awxNet-removeSearchOption', (e, device) => {
+ for (var i = 0; i < $scope.devices.length; i++) {
+ if ($scope.devices[i].id === device.id) {
+ $scope.devices.splice(i, 1);
+ }
+ }
+ });
+
+ //Handlers for actions drop down
+ $('#networking-actionsDropdown').on('select2:select', (e) => {
+ $scope.$broadcast('awxNet-toolbarButtonEvent', e.params.data.title);
+ });
+
+ $('#networking-actionsDropdown').on('select2:open', () => {
+ $('.select2-dropdown').addClass('Networking-dropDown');
+ });
+
+ // Handlers for search dropdown
+ $('#networking-search').on('select2:select', () => {
+ $scope.$broadcast('awxNet-search', $scope.device);
+ });
+
+ $('#networking-search').on('select2:open', () => {
+ $('.select2-dropdown').addClass('Networking-dropDown');
+ $scope.$broadcast('awxNet-SearchDropdown');
+ });
+
+ $('#networking-search').on('select2:close', () => {
+ setTimeout(function() {
+ $('.select2-container-active').removeClass('select2-container-active');
+ $(':focus').blur();
+ }, 1);
+ $scope.$broadcast('awxNet-SearchDropdownClose');
+ });
+
+}
+
+NetworkingController.$inject = [
+ 'resolvedModels',
+ '$state',
+ '$scope',
+ 'NetworkingStrings',
+ 'CreateSelect2'
+];
+
+export default NetworkingController;
+/* eslint-disable */
diff --git a/awx/ui/client/src/network-ui/network-nav/network.nav.strings.js b/awx/ui/client/src/network-ui/network-nav/network.nav.strings.js
new file mode 100644
index 0000000000..4aa4efca5e
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network-nav/network.nav.strings.js
@@ -0,0 +1,19 @@
+function NetworkingStrings (BaseString) {
+ BaseString.call(this, 'networking');
+
+ const { t } = this;
+ const ns = this.networking;
+
+ ns.state = {
+ BREADCRUMB_LABEL: t.s('INVENTORIES'),
+ };
+
+ ns.actions = {
+ EXPAND_PANEL: t.s('Expand Panel'),
+ COLLAPSE_PANEL: t.s('Collapse Panel')
+ };
+}
+
+NetworkingStrings.$inject = ['BaseStringService'];
+
+export default NetworkingStrings;
diff --git a/awx/ui/client/src/network-ui/network-nav/network.nav.view.html b/awx/ui/client/src/network-ui/network-nav/network.nav.view.html
new file mode 100644
index 0000000000..1e29d9a083
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network-nav/network.nav.view.html
@@ -0,0 +1,99 @@
+
+
+
+
+
diff --git a/awx/ui/client/src/network-ui/network.ui.app.js b/awx/ui/client/src/network-ui/network.ui.app.js
new file mode 100644
index 0000000000..bc71fd4a3e
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network.ui.app.js
@@ -0,0 +1,45 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+
+import atFeaturesNetworking from './network-nav/main';
+import networkDetailsDirective from './network-details/main';
+import networkZoomWidget from './zoom-widget/main';
+
+//console.log = function () { };
+var angular = require('angular');
+var NetworkUIController = require('./network.ui.controller.js');
+var cursor = require('./cursor.directive.js');
+var router = require('./router.directive.js');
+var switchd = require('./switch.directive.js');
+var host = require('./host.directive.js');
+var link = require('./link.directive.js');
+var contextMenu = require('./context.menu.directive.js');
+var contextMenuButton = require('./context.menu.button.directive.js');
+var defaultd = require('./default.directive.js');
+var quadrants = require('./quadrants.directive.js');
+var inventoryToolbox = require('./inventory.toolbox.directive.js');
+var debug = require('./debug.directive.js');
+var test_results = require('./test_results.directive.js');
+var awxNetworkUI = require('./network.ui.directive.js');
+
+var networkUI = angular.module('networkUI', [
+ 'monospaced.mousewheel',
+ atFeaturesNetworking,
+ networkDetailsDirective.name,
+ networkZoomWidget.name
+ ])
+ .controller('NetworkUIController', NetworkUIController.NetworkUIController)
+ .directive('awxNetCursor', cursor.cursor)
+ .directive('awxNetDebug', debug.debug)
+ .directive('awxNetRouter', router.router)
+ .directive('awxNetSwitch', switchd.switchd)
+ .directive('awxNetHost', host.host)
+ .directive('awxNetLink', link.link)
+ .directive('awxNetContextMenu', contextMenu.contextMenu)
+ .directive('awxNetContextMenuButton', contextMenuButton.contextMenuButton)
+ .directive('awxNetDefault', defaultd.defaultd)
+ .directive('awxNetQuadrants', quadrants.quadrants)
+ .directive('awxNetInventoryToolbox', inventoryToolbox.inventoryToolbox)
+ .directive('awxNetTestResults', test_results.test_results)
+ .directive('awxNetworkUi', awxNetworkUI.awxNetworkUI);
+
+exports.networkUI = networkUI;
diff --git a/awx/ui/client/src/network-ui/network.ui.controller.js b/awx/ui/client/src/network-ui/network.ui.controller.js
new file mode 100644
index 0000000000..de30650caa
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network.ui.controller.js
@@ -0,0 +1,1462 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+var angular = require('angular');
+var fsm = require('./fsm.js');
+var mode_fsm = require('./mode.fsm.js');
+var hotkeys = require('./hotkeys.fsm.js');
+var toolbox_fsm = require('./toolbox.fsm.js');
+var view = require('./view.fsm.js');
+var move = require('./move.fsm.js');
+var move_readonly = require('./move.readonly.fsm.js');
+var buttons = require('./buttons.fsm.js');
+var time = require('./time.fsm.js');
+var test_fsm = require('./test.fsm.js');
+var util = require('./util.js');
+var models = require('./models.js');
+var messages = require('./messages.js');
+var animations = require('./animations.js');
+var keybindings = require('./keybindings.fsm.js');
+var details_panel_fsm = require('./details.panel.fsm.js');
+var svg_crowbar = require('./vendor/svg-crowbar.js');
+var ReconnectingWebSocket = require('reconnectingwebsocket');
+
+var NetworkUIController = function($scope,
+ $document,
+ $location,
+ $window,
+ $http,
+ $q,
+ $state,
+ ProcessErrors,
+ ConfigService,
+ rbacUiControlService) {
+
+ window.scope = $scope;
+
+ $scope.http = $http;
+
+ $scope.api_token = '';
+ $scope.disconnected = false;
+ $scope.tests_enabled = false;
+
+ $scope.topology_id = 0;
+ // Create a web socket to connect to the backend server
+
+ $scope.inventory_id = $state.params.inventory_id;
+
+ var protocol = null;
+
+ if ($location.protocol() === 'http') {
+ protocol = 'ws';
+ } else if ($location.protocol() === 'https') {
+ protocol = 'wss';
+ }
+
+ $scope.initial_messages = [];
+ if (!$scope.disconnected) {
+ $scope.control_socket = new ReconnectingWebSocket(protocol + "://" + window.location.host + "/network_ui/topology?inventory_id=" + $scope.inventory_id,
+ null,
+ {debug: false, reconnectInterval: 300});
+ if ($scope.tests_enabled) {
+ $scope.test_socket = new ReconnectingWebSocket(protocol + "://" + window.location.host + "/network_ui/test?inventory_id=" + $scope.inventory_id,
+ null,
+ {debug: false, reconnectInterval: 300});
+ } else {
+ $scope.test_socket = {
+ on_message: util.noop,
+ send: util.noop
+ };
+ }
+
+ } else {
+ $scope.control_socket = {
+ on_message: util.noop
+ };
+ }
+ $scope.my_location = $location.protocol() + "://" + $location.host() + ':' + $location.port();
+ $scope.client_id = 0;
+ $scope.test_client_id = 0;
+ $scope.onMouseDownResult = "";
+ $scope.onMouseUpResult = "";
+ $scope.onMouseEnterResult = "";
+ $scope.onMouseLeaveResult = "";
+ $scope.onMouseMoveResult = "";
+ $scope.onMouseMoveResult = "";
+ $scope.current_scale = 1.0;
+ $scope.current_mode = null;
+ $scope.panX = 0;
+ $scope.panY = 0;
+ $scope.mouseX = 0;
+ $scope.mouseY = 0;
+ $scope.scaledX = 0;
+ $scope.scaledY = 0;
+ $scope.pressedX = 0;
+ $scope.pressedY = 0;
+ $scope.pressedScaledX = 0;
+ $scope.pressedScaledY = 0;
+ $scope.lastPanX = 0;
+ $scope.lastPanY = 0;
+ $scope.selected_devices = [];
+ $scope.selected_links = [];
+ $scope.selected_interfaces = [];
+ $scope.selected_items = [];
+ $scope.new_link = null;
+ $scope.new_stream = null;
+ $scope.last_key = "";
+ $scope.last_key_code = null;
+ $scope.last_event = null;
+ $scope.cursor = {'x':100, 'y': 100, 'hidden': true};
+
+ $scope.debug = {'hidden': true};
+ $scope.hide_buttons = false;
+ $scope.hide_menus = false;
+ $scope.hide_links = false;
+ $scope.hide_interfaces = false;
+ $scope.graph = {'width': window.innerWidth,
+ 'right_column': 300,
+ 'height': window.innerHeight};
+ $scope.MAX_ZOOM = 5;
+ $scope.MIN_ZOOM = 0.1;
+ $scope.device_id_seq = util.natural_numbers(0);
+ $scope.link_id_seq = util.natural_numbers(0);
+ $scope.message_id_seq = util.natural_numbers(0);
+ $scope.test_result_id_seq = util.natural_numbers(0);
+ $scope.animation_id_seq = util.natural_numbers(0);
+ $scope.overall_toolbox_collapsed = false;
+ $scope.time_pointer = -1;
+ $scope.frame = 0;
+ $scope.recording = false;
+ $scope.replay = false;
+ $scope.devices = [];
+ $scope.devices_by_name = {};
+ $scope.links = [];
+ $scope.links_in_vars_by_device = {};
+ $scope.tests = [];
+ $scope.current_tests = [];
+ $scope.current_test = null;
+ $scope.template_building = false;
+ $scope.version = null;
+ $scope.test_events = [];
+ $scope.test_results = [];
+ $scope.test_errors = [];
+ $scope.animations = [];
+ $scope.sequences = {};
+ $scope.view_port = {'x': 0,
+ 'y': 0,
+ 'width': 0,
+ 'height': 0,
+ };
+ $scope.trace_id_seq = util.natural_numbers(0);
+ $scope.trace_order_seq = util.natural_numbers(0);
+ $scope.trace_id = $scope.trace_id_seq();
+ $scope.jump = {from_x: 0,
+ from_y: 0,
+ to_x: 0,
+ to_y: 0};
+ $scope.canEdit = $scope.$parent.$resolve.resolvedModels.canEdit;
+ $scope.send_trace_message = function (message) {
+ if (!$scope.recording) {
+ return;
+ }
+ message.sender = $scope.test_client_id;
+ message.trace_id = $scope.trace_id;
+ message.message_id = $scope.message_id_seq();
+ var data = messages.serialize(message);
+ if (!$scope.disconnected) {
+ try {
+ $scope.test_socket.send(data);
+ }
+ catch(err) {
+ $scope.initial_messages.push(message);
+ }
+ }
+ };
+
+ $scope.onKeyDown = function ($event) {
+ if ($scope.recording) {
+ $scope.send_test_message(new messages.KeyEvent($scope.test_client_id,
+ $event.key,
+ $event.keyCode,
+ $event.type,
+ $event.altKey,
+ $event.shiftKey,
+ $event.ctrlKey,
+ $event.metaKey,
+ $scope.trace_id));
+ }
+ $scope.last_event = $event;
+ $scope.last_key = $event.key;
+ $scope.last_key_code = $event.keyCode;
+ $scope.first_channel.send('KeyDown', $event);
+ $scope.$apply();
+ $event.preventDefault();
+ };
+
+ //Define the FSMs
+ $scope.hotkeys_controller = new fsm.FSMController($scope, "hotkeys_fsm", hotkeys.Start, $scope);
+ $scope.keybindings_controller = new fsm.FSMController($scope, "keybindings_fsm", keybindings.Start, $scope);
+ $scope.view_controller = new fsm.FSMController($scope, "view_fsm", view.Start, $scope);
+ $scope.move_controller = new fsm.FSMController($scope, "move_fsm", move.Start, $scope);
+ $scope.move_readonly_controller = new fsm.FSMController($scope, "move_readonly_fsm", move_readonly.Start, $scope);
+ $scope.details_panel_controller = new fsm.FSMController($scope, "details_panel_fsm", details_panel_fsm.Start, $scope);
+ $scope.buttons_controller = new fsm.FSMController($scope, "buttons_fsm", buttons.Start, $scope);
+ $scope.time_controller = new fsm.FSMController($scope, "time_fsm", time.Start, $scope);
+ $scope.test_controller = new fsm.FSMController($scope, "test_fsm", test_fsm.Start, $scope);
+
+ $scope.inventory_toolbox_controller = new fsm.FSMController($scope, "toolbox_fsm", toolbox_fsm.Start, $scope);
+
+ var toolboxTopMargin = $('.Networking-top').height();
+ var toolboxTitleMargin = toolboxTopMargin + 35;
+ var toolboxHeight = $scope.graph.height - $('.Networking-top').height();
+
+ $scope.update_links_in_vars_by_device = function (device_name, variables) {
+
+ var j = 0;
+ var link = null;
+
+ if (variables.ansible_topology !== undefined) {
+ if (variables.ansible_topology.links !== undefined) {
+ for (j=0; j < variables.ansible_topology.links.length; j++) {
+ link = variables.ansible_topology.links[j];
+ if (link.remote_device_name !== undefined &&
+ link.remote_interface_name !== undefined &&
+ link.name !== undefined) {
+ if ($scope.links_in_vars_by_device[device_name] === undefined) {
+ $scope.links_in_vars_by_device[device_name] = [];
+ }
+ if ($scope.links_in_vars_by_device[link.remote_device_name] === undefined) {
+ $scope.links_in_vars_by_device[link.remote_device_name] = [];
+ }
+ $scope.links_in_vars_by_device[device_name].push({
+ from_interface: link.name,
+ to_interface: link.remote_interface_name,
+ from_device: device_name,
+ to_device: link.remote_device_name
+ });
+ $scope.links_in_vars_by_device[link.remote_device_name].push({
+ from_interface: link.remote_interface_name,
+ to_interface: link.name,
+ from_device: link.remote_device_name,
+ to_device: device_name
+ });
+ }
+ }
+ }
+ }
+ };
+
+ $scope.for_each_page = function(url, callback, limit) {
+
+ function rec(url, rec_limit) {
+ if (rec_limit <= 0) {
+ return;
+ }
+ $http.get(url)
+ .then(function(response) {
+ callback(response.data.results);
+ if (response.data.next) {
+ rec(response.data.next, rec_limit-1);
+ }
+ })
+ .catch(({data, status}) => {
+ ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get host data: ' + status });
+ });
+ }
+ rec(url, limit);
+ };
+
+ //Inventory Toolbox Setup
+ $scope.inventory_toolbox = new models.ToolBox(0, 'Inventory', 'device', 0, toolboxTopMargin, 200, toolboxHeight);
+ if (!$scope.disconnected) {
+ $scope.for_each_page('/api/v2/inventories/' + $scope.inventory_id + '/hosts/',
+ function(all_results) {
+ let hosts = all_results;
+ console.log(hosts.length);
+ for(var i = 0; i= 0; i--) {
+ if (devices[i].is_selected($scope.scaledX, $scope.scaledY)) {
+ devices[i].selected = true;
+ $scope.send_control_message(new messages.DeviceSelected($scope.client_id, devices[i].id));
+ last_selected_device = devices[i];
+ if ($scope.selected_items.indexOf($scope.devices[i]) === -1) {
+ $scope.selected_items.push($scope.devices[i]);
+ }
+ if ($scope.selected_devices.indexOf(devices[i]) === -1) {
+ $scope.selected_devices.push(devices[i]);
+ }
+ if (!multiple_selection) {
+ break;
+ }
+ }
+ }
+
+ // Do not select interfaces if a device was selected
+ if (last_selected_device === null && !$scope.hide_interfaces) {
+ for (i = devices.length - 1; i >= 0; i--) {
+ for (j = devices[i].interfaces.length - 1; j >= 0; j--) {
+ if (devices[i].interfaces[j].is_selected($scope.scaledX, $scope.scaledY)) {
+ devices[i].interfaces[j].selected = true;
+ last_selected_interface = devices[i].interfaces[j];
+ if ($scope.selected_interfaces.indexOf($scope.devices[i].interfaces[j]) === -1) {
+ $scope.selected_interfaces.push($scope.devices[i].interfaces[j]);
+ }
+ if ($scope.selected_items.indexOf($scope.devices[i].interfaces[j]) === -1) {
+ $scope.selected_items.push($scope.devices[i].interfaces[j]);
+ }
+ if (!multiple_selection) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Do not select links if a device was selected
+ if (last_selected_device === null && last_selected_interface === null) {
+ for (i = $scope.links.length - 1; i >= 0; i--) {
+ if ($scope.links[i].is_selected($scope.scaledX, $scope.scaledY)) {
+ $scope.links[i].selected = true;
+ $scope.send_control_message(new messages.LinkSelected($scope.client_id, $scope.links[i].id));
+ last_selected_link = $scope.links[i];
+ if ($scope.selected_items.indexOf($scope.links[i]) === -1) {
+ $scope.selected_items.push($scope.links[i]);
+ }
+ if ($scope.selected_links.indexOf($scope.links[i]) === -1) {
+ $scope.selected_links.push($scope.links[i]);
+ if (!multiple_selection) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return {last_selected_device: last_selected_device,
+ last_selected_link: last_selected_link,
+ last_selected_interface: last_selected_interface,
+ };
+ };
+
+ // Event Handlers
+
+ $scope.normalize_mouse_event = function ($event) {
+ if ($event.pageX !== undefined) {
+ $event.x = $event.pageX;
+ }
+ if ($event.pageY !== undefined) {
+ $event.y = $event.pageY;
+ }
+ if ($event.originalEvent !== undefined) {
+ var originalEvent = $event.originalEvent;
+ if (originalEvent.wheelDelta !== undefined) {
+ $event.delta = $event.originalEvent.wheelDelta;
+ }
+ if (originalEvent.wheelDeltaX !== undefined) {
+ $event.deltaX = $event.originalEvent.wheelDeltaX;
+ }
+ if (originalEvent.wheelDeltaY !== undefined) {
+ $event.deltaY = $event.originalEvent.wheelDeltaY;
+ }
+ }
+ };
+
+ $scope.onMouseDown = function ($event) {
+ $scope.normalize_mouse_event($event);
+ if ($scope.recording) {
+ $scope.send_test_message(new messages.MouseEvent($scope.test_client_id, $event.x, $event.y, $event.type, $scope.trace_id));
+ }
+ $scope.last_event = $event;
+ $scope.first_channel.send('MouseDown', $event);
+ $scope.onMouseDownResult = getMouseEventResult($event);
+ $event.preventDefault();
+ };
+
+ $scope.onMouseUp = function ($event) {
+ $scope.normalize_mouse_event($event);
+ if ($scope.recording) {
+ $scope.send_test_message(new messages.MouseEvent($scope.test_client_id, $event.x, $event.y, $event.type, $scope.trace_id));
+ }
+ $scope.last_event = $event;
+ $scope.first_channel.send('MouseUp', $event);
+ $scope.onMouseUpResult = getMouseEventResult($event);
+ $event.preventDefault();
+ };
+
+ $scope.onMouseLeave = function ($event) {
+ $scope.normalize_mouse_event($event);
+ if ($scope.recording) {
+ $scope.send_test_message(new messages.MouseEvent($scope.test_client_id, $event.x, $event.y, $event.type, $scope.trace_id));
+ }
+ $scope.onMouseLeaveResult = getMouseEventResult($event);
+ $event.preventDefault();
+ };
+
+ $scope.onMouseMove = function ($event) {
+ $scope.normalize_mouse_event($event);
+ if ($scope.recording) {
+ $scope.send_test_message(new messages.MouseEvent($scope.test_client_id, $event.x, $event.y, $event.type, $scope.trace_id));
+ }
+ //var coords = getCrossBrowserElementCoords($event);
+ $scope.cursor.x = $event.x;
+ $scope.cursor.y = $event.y;
+ $scope.mouseX = $event.x;
+ $scope.mouseY = $event.y;
+ $scope.updateScaledXY();
+ $scope.first_channel.send('MouseMove', $event);
+ $scope.onMouseMoveResult = getMouseEventResult($event);
+ $event.preventDefault();
+ };
+
+ $scope.onMouseOver = function ($event) {
+ $scope.normalize_mouse_event($event);
+ if ($scope.recording) {
+ $scope.send_test_message(new messages.MouseEvent($scope.test_client_id, $event.x, $event.y, $event.type, $scope.trace_id));
+ }
+ $scope.onMouseOverResult = getMouseEventResult($event);
+ $event.preventDefault();
+ };
+
+ $scope.onMouseEnter = $scope.onMouseOver;
+
+ $scope.onMouseWheel = function ($event) {
+ $scope.normalize_mouse_event($event);
+ var delta = $event.delta;
+ var deltaX = $event.deltaX;
+ var deltaY = $event.deltaY;
+ if ($scope.recording) {
+ $scope.send_test_message(new messages.MouseWheelEvent($scope.test_client_id, delta, deltaX, deltaY, $event.type, $event.originalEvent.metaKey, $scope.trace_id));
+ }
+ $scope.last_event = $event;
+ $scope.first_channel.send('MouseWheel', [$event, delta, deltaX, deltaY]);
+ $event.preventDefault();
+ };
+
+ // Conext Menu Button Handlers
+ $scope.removeContextMenu = function(){
+ let context_menu = $scope.context_menus[0];
+ context_menu.enabled = false;
+ context_menu.x = -100000;
+ context_menu.y = -100000;
+ context_menu.buttons.forEach(function(button){
+ button.enabled = false;
+ button.x = -100000;
+ button.y = -100000;
+ });
+ };
+
+ $scope.closeDetailsPanel = function () {
+ $scope.first_channel.send('DetailsPanelClose', {});
+ };
+
+ $scope.$on('awxNet-closeDetailsPanel', $scope.closeDetailsPanel);
+
+ $scope.onDetailsContextButton = function () {
+ function emitCallback(item, canAdd){
+ $scope.first_channel.send('DetailsPanel', {});
+ $scope.removeContextMenu();
+ $scope.update_toolbox_heights();
+ $scope.$emit('awxNet-showDetails', item, canAdd);
+ }
+
+ // show details for devices
+ if ($scope.selected_devices.length === 1 && $scope.selected_devices[0].host_id === 0){
+ // following block is intended for devices added in the network UI but not in Tower
+ emitCallback($scope.selected_devices[0]);
+ }
+
+ // following block is intended for devices that are saved in the API
+ if ($scope.selected_devices.length === 1 && $scope.selected_devices[0].host_id !== 0){
+ let host_id = $scope.selected_devices[0].host_id;
+ let url = `/api/v2/hosts/${host_id}/`;
+ let hostData = $http.get(url)
+ .then(function(response) {
+ let host = response.data;
+ host.host_id = host.id;
+ return host;
+ })
+ .catch(({data, status}) => {
+ ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get host data: ' + status });
+ });
+ let canAdd = rbacUiControlService.canAdd('hosts')
+ .then(function(res) {
+ return res.canAdd;
+ })
+ .catch(function() {
+ return false;
+ });
+ Promise.all([hostData, canAdd]).then((values) => {
+ let item = values[0];
+ let canAdd = values[1];
+ emitCallback(item, canAdd);
+ });
+ }
+
+ // show details for interfaces
+ else if($scope.selected_interfaces.length === 1){
+ emitCallback($scope.selected_interfaces[0]);
+ }
+
+ // show details for links
+ else if($scope.selected_links.length === 1){
+ emitCallback($scope.selected_links[0]);
+ }
+
+ };
+
+ $scope.onDeviceDestroy = function(data) {
+ $scope.destroy_device(data);
+ };
+
+ $scope.destroy_device = function(data) {
+
+ // Delete the device and any links connecting to the device.
+ var i = 0;
+ var j = 0;
+ var dindex = -1;
+ var lindex = -1;
+ var devices = $scope.devices.slice();
+ var all_links = $scope.links.slice();
+ for (i = 0; i < devices.length; i++) {
+ if (devices[i].id === data.id) {
+ dindex = $scope.devices.indexOf(devices[i]);
+ if (dindex !== -1) {
+ $scope.devices.splice(dindex, 1);
+ }
+ lindex = -1;
+ for (j = 0; j < all_links.length; j++) {
+ if (all_links[j].to_device === devices[i] ||
+ all_links[j].from_device === devices[i]) {
+ lindex = $scope.links.indexOf(all_links[j]);
+ if (lindex !== -1) {
+ $scope.links.splice(lindex, 1);
+ }
+ }
+ }
+ }
+ }
+ };
+
+ $scope.deleteDevice = function(){
+ var i = 0;
+ var j = 0;
+ var index = -1;
+ var devices = $scope.selected_devices;
+ var all_links = $scope.links.slice();
+ $scope.selected_devices = [];
+ $scope.selected_links = [];
+ $scope.move_controller.changeState(move.Ready);
+ for (i = 0; i < devices.length; i++) {
+ index = $scope.devices.indexOf(devices[i]);
+ if (index !== -1) {
+ $scope.devices.splice(index, 1);
+ $scope.devices_by_name[devices[i].name] = undefined;
+ $scope.$emit('awxNet-removeSearchOption', devices[i]);
+ devices[i].x = 0;
+ devices[i].y = 0;
+ devices[i].selected = false;
+ devices[i].remote_selected = false;
+ devices[i].interfaces = [];
+ devices[i].interfaces_by_name = [];
+ $scope.inventory_toolbox.items.push(devices[i]);
+ $scope.send_control_message(new messages.DeviceDestroy($scope.client_id,
+ devices[i].id,
+ devices[i].x,
+ devices[i].y,
+ devices[i].name,
+ devices[i].type,
+ devices[i].host_id));
+ }
+ for (j = 0; j < all_links.length; j++) {
+ if (all_links[j].to_device === devices[i] ||
+ all_links[j].from_device === devices[i]) {
+ index = $scope.links.indexOf(all_links[j]);
+ if (index !== -1) {
+ $scope.links.splice(index, 1);
+ $scope.send_control_message(new messages.LinkDestroy($scope.client_id,
+ all_links[j].id,
+ all_links[j].from_device.id,
+ all_links[j].to_device.id,
+ all_links[j].from_interface.id,
+ all_links[j].to_interface.id,
+ all_links[j].name));
+
+ }
+ }
+ }
+ }
+ };
+
+ $scope.onDeleteContextMenu = function(){
+ $scope.removeContextMenu();
+ if($scope.selected_devices.length === 1){
+ $scope.deleteDevice();
+ }
+ };
+
+ $scope.$on('awxNet-hideToolbox', () => {
+ $scope.first_channel.send("ToggleToolbox", {});
+ $scope.overall_toolbox_collapsed = !$scope.overall_toolbox_collapsed;
+ });
+
+ $scope.$on('awxNet-toolbarButtonEvent', function(e, functionName){
+ $scope[`on${functionName}Button`]();
+ });
+
+ $scope.$on('awxNet-SearchDropdown', function(){
+ $scope.first_channel.send('SearchDropdown', {});
+ });
+
+ $scope.$on('awxNet-SearchDropdownClose', function(){
+ $scope.first_channel.send('SearchDropdownClose', {});
+ });
+
+ $scope.$on('awxNet-search', function(e, device){
+
+ var searched;
+ for(var i = 0; i < $scope.devices.length; i++){
+ if(Number(device.id) === $scope.devices[i].id){
+ searched = $scope.devices[i];
+ }
+ }
+ searched.selected = true;
+ $scope.selected_devices.push(searched);
+ $scope.jump_to_animation(searched.x, searched.y, 1.0);
+ });
+
+ $scope.jump_to_animation = function(jump_to_x, jump_to_y, jump_to_scale, updateZoom) {
+ $scope.cancel_animations();
+ var v_center = $scope.to_virtual_coordinates($scope.graph.width/2, $scope.graph.height/2);
+ $scope.jump.from_x = v_center.x;
+ $scope.jump.from_y = v_center.y;
+ $scope.jump.to_x = jump_to_x;
+ $scope.jump.to_y = jump_to_y;
+ var distance = util.distance(v_center.x, v_center.y, jump_to_x, jump_to_y);
+ var num_frames = 30 * Math.floor((1 + 4 * distance / (distance + 3000)));
+ var scale_animation = new models.Animation($scope.animation_id_seq(),
+ num_frames,
+ {
+ c: -0.1,
+ distance: distance,
+ end_height: (1.0/jump_to_scale) - 1,
+ current_scale: $scope.current_scale,
+ scope: $scope,
+ updateZoomBoolean: updateZoom
+ },
+ $scope,
+ $scope,
+ animations.scale_animation);
+ $scope.animations.push(scale_animation);
+ var pan_animation = new models.Animation($scope.animation_id_seq(),
+ num_frames,
+ {
+ x2: jump_to_x,
+ y2: jump_to_y,
+ x1: v_center.x,
+ y1: v_center.y,
+ scope: $scope
+ },
+ $scope,
+ $scope,
+ animations.pan_animation);
+ $scope.animations.push(pan_animation);
+ };
+
+ $scope.$on('awxNet-zoom', (e, zoomPercent) => {
+ let v_center = $scope.to_virtual_coordinates($scope.graph.width/2, $scope.graph.height/2);
+ let scale = Math.pow(10, (zoomPercent - 120) / 120);
+ $scope.jump_to_animation(v_center.x, v_center.y, scale, false);
+ });
+
+ $scope.onRecordButton = function () {
+ $scope.recording = ! $scope.recording;
+ if ($scope.recording) {
+ $scope.trace_id = $scope.trace_id_seq();
+ $scope.send_test_message(new messages.MultipleMessage($scope.test_client_id,
+ [new messages.StartRecording($scope.test_client_id, $scope.trace_id),
+ new messages.ViewPort($scope.test_client_id,
+ $scope.current_scale,
+ $scope.panX,
+ $scope.panY,
+ $scope.graph.width,
+ $scope.graph.height,
+ $scope.trace_id),
+ new messages.Snapshot($scope.test_client_id,
+ $scope.devices,
+ $scope.links,
+ $scope.inventory_toolbox.items,
+ 0,
+ $scope.trace_id)]));
+ } else {
+ $scope.send_test_message(new messages.MultipleMessage($scope.test_client_id,
+ [new messages.Snapshot($scope.test_client_id,
+ $scope.devices,
+ $scope.links,
+ $scope.inventory_toolbox.items,
+ 1,
+ $scope.trace_id),
+ new messages.StopRecording($scope.test_client_id, $scope.trace_id)]));
+ }
+ };
+
+ $scope.onExportButton = function () {
+ $scope.cursor.hidden = true;
+ $scope.debug.hidden = true;
+ $scope.hide_buttons = true;
+ $scope.hide_menus = true;
+ setTimeout(function () {
+ svg_crowbar.svg_crowbar();
+ $scope.cursor.hidden = false;
+ $scope.hide_buttons = false;
+ $scope.hide_menus = false;
+ $scope.$apply();
+ }, 1000);
+ };
+
+ $scope.onExportYamlButton = function () {
+ $window.open('/network_ui/topology.yaml?topology_id=' + $scope.topology_id , '_blank');
+ };
+
+ // Context Menu Buttons
+ const contextMenuButtonHeight = 26;
+ let contextMenuHeight = 64;
+ $scope.context_menu_buttons = [
+ new models.ContextMenuButton("Details", 236, 231, 160, contextMenuButtonHeight, $scope.onDetailsContextButton, $scope),
+ new models.ContextMenuButton("Remove", 256, 231, 160, contextMenuButtonHeight, $scope.onDeleteContextMenu, $scope)
+ ];
+ if(!$scope.canEdit){
+ $scope.context_menu_buttons.pop();
+ contextMenuHeight = $scope.context_menu_buttons.length * contextMenuButtonHeight + 12;
+ }
+ // Context Menus
+ $scope.context_menus = [
+ new models.ContextMenu('HOST', 210, 200, 160, contextMenuHeight, $scope.contextMenuCallback, false, $scope.context_menu_buttons, $scope)
+ ];
+
+ $scope.onDownloadTraceButton = function () {
+ window.open("/network_ui_test/download_trace?topology_id=" + $scope.topology_id + "&trace_id=" + $scope.trace_id + "&client_id=" + $scope.test_client_id);
+ };
+
+ $scope.onDownloadRecordingButton = function () {
+ window.open("/network_ui_test/download_recording?topology_id=" + $scope.topology_id + "&trace_id=" + $scope.trace_id + "&client_id=" + $scope.test_client_id);
+ };
+
+ $scope.onUploadTestButton = function () {
+ window.open("/network_ui_test/upload_test", "_top");
+ };
+
+ $scope.onRunTestsButton = function () {
+
+ $scope.test_results = [];
+ $scope.current_tests = $scope.tests.slice();
+ $scope.test_channel.send("EnableTest", new messages.EnableTest());
+ };
+
+ $scope.all_buttons = $scope.context_menu_buttons;
+
+ $scope.onDeviceCreate = function(data) {
+ $scope.create_device(data);
+ };
+
+ $scope.create_device = function(data) {
+ console.log(data);
+ var device = new models.Device(data.id,
+ data.name,
+ data.x,
+ data.y,
+ data.type,
+ data.host_id);
+ $scope.device_id_seq = util.natural_numbers(data.id);
+ $scope.devices.push(device);
+ $scope.devices_by_name[device.name] = device;
+ };
+
+ $scope.onInterfaceCreate = function(data) {
+ $scope.create_interface(data);
+ };
+
+ $scope.create_interface = function(data) {
+ var i = 0;
+ var new_interface = new models.Interface(data.id, data.name);
+ for (i = 0; i < $scope.devices.length; i++){
+ if ($scope.devices[i].id === data.device_id) {
+ $scope.devices[i].interface_seq = util.natural_numbers(data.id);
+ new_interface.device = $scope.devices[i];
+ $scope.devices[i].interfaces.push(new_interface);
+ }
+ }
+ };
+
+ $scope.onLinkCreate = function(data) {
+ console.log(data);
+ $scope.create_link(data);
+ };
+
+ $scope.create_link = function(data) {
+ var i = 0;
+ var j = 0;
+ var new_link = new models.Link(null, null, null, null);
+ new_link.id = data.id;
+ $scope.link_id_seq = util.natural_numbers(data.id);
+ for (i = 0; i < $scope.devices.length; i++){
+ if ($scope.devices[i].id === data.from_device_id) {
+ new_link.from_device = $scope.devices[i];
+ for (j = 0; j < $scope.devices[i].interfaces.length; j++){
+ if ($scope.devices[i].interfaces[j].id === data.from_interface_id) {
+ new_link.from_interface = $scope.devices[i].interfaces[j];
+ $scope.devices[i].interfaces[j].link = new_link;
+ }
+ }
+ }
+ }
+ for (i = 0; i < $scope.devices.length; i++){
+ if ($scope.devices[i].id === data.to_device_id) {
+ new_link.to_device = $scope.devices[i];
+ for (j = 0; j < $scope.devices[i].interfaces.length; j++){
+ if ($scope.devices[i].interfaces[j].id === data.to_interface_id) {
+ new_link.to_interface = $scope.devices[i].interfaces[j];
+ $scope.devices[i].interfaces[j].link = new_link;
+ }
+ }
+ }
+ }
+ console.log(new_link);
+ if (new_link.from_interface !== null && new_link.to_interface !== null) {
+ new_link.from_interface.dot();
+ new_link.to_interface.dot();
+ }
+ if (new_link.from_device !== null && new_link.to_device !== null) {
+ $scope.links.push(new_link);
+ }
+ };
+
+ $scope.onLinkDestroy = function(data) {
+ $scope.destroy_link(data);
+ };
+
+ $scope.destroy_link = function(data) {
+ var i = 0;
+ var link = null;
+ var index = -1;
+ for (i = 0; i < $scope.links.length; i++) {
+ link = $scope.links[i];
+ if (link.id === data.id &&
+ link.from_device.id === data.from_device_id &&
+ link.to_device.id === data.to_device_id &&
+ link.to_interface.id === data.to_interface_id &&
+ link.from_interface.id === data.from_interface_id) {
+ link.from_interface.link = null;
+ link.to_interface.link = null;
+ index = $scope.links.indexOf(link);
+ $scope.links.splice(index, 1);
+ }
+ }
+ };
+
+ $scope.onDeviceMove = function(data) {
+ $scope.move_device(data);
+ };
+
+ $scope.move_device = function(data) {
+ var i = 0;
+ var j = 0;
+ for (i = 0; i < $scope.devices.length; i++) {
+ if ($scope.devices[i].id === data.id) {
+ $scope.devices[i].x = data.x;
+ $scope.devices[i].y = data.y;
+ for (j = 0; j < $scope.devices[i].interfaces.length; j ++) {
+ $scope.devices[i].interfaces[j].dot();
+ if ($scope.devices[i].interfaces[j].link !== null) {
+ $scope.devices[i].interfaces[j].link.to_interface.dot();
+ $scope.devices[i].interfaces[j].link.from_interface.dot();
+ }
+ }
+ break;
+ }
+ }
+ };
+
+ $scope.onClientId = function(data) {
+ $scope.client_id = data;
+ };
+
+ $scope.onTopology = function(data) {
+ $scope.topology_id = data.topology_id;
+ $scope.panX = data.panX;
+ $scope.panY = data.panX;
+ $scope.current_scale = data.scale;
+ $scope.$emit('awxNet-UpdateZoomWidget', $scope.current_scale, true);
+ $scope.link_id_seq = util.natural_numbers(data.link_id_seq);
+ $scope.device_id_seq = util.natural_numbers(data.device_id_seq);
+ };
+
+ $scope.onDeviceSelected = function(data) {
+ var i = 0;
+ for (i = 0; i < $scope.devices.length; i++) {
+ if ($scope.devices[i].id === data.id) {
+ $scope.devices[i].remote_selected = true;
+ }
+ }
+ };
+
+ $scope.onDeviceUnSelected = function(data) {
+ var i = 0;
+ for (i = 0; i < $scope.devices.length; i++) {
+ if ($scope.devices[i].id === data.id) {
+ $scope.devices[i].remote_selected = false;
+ }
+ }
+ };
+
+ $scope.onSnapshot = function (data) {
+
+ //Erase the existing state
+ $scope.devices = [];
+ $scope.links = [];
+ $scope.devices_by_name = {};
+
+ var device_map = {};
+ var device_interface_map = {};
+ var i = 0;
+ var j = 0;
+ var device = null;
+ var intf = null;
+ var new_device = null;
+ var new_intf = null;
+ var max_device_id = null;
+ var max_link_id = null;
+ var min_x = null;
+ var min_y = null;
+ var max_x = null;
+ var max_y = null;
+ var new_link = null;
+
+ //Build the devices
+ for (i = 0; i < data.devices.length; i++) {
+ device = data.devices[i];
+ if (max_device_id === null || device.id > max_device_id) {
+ max_device_id = device.id;
+ }
+ if (min_x === null || device.x < min_x) {
+ min_x = device.x;
+ }
+ if (min_y === null || device.y < min_y) {
+ min_y = device.y;
+ }
+ if (max_x === null || device.x > max_x) {
+ max_x = device.x;
+ }
+ if (max_y === null || device.y > max_y) {
+ max_y = device.y;
+ }
+ if (device.device_type === undefined) {
+ device.device_type = device.type;
+ }
+ new_device = new models.Device(device.id,
+ device.name,
+ device.x,
+ device.y,
+ device.device_type,
+ device.host_id);
+ if (device.variables !== undefined) {
+ new_device.variables = device.variables;
+ }
+
+ for (j=0; j < $scope.inventory_toolbox.items.length; j++) {
+ if($scope.inventory_toolbox.items[j].name === device.name) {
+ $scope.inventory_toolbox.items.splice(j, 1);
+ break;
+ }
+ }
+ new_device.interface_seq = util.natural_numbers(device.interface_id_seq);
+ new_device.process_id_seq = util.natural_numbers(device.process_id_seq);
+ $scope.devices.push(new_device);
+ $scope.devices_by_name[new_device.name] = new_device;
+ device_map[device.id] = new_device;
+ device_interface_map[device.id] = {};
+ for (j = 0; j < device.interfaces.length; j++) {
+ intf = device.interfaces[j];
+ new_intf = (new models.Interface(intf.id,
+ intf.name));
+ new_intf.device = new_device;
+ device_interface_map[device.id][intf.id] = new_intf;
+ new_device.interfaces.push(new_intf);
+ new_device.interfaces_by_name[new_intf.name] = new_intf;
+ }
+ }
+
+ //Build the links
+ var link = null;
+ for (i = 0; i < data.links.length; i++) {
+ link = data.links[i];
+ if (max_link_id === null || link.id > max_link_id) {
+ max_link_id = link.id;
+ }
+ new_link = new models.Link(link.id,
+ device_map[link.from_device_id],
+ device_map[link.to_device_id],
+ device_interface_map[link.from_device_id][link.from_interface_id],
+ device_interface_map[link.to_device_id][link.to_interface_id]);
+ new_link.name = link.name;
+ $scope.links.push(new_link);
+ device_interface_map[link.from_device_id][link.from_interface_id].link = new_link;
+ device_interface_map[link.to_device_id][link.to_interface_id].link = new_link;
+ }
+
+ var diff_x;
+ var diff_y;
+
+ // Calculate the new scale to show the entire diagram
+ if (min_x !== null && min_y !== null && max_x !== null && max_y !== null) {
+ diff_x = max_x - min_x;
+ diff_y = max_y - min_y;
+
+ $scope.current_scale = Math.min(2, Math.max(0.10, Math.min((window.innerWidth-200)/diff_x, (window.innerHeight-300)/diff_y)));
+ $scope.$emit('awxNet-UpdateZoomWidget', $scope.current_scale, true);
+ $scope.updateScaledXY();
+ $scope.updatePanAndScale();
+ }
+ // Calculate the new panX and panY to show the entire diagram
+ if (min_x !== null && min_y !== null) {
+ diff_x = max_x - min_x;
+ diff_y = max_y - min_y;
+ $scope.panX = $scope.current_scale * (-min_x - diff_x/2) + window.innerWidth/2;
+ $scope.panY = $scope.current_scale * (-min_y - diff_y/2) + window.innerHeight/2;
+ $scope.updateScaledXY();
+ $scope.updatePanAndScale();
+ }
+
+ //Update the device_id_seq to be greater than all device ids to prevent duplicate ids.
+ if (max_device_id !== null) {
+ $scope.device_id_seq = util.natural_numbers(max_device_id);
+ }
+ //
+ //Update the link_id_seq to be greater than all link ids to prevent duplicate ids.
+ if (max_link_id !== null) {
+ $scope.link_id_seq = util.natural_numbers(max_link_id);
+ }
+
+ console.log(['data.inventory_toolbox', data.inventory_toolbox]);
+ if (data.inventory_toolbox !== undefined) {
+ $scope.inventory_toolbox.items = [];
+ for (i = 0; i < data.inventory_toolbox.length; i++) {
+ device = data.inventory_toolbox[i];
+ console.log(device);
+ if (device.device_type === undefined) {
+ device.device_type = device.type;
+ }
+ new_device = new models.Device(device.id,
+ device.name,
+ device.x,
+ device.y,
+ device.device_type,
+ device.host_id);
+ if (device.variables !== undefined) {
+ new_device.variables = device.variables;
+ }
+ $scope.inventory_toolbox.items.push(new_device);
+ }
+ console.log($scope.inventory_toolbox.items);
+ }
+
+ $scope.updateInterfaceDots();
+ $scope.$emit('awxNet-instatiateSelect', $scope.devices);
+ };
+
+ $scope.updateInterfaceDots = function() {
+ var i = 0;
+ var j = 0;
+ var devices = $scope.devices;
+ for (i = devices.length - 1; i >= 0; i--) {
+ for (j = devices[i].interfaces.length - 1; j >= 0; j--) {
+ devices[i].interfaces[j].dot();
+ }
+ }
+ };
+
+ $scope.control_socket.onmessage = function(message) {
+ $scope.first_channel.send('Message', message);
+ $scope.$apply();
+ };
+
+ $scope.control_socket.onopen = function() {
+ //ignore
+ };
+
+ $scope.test_socket.onmessage = function(message) {
+ $scope.test_channel.send('Message', message);
+ $scope.$apply();
+ };
+
+ $scope.test_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();
+ }
+ // Call onopen directly if $scope.test_socket is already open
+ if ($scope.test_socket.readyState === WebSocket.OPEN) {
+ $scope.test_socket.onopen();
+ }
+
+ $scope.send_test_message = function (message) {
+ var i = 0;
+ message.sender = $scope.test_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.test_socket.send(data);
+ }
+ };
+
+ $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);
+ }
+ };
+
+
+ // End web socket
+ //
+
+ angular.element($window).bind('resize', function(){
+
+ $scope.graph.width = $window.innerWidth;
+ $scope.graph.right_column = 300;
+ $scope.graph.height = $window.innerHeight;
+
+ $scope.update_size();
+
+ // manuall $digest required as resize event
+ // is outside of angular
+ $scope.$digest();
+ });
+
+ //60fps ~ 17ms delay
+ setInterval( function () {
+ $scope.frame = Math.floor(window.performance.now());
+ $scope.$apply();
+ }, 17);
+
+ console.log("Network UI started");
+
+ $scope.$on('$destroy', function () {
+ console.log("Network UI stopping");
+ $scope.first_channel.send('UnbindDocument', {});
+ });
+
+ $scope.update_toolbox_heights = function(){
+ var toolboxTopMargin = $('.Networking-top').height();
+ var toolboxTitleMargin = toolboxTopMargin + 35;
+ var toolboxHeight = $scope.graph.height - toolboxTopMargin;
+
+ let toolboxes = ['inventory_toolbox'];
+ toolboxes.forEach((toolbox) => {
+ $scope[toolbox].y = toolboxTopMargin;
+ $scope[toolbox].height = toolboxHeight;
+ $scope[toolbox].title_coordinates.y = toolboxTitleMargin;
+ });
+
+ $('.Networking-detailPanel').height(toolboxHeight);
+ $('.Networking-detailPanel').css('top', toolboxTopMargin);
+ };
+
+ $scope.update_size = function () {
+ $scope.update_toolbox_heights();
+ };
+
+ setInterval( function () {
+ var test_event = null;
+ if ($scope.test_events.length > 0) {
+ test_event = $scope.test_events.shift();
+ test_event.sender = 0;
+ try {
+ $scope.first_channel.send(test_event.msg_type, test_event);
+ } catch (err) {
+ console.log(["Test Error:", $scope.current_test, err]);
+ $scope.test_errors.push(err);
+ }
+ }
+ $scope.$apply();
+ }, 10);
+
+ ConfigService
+ .getConfig()
+ .then(function(config){
+ $scope.version = config.version;
+ });
+
+ $scope.reset_coverage = function() {
+ var i = null;
+ var coverage = null;
+ var f = null;
+ if (typeof(window.__coverage__) !== "undefined" && window.__coverage__ !== null) {
+ for (f in window.__coverage__) {
+ coverage = window.__coverage__[f];
+ for (i in coverage.b) {
+ coverage.b[i] = [0, 0];
+ }
+ for (i in coverage.f) {
+ coverage.f[i] = 0;
+ }
+ for (i in coverage.s) {
+ coverage.s[i] = 0;
+ }
+ }
+ }
+ };
+
+ $scope.reset_flags = function () {
+ $scope.debug = {'hidden': true};
+ $scope.hide_buttons = false;
+ $scope.hide_links = false;
+ $scope.hide_interfaces = false;
+ };
+
+
+ $scope.reset_fsm_state = function () {
+ $scope.hotkeys_controller.state = hotkeys.Start;
+ $scope.hotkeys_controller.state.start($scope.hotkeys_controller);
+ $scope.keybindings_controller.state = keybindings.Start;
+ $scope.keybindings_controller.state.start($scope.keybindings_controller);
+ $scope.view_controller.state = view.Start;
+ $scope.view_controller.state.start($scope.view_controller);
+ $scope.move_controller.state = move.Start;
+ $scope.move_controller.state.start($scope.move_controller);
+ $scope.move_readonly_controller.state = move_readonly.Start;
+ $scope.move_readonly_controller.state.start($scope.move_readonly_controller);
+ $scope.details_panel_controller.state = details_panel_fsm.Start;
+ $scope.details_panel_controller.state.start($scope.details_panel_controller);
+ $scope.time_controller.state = time.Start;
+ $scope.time_controller.state.start($scope.time_controller);
+ $scope.inventory_toolbox_controller.state = toolbox_fsm.Start;
+ $scope.inventory_toolbox_controller.state.start($scope.inventory_toolbox_controller);
+ $scope.mode_controller.state = mode_fsm.Start;
+ $scope.mode_controller.state.start($scope.mode_controller);
+ };
+
+ $scope.reset_toolboxes = function () {
+ $scope.inventory_toolbox.items = [];
+ $scope.inventory_toolbox.scroll_offset = 0;
+ };
+
+ $scope.cancel_animations = function () {
+
+ var i = 0;
+ for (i = 0; i < $scope.animations.length; i++) {
+ this.animations[i].fsm.handle_message('AnimationCancelled');
+ }
+ $scope.animations = [];
+ };
+};
+
+exports.NetworkUIController = NetworkUIController;
+console.log("Network UI loaded");
diff --git a/awx/ui/client/src/network-ui/network.ui.directive.js b/awx/ui/client/src/network-ui/network.ui.directive.js
new file mode 100644
index 0000000000..af53c59677
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network.ui.directive.js
@@ -0,0 +1,8 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+
+const templateUrl = require('~network-ui/network_ui.partial.svg');
+
+function awxNetworkUI () {
+ return { restrict: 'E', templateUrl};
+}
+exports.awxNetworkUI = awxNetworkUI;
diff --git a/awx/ui/client/src/network-ui/network_ui.partial.svg b/awx/ui/client/src/network-ui/network_ui.partial.svg
new file mode 100644
index 0000000000..afb740b3d5
--- /dev/null
+++ b/awx/ui/client/src/network-ui/network_ui.partial.svg
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/src/network-ui/quadrants.directive.js b/awx/ui/client/src/network-ui/quadrants.directive.js
new file mode 100644
index 0000000000..85874c9f21
--- /dev/null
+++ b/awx/ui/client/src/network-ui/quadrants.directive.js
@@ -0,0 +1,8 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+
+const templateUrl = require('~network-ui/quadrants.partial.svg');
+
+function quadrants () {
+ return { restrict: 'A', templateUrl};
+}
+exports.quadrants = quadrants;
diff --git a/awx/ui/client/src/network-ui/quadrants.partial.svg b/awx/ui/client/src/network-ui/quadrants.partial.svg
new file mode 100644
index 0000000000..142b733d42
--- /dev/null
+++ b/awx/ui/client/src/network-ui/quadrants.partial.svg
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
diff --git a/awx/ui/client/src/network-ui/router.directive.js b/awx/ui/client/src/network-ui/router.directive.js
new file mode 100644
index 0000000000..22059d930d
--- /dev/null
+++ b/awx/ui/client/src/network-ui/router.directive.js
@@ -0,0 +1,8 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+
+const templateUrl = require('~network-ui/router.partial.svg');
+
+function router () {
+ return { restrict: 'A', templateUrl};
+}
+exports.router = router;
diff --git a/awx/ui/client/src/network-ui/router.partial.svg b/awx/ui/client/src/network-ui/router.partial.svg
new file mode 100644
index 0000000000..99a2606926
--- /dev/null
+++ b/awx/ui/client/src/network-ui/router.partial.svg
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.name}}
+ {{item.name}}{{item.edit_label?'_':''}}
+
+
diff --git a/awx/ui/client/src/network-ui/style.less b/awx/ui/client/src/network-ui/style.less
new file mode 100644
index 0000000000..0eddbaa9e7
--- /dev/null
+++ b/awx/ui/client/src/network-ui/style.less
@@ -0,0 +1,692 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+@import 'network-nav/network.nav.block.less';
+@import 'network-details/details.block.less';
+@import 'network-details/network-extra-vars/network-extra-vars.block.less';
+@import 'zoom-widget/zoom.block.less';
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ src: url(/static/assets/OpenSans-Regular.ttf);
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: bold;
+ font-weight: 600;
+ src: url(/static/assets/OpenSans-Bold.ttf);
+}
+
+@selected-red: #c9232c;
+@selected-mango: #ff5850;
+@selected-blue: #5bbddf;
+@light-background: #ffffff;
+@light-widget-detail: #ffffff;
+@dark-widget-detail: #707070;
+@widget-body: #D7D7D7;
+@link: #D7D7D7;
+@group: #707070;
+@debug-copynot: rgb(77,200,242);
+@button-body: #ffffff;
+@button-text: #707070;
+@button-outline: #b7b7b7;
+@button-body-hover: #f2f2f2;
+@button-body-pressed: #848992;
+@button-text-pressed: #ffffff;
+@green: #5CB85C;
+@red: #D9534F;
+@blue: #337AB7;
+@light-toolbox-background: #f6f6f6;
+@icon-background-hover:@blue;
+@context-menu-text: #333;
+
+.NetworkUI {
+ background-color: @light-toolbox-background;
+}
+
+.NetworkUI__text {
+ fill: @button-text;
+ font-family: 'Open Sans';
+}
+
+.NetworkUI__debug-text {
+ fill: @debug-copynot;
+ font-family: 'Open Sans';
+}
+
+.NetworkUI--debug {
+ fill-opacity: 0;
+ stroke: @debug-copynot;
+ stroke-width: 1;
+}
+
+.NetworkUI--construction {
+ fill-opacity: 0;
+ stroke: @debug-copynot;
+ stroke-width: 1;
+}
+
+
+.NetworkUI__link--selected {
+ stroke: @selected-blue;
+ stroke-width: 6;
+}
+
+.NetworkUI__link--remote-selected {
+ stroke: @selected-mango;
+ stroke-width: 6;
+}
+
+.NetworkUI__link--selected-conflict {
+ stroke: @selected-red;
+ stroke-width: 6;
+}
+
+
+.NetworkUI__link {
+ stroke: @link;
+ stroke-width: 2;
+}
+
+.NetworkUI__link--link-pass {
+ stroke: @green;
+ stroke-width: 2;
+}
+
+.NetworkUI__link--link-fail {
+ stroke: @red;
+ stroke-width: 2;
+}
+
+.NetworkUI__link--debug {
+ stroke: @debug-copynot;
+ stroke-width: 1;
+}
+
+.NetworkUI__cursor {
+ stroke: @dark-widget-detail;
+ stroke-width: 2;
+}
+
+.NetworkUI__debug-cursor {
+ stroke: @debug-copynot;
+ stroke-width: 4;
+}
+
+.NetworkUI--hidden {
+ display: none;
+}
+
+.NetworkUI__router {
+ fill: @blue;
+}
+
+.NetworkUI__router--background{
+ fill: @light-background;
+}
+
+.NetworkUI__router--selected {
+ stroke: @selected-blue;
+ stroke-width: 10;
+ fill:@light-background;
+}
+
+.NetworkUI__router--remote-selected {
+ stroke: @selected-mango;
+ stroke-width: 10;
+ fill: @light-background
+}
+
+.NetworkUI__router--selected-conflict {
+ stroke: @selected-red;
+ stroke-width: 10;
+}
+
+.NetworkUI__router-text {
+ fill: @button-text;
+ font-family: 'Open Sans';
+}
+
+
+.NetworkUI__router-text--selected {
+ font-family: 'Open Sans';
+}
+
+
+.NetworkUI__switch {
+ fill: @blue;
+}
+
+.NetworkUI__switch--background{
+ fill: @light-background;
+}
+
+.NetworkUI__switch--selected {
+ stroke: @selected-blue;
+ stroke-width: 10;
+ fill:@light-background;
+}
+
+.NetworkUI__switch--remote-selected {
+ stroke: @selected-mango;
+ stroke-width: 10;
+ fill: @light-background
+}
+
+.NetworkUI__switch--selected-conflict {
+ stroke: @selected-red;
+ stroke-width: 10;
+}
+
+.NetworkUI__switch line {
+ stroke: @light-widget-detail;
+ stroke-width: 20;
+}
+
+.NetworkUI__switch polygon {
+ fill: @light-widget-detail;
+ stroke: none;
+}
+
+.NetworkUI__switch-text {
+ fill: @button-text;
+ font-family: 'Open Sans';
+}
+
+
+.NetworkUI__switch-text--selected {
+ font-family: 'Open Sans';
+}
+
+.NetworkUI__rack {
+ fill: @blue;
+}
+
+.NetworkUI__rack--background {
+ fill: @light-background;
+}
+
+.NetworkUI__rack--selected {
+ fill: @light-background;
+ stroke: @selected-blue;
+ stroke-width: 10;
+}
+
+.NetworkUI__rack--remote-selected {
+ fill: @light-background;
+ stroke: @selected-mango;
+ stroke-width: 10;
+}
+
+.NetworkUI__rack--selected-conflict {
+ fill: @selected-red;
+ stroke: @selected-red;
+ stroke-width: 10;
+}
+
+.NetworkUI__rack line {
+ stroke: @light-widget-detail;
+ stroke-width: 20;
+}
+
+.NetworkUI__rack circle {
+ fill: @light-widget-detail;
+ stroke: none;
+}
+
+
+.NetworkUI__rack-text {
+ fill: @button-text;
+ font-family: 'Open Sans';
+}
+
+.NetworkUI__rack-text--selected {
+ font-family: 'Open Sans';
+}
+
+.NetworkUI__site {
+ fill: @blue;
+ stroke: @dark-widget-detail;
+ stroke-width: 2;
+}
+
+.NetworkUI__site--fill0{
+ fill:@blue;
+}
+
+.NetworkUI__site--fill1{
+ fill:none;
+ stroke:@light-background;
+ stroke-width:2;
+ stroke-miterlimit:10;
+}
+
+.NetworkUI__site--fill2{
+ fill:@light-background;
+}
+
+.NetworkUI__site--network {
+ fill: @dark-widget-detail;
+ stroke: @dark-widget-detail;
+ stroke-width: 2;
+}
+
+.NetworkUI__site--background {
+ fill: @light-background;
+}
+
+.NetworkUI__site--selected {
+ fill: @light-background;
+ stroke: @selected-blue;
+ stroke-width: 10;
+}
+
+.NetworkUI__site--remote-selected {
+ fill: @light-background;
+ stroke: @selected-mango;
+ stroke-width: 10;
+}
+
+.NetworkUI__site--selected-conflict {
+ fill: @selected-red;
+ stroke: @selected-red;
+ stroke-width: 10;
+}
+
+.NetworkUI__site line {
+ stroke: @dark-widget-detail;
+ stroke-width: 10;
+}
+
+.NetworkUI__site circle {
+ fill: @light-widget-detail;
+ stroke: none;
+}
+
+
+.NetworkUI__site-text {
+ fill: @button-text;
+ font-family: 'Open Sans';
+}
+
+.NetworkUI__site-text--selected {
+ font-family: 'Open Sans';
+}
+
+.NetworkUI__button {
+ fill: @button-body;
+ stroke: @button-outline;
+ stroke-width: 1;
+}
+
+.NetworkUI__button-text {
+ fill: @button-text;
+ font-family: 'Open Sans';
+ font-size: 14px;
+}
+
+.NetworkUI__button--button-pressed {
+ fill: @button-body-pressed;
+ stroke: @button-outline;
+ stroke-width: 1;
+}
+
+.NetworkUI__button-text--button-pressed {
+ fill: @button-text-pressed;
+ font-family: 'Open Sans';
+ font-size: 14px;
+}
+
+.NetworkUI__button--button-hover {
+ fill: @button-body-hover;
+ stroke: @button-outline;
+ stroke-width: 1;
+}
+
+.NetworkUI__button-text--button-hover {
+ fill: @button-text;
+ font-family: 'Open Sans';
+ font-size: 14px;
+}
+
+.NetworkUI__host {
+ fill: @blue;
+}
+
+.NetworkUI__host--background {
+ fill: @light-background;
+}
+
+.NetworkUI__host--selected {
+ fill: @light-background;
+ stroke: @selected-blue;
+ stroke-width: 10;
+}
+
+.NetworkUI__host--remote-selected {
+ fill: @light-background;
+ stroke: @selected-mango;
+ stroke-width: 10;
+}
+
+.NetworkUI__host--selected-conflict {
+ fill: @selected-red;
+ stroke: @selected-red;
+ stroke-width: 10;
+}
+
+.NetworkUI__host line {
+ stroke: @light-widget-detail;
+ stroke-width: 20;
+}
+
+.NetworkUI__host circle {
+ fill: @light-widget-detail;
+ stroke: none;
+}
+
+
+.NetworkUI__host-text {
+ fill: @button-text;
+ font-family: 'Open Sans';
+}
+
+
+.NetworkUI__host-text--selected {
+ font-family: 'Open Sans';
+}
+
+.NetworkUI__device {
+ fill: @widget-body;
+ stroke: @dark-widget-detail;
+ stroke-width: 2;
+}
+
+.NetworkUI__device-background {
+ fill: @light-background;
+ stroke: @light-background;
+ stroke-width: 2;
+}
+
+.NetworkUI__device--selected {
+ fill: @selected-blue;
+ stroke: @selected-blue;
+ stroke-width: 10;
+}
+
+.NetworkUI__device--remote-selected {
+ fill: @light-background;
+ stroke: @selected-mango;
+ stroke-width: 10;
+}
+
+.NetworkUI__device--selected-conflict {
+ fill: @selected-red;
+ stroke: @selected-red;
+ stroke-width: 10;
+}
+
+.NetworkUI__device line {
+ stroke: @light-widget-detail;
+ stroke-width: 20;
+}
+
+
+.NetworkUI__device-text {
+ fill: @button-text;
+ font-family: 'Open Sans';
+}
+
+
+.NetworkUI__device-text--selected {
+ font-family: 'Open Sans';
+}
+
+.NetworkUI__status {
+ fill: @widget-body;
+ stroke: @dark-widget-detail;
+ stroke-width: 2;
+}
+
+.NetworkUI__status--pass {
+ fill: @green;
+ stroke: @dark-widget-detail;
+ stroke-width: 2;
+}
+
+.NetworkUI__status--fail {
+ fill: @red;
+ stroke: @dark-widget-detail;
+ stroke-width: 2;
+}
+
+.NetworkUI__status-path {
+ fill: none;
+ stroke: @dark-widget-detail;
+ stroke-width: 2;
+}
+
+.NetworkUI__circle-debug {
+ fill: @debug-copynot;
+}
+
+
+.NetworkUI__interface {
+ fill: @button-outline;
+}
+
+.NetworkUI__interface--selected {
+ fill: @selected-blue;
+}
+
+.NetworkUI__interface-text {
+ fill: @button-text;
+ font-size: 8px;
+ font-family: 'Open Sans';
+}
+
+
+.NetworkUI__interface-text--selected {
+ font-size: 8px;
+ font-family: 'Open Sans';
+}
+
+.NetworkUI__link-text {
+ fill: @button-text;
+ font-size: 8px;
+ font-family: 'Open Sans';
+}
+
+.NetworkUI__group--selected {
+ stroke: @selected-blue;
+ stroke-width: 6;
+ fill: none;
+}
+
+.NetworkUI__group--remote-selected {
+ stroke: @selected-mango;
+ stroke-width: 6;
+ fill: none;
+}
+
+.NetworkUI__group--selected-conflict {
+ stroke: @selected-red;
+ stroke-width: 6;
+ fill: none;
+}
+
+.NetworkUI__group {
+ stroke: @group;
+ stroke-width: 2;
+ fill: none;
+}
+
+.NetworkUI__group--debug {
+ stroke: @debug-copynot;
+ stroke-width: 1;
+ fill: none;
+}
+
+.NetworkUI__group-text {
+ fill: @button-text;
+ font-family: 'Open Sans';
+}
+
+
+.NetworkUI__group-text--selected {
+ font-family: 'Open Sans';
+}
+
+.NetworkUI__location-text {
+ fill: @button-text;
+ font-family: 'Open Sans';
+}
+
+
+.NetworkUI__toolbox {
+ stroke: none;
+ fill: @light-background;
+}
+
+.NetworkUI__toolbox-collapsed{
+ fill: @light-background;
+ stroke: @button-outline;
+ stroke-width: 1;
+ rx: 0;
+ stroke-dasharray: calc(~"100vh - 80px");
+ stroke-dashoffset: -45;
+}
+
+.NetworkUI__toolbox-bezel {
+ stroke: @button-outline;
+ stroke-width: 1;
+ fill: none;
+ rx: 0;
+ stroke-dasharray: calc(~"100vh - 80px");
+ stroke-dashoffset: -200;
+}
+
+.NetworkUI__process {
+ fill: @widget-body;
+ stroke: @dark-widget-detail;
+ stroke-width: 2;
+}
+
+.NetworkUI__process-background {
+ fill: @light-background;
+ stroke: @light-background;
+ stroke-width: 2;
+}
+
+.NetworkUI__process--selected {
+ fill: @selected-blue;
+ stroke: @selected-blue;
+ stroke-width: 10;
+}
+
+.NetworkUI__process--remote-selected {
+ fill: @selected-mango;
+ stroke: @selected-mango;
+ stroke-width: 10;
+}
+
+.NetworkUI__process--selected-conflict {
+ fill: @selected-red;
+ stroke: @selected-red;
+ stroke-width: 10;
+}
+
+.NetworkUI__process path {
+ fill: @widget-body;
+ stroke: @dark-widget-detail;
+ stroke-width: 2;
+}
+
+.NetworkUI__process-text {
+ fill: @button-text;
+ font-family: 'Open Sans';
+}
+
+
+.NetworkUI__process-text--selected {
+ font-family: 'Open Sans';
+}
+
+.NetworkUI__stream {
+ fill: none;
+ stroke: @dark-widget-detail;
+}
+
+.NetworkUI__stream-arrow {
+ fill: @dark-widget-detail;
+ stroke: @dark-widget-detail;
+}
+
+.NetworkUI__stream--selected {
+ fill: none;
+ stroke: @selected-blue;
+ stroke-width: 6;
+}
+.NetworkUI__stream-arrow--selected {
+ fill: @selected-blue;
+ stroke: @selected-blue;
+}
+
+.NetworkUI__stream-text {
+ fill: @button-text;
+ font-size: 8px;
+ font-family: 'Open Sans';
+}
+
+.NetworkUI__toolbox--title{
+ fill: @dark-widget-detail;
+ text-transform: uppercase;
+ font-size: 14px;
+ font-weight: bold;
+}
+
+.NetworkUI__contextMenu{
+ fill: @light-background;
+ stroke: @button-outline;
+ stroke-width: 1;
+
+}
+
+.NetworkUI__contextMenuButton{
+ fill: @light-background;
+}
+
+.NetworkUI__contextMenuButtonText{
+ fill: @context-menu-text;
+ font-family: 'Open Sans';
+ font-size: 14px;
+}
+
+.NetworkUI__contextMenuButtonText-hover{
+ fill: @context-menu-text;
+ font-family: 'Open Sans';
+ font-size: 14px;
+}
+
+.NetworkUI__contextMenuButton-hover,
+.NetworkUI__contextMenuButton-pressed{
+ fill:@button-body-hover;
+}
+
+.NetworkUI__contextMenuRemoveButtonText{
+ fill: @red;
+ font-family: 'Open Sans';
+ font-size: 14px;
+}
+
+.NetworkUI__contextMenuRemoveButtonText-hover{
+ fill: @red;
+ font-family: 'Open Sans';
+ font-size: 14px;
+}
+
+.NetworkUI__test_results {
+ fill: @light-background;
+ stroke: @dark-widget-detail;
+}
diff --git a/awx/ui/client/src/network-ui/switch.directive.js b/awx/ui/client/src/network-ui/switch.directive.js
new file mode 100644
index 0000000000..a9fbb48a8d
--- /dev/null
+++ b/awx/ui/client/src/network-ui/switch.directive.js
@@ -0,0 +1,8 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+
+const templateUrl = require('~network-ui/switch.partial.svg');
+
+function switchd () {
+ return { restrict: 'A', templateUrl};
+}
+exports.switchd = switchd;
diff --git a/awx/ui/client/src/network-ui/switch.partial.svg b/awx/ui/client/src/network-ui/switch.partial.svg
new file mode 100644
index 0000000000..65692d8101
--- /dev/null
+++ b/awx/ui/client/src/network-ui/switch.partial.svg
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.name}}
+ {{item.name}}{{item.edit_label?'_':''}}
+
+
diff --git a/awx/ui/client/src/network-ui/templates/fsm.jst b/awx/ui/client/src/network-ui/templates/fsm.jst
new file mode 100644
index 0000000000..afe75f5cd8
--- /dev/null
+++ b/awx/ui/client/src/network-ui/templates/fsm.jst
@@ -0,0 +1,26 @@
+var inherits = require('inherits');
+var fsm = require('./fsm.js');
+
+function _State () {
+}
+inherits(_State, fsm._State);
+
+{%for state in states%}
+function _{{state.label}} () {
+ this.name = '{{state.label}}';
+}
+inherits(_{{state.label}}, _State);
+var {{state.label}} = new _{{state.label}}();
+exports.{{state.label}} = {{state.label}};
+{%endfor%}
+
+{%for state in states%}
+{%for fn, transitions in state.functions%}
+_{{state.label}}.prototype.{{fn}} = function (controller) {
+{%for tn in transitions %}
+ controller.changeState({{tn.to_state}});
+{%endfor%}
+};
+_{{state.label}}.prototype.{{fn}}.transitions = [{%for t in transitions%}'{{t.to_state}}'{% if not loop.last%}, {%endif%}{%endfor%}];
+{%endfor%}
+{%endfor%}
diff --git a/awx/ui/client/src/network-ui/test.fsm.js b/awx/ui/client/src/network-ui/test.fsm.js
new file mode 100644
index 0000000000..4867e92f59
--- /dev/null
+++ b/awx/ui/client/src/network-ui/test.fsm.js
@@ -0,0 +1,186 @@
+var inherits = require('inherits');
+var fsm = require('./fsm.js');
+var messages = require('./messages.js');
+var models = require('./models.js');
+
+function _State () {
+}
+inherits(_State, fsm._State);
+
+
+function _Disabled () {
+ this.name = 'Disabled';
+}
+inherits(_Disabled, _State);
+var Disabled = new _Disabled();
+exports.Disabled = Disabled;
+
+function _Start () {
+ this.name = 'Start';
+}
+inherits(_Start, _State);
+var Start = new _Start();
+exports.Start = Start;
+
+function _Running () {
+ this.name = 'Running';
+}
+inherits(_Running, _State);
+var Running = new _Running();
+exports.Running = Running;
+
+function _Loading () {
+ this.name = 'Loading';
+}
+inherits(_Loading, _State);
+var Loading = new _Loading();
+exports.Loading = Loading;
+
+function _Ready () {
+ this.name = 'Ready';
+}
+inherits(_Ready, _State);
+var Ready = new _Ready();
+exports.Ready = Ready;
+
+function _Reporting () {
+ this.name = 'Reporting';
+}
+inherits(_Reporting, _State);
+var Reporting = new _Reporting();
+exports.Reporting = Reporting;
+
+
+_State.prototype.onMessage = function(controller, msg_type, message) {
+
+ var type_data = JSON.parse(message.data);
+ var type = type_data[0];
+ var data = type_data[1];
+
+ controller.handle_message(type, data);
+};
+
+_State.prototype.onid = function(controller, msg_type, message) {
+ controller.scope.test_client_id = message;
+};
+
+_State.prototype.onTestCase = function(controller, msg_type, message) {
+ if ('runnable' in message[1]) {
+ if (!message[1].runnable) {
+ return;
+ }
+ }
+ controller.scope.tests.push(new models.Test(message[0],
+ message[1].event_trace,
+ [],
+ message[1].snapshots[0],
+ message[1].snapshots[1]));
+};
+
+_Disabled.prototype.onEnableTest = function (controller) {
+
+ controller.changeState(Ready);
+};
+_Disabled.prototype.onEnableTest.transitions = ['Ready'];
+
+
+
+_Start.prototype.start = function (controller) {
+
+ controller.changeState(Disabled);
+
+};
+_Start.prototype.start.transitions = ['Disabled'];
+
+
+
+_Running.prototype.onTestCompleted = function (controller) {
+
+ controller.changeState(Reporting);
+};
+_Running.prototype.onTestCompleted.transitions = ['Reporting'];
+
+_Reporting.prototype.start = function (controller) {
+
+ var test_result = null;
+ controller.scope.replay = false;
+ controller.scope.disconnected = false;
+ controller.scope.recording = false;
+ controller.scope.cursor.hidden = true;
+ var result = "passed";
+ if (controller.scope.test_errors.length > 0) {
+ result = "errored";
+ }
+ test_result = new models.TestResult(controller.scope.test_result_id_seq(),
+ controller.scope.current_test.name,
+ result,
+ new Date().toISOString(),
+ controller.scope.version);
+ controller.scope.test_results.push(test_result);
+ console.log(["Reporting test", test_result.name, test_result.id]);
+ controller.scope.send_test_message(new messages.TestResult(controller.scope.client_id,
+ test_result.id,
+ test_result.name,
+ test_result.result,
+ test_result.date,
+ test_result.code_under_test));
+ if (typeof(window.__coverage__) !== "undefined" && window.__coverage__ !== null) {
+ console.log(["Reporting coverage", test_result.name, test_result.id]);
+ controller.scope.send_test_message(new messages.Coverage(controller.scope.client_id, window.__coverage__, test_result.id));
+ }
+ controller.changeState(Loading);
+};
+_Reporting.prototype.start.transitions = ['Loading'];
+
+
+_Loading.prototype.start = function (controller) {
+
+ if (controller.scope.current_tests.length === 0) {
+ controller.changeState(Disabled);
+ } else {
+ console.log("Starting test");
+ controller.scope.reset_coverage();
+ controller.scope.current_test = controller.scope.current_tests.shift();
+ controller.scope.reset_toolboxes();
+ controller.scope.onSnapshot(controller.scope.current_test.pre_test_snapshot);
+ controller.scope.replay = true;
+ controller.scope.disconnected = true;
+ controller.scope.test_errors = [];
+ controller.scope.test_events = controller.scope.current_test.event_trace.slice();
+ controller.scope.test_events.push(new messages.TestCompleted());
+ controller.scope.reset_flags();
+ controller.scope.reset_fsm_state();
+ controller.scope.cancel_animations();
+ controller.scope.cursor.hidden = false;
+ controller.changeState(Running);
+ }
+};
+_Loading.prototype.start.transitions = ['Running'];
+
+
+
+_Ready.prototype.onDisableTest = function (controller) {
+
+ controller.changeState(Disabled);
+};
+_Ready.prototype.onDisableTest.transitions = ['Disabled'];
+
+_Ready.prototype.start = function (controller) {
+
+ var load_id = controller.scope.test_result_id_seq();
+
+ console.log(["Reporting Load", load_id]);
+ controller.scope.send_test_message(new messages.TestResult(controller.scope.client_id,
+ load_id,
+ "Load",
+ "passed",
+ new Date().toISOString(),
+ controller.scope.version));
+ if (typeof(window.__coverage__) !== "undefined" && window.__coverage__ !== null) {
+ console.log(["Reporting Load Coverage", load_id]);
+ controller.scope.send_test_message(new messages.Coverage(controller.scope.client_id, window.__coverage__, load_id));
+ }
+
+ controller.changeState(Loading);
+};
+_Ready.prototype.start.transitions = ['Loading'];
diff --git a/awx/ui/client/src/network-ui/test_results.directive.js b/awx/ui/client/src/network-ui/test_results.directive.js
new file mode 100644
index 0000000000..9430ad4056
--- /dev/null
+++ b/awx/ui/client/src/network-ui/test_results.directive.js
@@ -0,0 +1,8 @@
+/* Copyright (c) 2018 Red Hat, Inc. */
+
+const templateUrl = require('~network-ui/test_results.partial.svg');
+
+function test_results () {
+ return { restrict: 'A', templateUrl};
+}
+exports.test_results = test_results;
diff --git a/awx/ui/client/src/network-ui/test_results.partial.svg b/awx/ui/client/src/network-ui/test_results.partial.svg
new file mode 100644
index 0000000000..bc0c422a46
--- /dev/null
+++ b/awx/ui/client/src/network-ui/test_results.partial.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+Test Results {{version}}
+
+
+{{result.name}} - {{result.result}}
+
+
+
+
+
diff --git a/awx/ui/client/src/network-ui/time.fsm.js b/awx/ui/client/src/network-ui/time.fsm.js
new file mode 100644
index 0000000000..cd425df2ae
--- /dev/null
+++ b/awx/ui/client/src/network-ui/time.fsm.js
@@ -0,0 +1,195 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+var inherits = require('inherits');
+var fsm = require('./fsm.js');
+var util = require('./util.js');
+
+function _State () {
+}
+inherits(_State, fsm._State);
+
+function _Start () {
+ this.name = 'Start';
+}
+inherits(_Start, _State);
+var Start = new _Start();
+exports.Start = Start;
+
+function _Present () {
+ this.name = 'Present';
+}
+inherits(_Present, _State);
+var Present = new _Present();
+exports.Present = Present;
+
+_Start.prototype.start = function (controller) {
+
+ controller.changeState(Present);
+
+};
+_Start.prototype.start.transitions = ['Present'];
+
+_Present.prototype.onMessage = function(controller, msg_type, message) {
+
+ var type_data = JSON.parse(message.data);
+ var type = type_data[0];
+ var data = type_data[1];
+
+ controller.handle_message(type, data);
+};
+
+_Present.prototype.onMultipleMessage = function(controller, msg_type, message) {
+
+ var i = 0;
+ if (message.sender !== controller.scope.client_id) {
+ for (i = 0; i< message.messages.length; i++) {
+ controller.handle_message(message.messages[i].msg_type, message.messages[i]);
+ }
+ }
+};
+
+_Present.prototype.onDeviceCreate = function(controller, msg_type, message) {
+ if (message.sender !== controller.scope.client_id) {
+ controller.scope.onDeviceCreate(message);
+ }
+};
+_Present.prototype.onInterfaceCreate = function(controller, msg_type, message) {
+ if (message.sender !== controller.scope.client_id) {
+ controller.scope.onInterfaceCreate(message);
+ }
+};
+_Present.prototype.onLinkCreate = function(controller, msg_type, message) {
+ if (message.sender !== controller.scope.client_id) {
+ controller.scope.onLinkCreate(message);
+ }
+};
+_Present.prototype.onDeviceMove = function(controller, msg_type, message) {
+ if (message.sender !== controller.scope.client_id) {
+ controller.scope.onDeviceMove(message);
+ }
+};
+_Present.prototype.onDeviceDestroy = function(controller, msg_type, message) {
+ if (message.sender !== controller.scope.client_id) {
+ controller.scope.onDeviceDestroy(message);
+ }
+};
+_Present.prototype.onLinkDestroy = function(controller, msg_type, message) {
+ if (message.sender !== controller.scope.client_id) {
+ controller.scope.onLinkDestroy(message);
+ }
+};
+_Present.prototype.onDeviceSelected = function(controller, msg_type, message) {
+ if (message.sender !== controller.scope.client_id) {
+ controller.scope.onDeviceSelected(message);
+ }
+};
+_Present.prototype.onDeviceUnSelected = function(controller, msg_type, message) {
+ if (message.sender !== controller.scope.client_id) {
+ controller.scope.onDeviceUnSelected(message);
+ }
+};
+_Present.prototype.onSnapshot = function(controller, msg_type, message) {
+ if (message.sender !== controller.scope.client_id) {
+ controller.scope.onSnapshot(message);
+ }
+};
+_Present.prototype.onid = function(controller, msg_type, message) {
+ controller.scope.onClientId(message);
+};
+_Present.prototype.onTopology = function(controller, msg_type, message) {
+ controller.scope.onTopology(message);
+};
+_Present.prototype.onViewPort = function(controller, msg_type, message) {
+ if (message.sender === controller.scope.client_id) {
+ return;
+ }
+ controller.scope.current_scale = message.scale;
+ controller.scope.panX = message.panX;
+ controller.scope.panY = message.panY;
+ if (message.graph_width !== undefined) {
+ controller.scope.graph.width = message.graph_width;
+ }
+ if (message.graph_height !== undefined) {
+ controller.scope.graph.height = message.graph_height;
+ }
+ controller.scope.update_toolbox_heights();
+ controller.scope.updateScaledXY();
+ controller.scope.updatePanAndScale();
+};
+_Present.prototype.onMouseEvent = function(controller, msg_type, message) {
+ if (!controller.scope.replay) {
+ return;
+ }
+ if (message.sender === controller.scope.client_id) {
+ return;
+ }
+ message.preventDefault = util.noop;
+ if (message.type === "mousemove") {
+ controller.scope.onMouseMove(message);
+ }
+ if (message.type === "mouseup") {
+ controller.scope.onMouseUp(message);
+ }
+ if (message.type === "mousedown") {
+ controller.scope.onMouseDown(message);
+ }
+ if (message.type === "mouseover") {
+ controller.scope.onMouseOver(message);
+ }
+};
+_Present.prototype.onMouseWheelEvent = function(controller, msg_type, message) {
+ if (!controller.scope.replay) {
+ return;
+ }
+ if (message.sender === controller.scope.client_id) {
+ return;
+ }
+ message.preventDefault = util.noop;
+ message.stopPropagation = util.noop;
+ controller.scope.onMouseWheel(message, message.delta, message.deltaX, message.deltaY);
+};
+ _Present.prototype.onKeyEvent = function(controller, msg_type, message) {
+ if (!controller.scope.replay) {
+ return;
+ }
+ if (message.sender === controller.scope.client_id) {
+ return;
+ }
+ message.preventDefault = util.noop;
+ if (message.type === "keydown") {
+ controller.scope.onKeyDown(message);
+ }
+};
+
+_Present.prototype.onRecordButton = function(controller) {
+ controller.scope.onRecordButton();
+};
+
+_Present.prototype.onExportButton = function(controller) {
+ controller.scope.onExportButton();
+};
+
+_Present.prototype.onExportYamlButton = function(controller) {
+ controller.scope.onExportYamlButton();
+};
+
+_Present.prototype.onDownloadTraceButton = function(controller) {
+ controller.scope.onDownloadTraceButton();
+};
+
+_Present.prototype.onDownloadRecordingButton = function(controller) {
+ controller.scope.onDownloadRecordingButton();
+};
+
+_Present.prototype.onNoop = function() {
+
+};
+
+_Present.prototype.onTestCompleted = function(controller, msg_type, message) {
+
+ controller.scope.test_channel.send(msg_type, message);
+};
+
+
+_Present.prototype.onError = function(controller, msg_type, message) {
+ throw new Error("ServerError: " + message);
+};
diff --git a/awx/ui/client/src/network-ui/toolbox.fsm.js b/awx/ui/client/src/network-ui/toolbox.fsm.js
new file mode 100644
index 0000000000..c8a80c6463
--- /dev/null
+++ b/awx/ui/client/src/network-ui/toolbox.fsm.js
@@ -0,0 +1,332 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+var inherits = require('inherits');
+var fsm = require('./fsm.js');
+
+function _State () {
+}
+inherits(_State, fsm._State);
+
+
+function _Dropping () {
+ this.name = 'Dropping';
+}
+inherits(_Dropping, _State);
+var Dropping = new _Dropping();
+exports.Dropping = Dropping;
+
+function _Selecting () {
+ this.name = 'Selecting';
+}
+inherits(_Selecting, _State);
+var Selecting = new _Selecting();
+exports.Selecting = Selecting;
+
+function _Selected () {
+ this.name = 'Selected';
+}
+inherits(_Selected, _State);
+var Selected = new _Selected();
+exports.Selected = Selected;
+
+function _Ready () {
+ this.name = 'Ready';
+}
+inherits(_Ready, _State);
+var Ready = new _Ready();
+exports.Ready = Ready;
+
+function _Scrolling () {
+ this.name = 'Scrolling';
+}
+inherits(_Scrolling, _State);
+var Scrolling = new _Scrolling();
+exports.Scrolling = Scrolling;
+
+function _Start () {
+ this.name = 'Start';
+}
+inherits(_Start, _State);
+var Start = new _Start();
+exports.Start = Start;
+
+function _Move () {
+ this.name = 'Move';
+}
+inherits(_Move, _State);
+var Move = new _Move();
+exports.Move = Move;
+
+function _OffScreen () {
+ this.name = 'OffScreen';
+}
+inherits(_OffScreen, _State);
+var OffScreen = new _OffScreen();
+exports.OffScreen = OffScreen;
+
+function _OffScreen2 () {
+ this.name = 'OffScreen2';
+}
+inherits(_OffScreen2, _State);
+var OffScreen2 = new _OffScreen2();
+exports.OffScreen2 = OffScreen2;
+
+function _Disabled () {
+ this.name = 'Disabled';
+}
+inherits(_Disabled, _State);
+var Disabled = new _Disabled();
+exports.Disabled = Disabled;
+
+
+_Dropping.prototype.start = function (controller) {
+
+
+ var i = 0;
+ var toolbox = controller.toolbox;
+ for(i = 0; i < toolbox.items.length; i++) {
+ toolbox.items[i].selected = false;
+ }
+
+ controller.dropped_action(toolbox.selected_item);
+
+ if (controller.remove_on_drop && !toolbox.selected_item.template) {
+ var dindex = toolbox.items.indexOf(toolbox.selected_item);
+ if (dindex !== -1) {
+ toolbox.items.splice(dindex, 1);
+ }
+ }
+
+ toolbox.selected_item = null;
+ controller.changeState(Ready);
+};
+_Dropping.prototype.start.transitions = ['Ready'];
+
+
+
+_Selected.prototype.onMouseMove = function (controller) {
+
+ controller.changeState(Move);
+
+};
+_Selected.prototype.onMouseMove.transitions = ['Move'];
+
+_Selected.prototype.onMouseUp = function (controller) {
+
+ var i = 0;
+ var toolbox = controller.toolbox;
+ for(i = 0; i < toolbox.items.length; i++) {
+ toolbox.items[i].selected = false;
+ }
+ toolbox.selected_item = null;
+ controller.changeState(Ready);
+};
+_Selected.prototype.onMouseUp.transitions = ['Ready'];
+
+
+_Selecting.prototype.onMouseDown = function (controller) {
+
+ var i = 0;
+
+ var toolbox = controller.toolbox;
+ var scope = controller.scope;
+ var selected_item = Math.floor((controller.scope.mouseY - toolbox.y - toolbox.scroll_offset) / toolbox.spacing);
+
+ for(i = 0; i < toolbox.items.length; i++) {
+ toolbox.items[i].selected = false;
+ }
+ if (selected_item >= 0 && selected_item < toolbox.items.length) {
+ toolbox.items[selected_item].selected = true;
+ toolbox.selected_item = toolbox.items[selected_item];
+ scope.pressedX = scope.mouseX;
+ scope.pressedY = scope.mouseY;
+ scope.pressedScaledX = scope.scaledX;
+ scope.pressedScaledY = scope.scaledY;
+ toolbox.selected_item.x = toolbox.x + toolbox.width/2;
+ toolbox.selected_item.y = selected_item * toolbox.spacing + toolbox.y + toolbox.scroll_offset + toolbox.spacing/2;
+ controller.scope.clear_selections();
+ controller.scope.first_channel.send("UnselectAll", {});
+ controller.changeState(Selected);
+ } else {
+ toolbox.selected_item = null;
+ controller.changeState(Ready);
+ }
+
+};
+_Selecting.prototype.onMouseDown.transitions = ['Selected', 'Ready'];
+
+_Ready.prototype.onEnable = function () {
+
+};
+
+_Ready.prototype.onMouseDown = function (controller, msg_type, $event) {
+
+ if(controller.toolbox.enabled &&
+ controller.scope.mouseX > controller.toolbox.x &&
+ controller.scope.mouseY > controller.toolbox.y &&
+ controller.scope.mouseX < controller.toolbox.x + controller.toolbox.width &&
+ controller.scope.mouseY < controller.toolbox.y + controller.toolbox.height) {
+
+ controller.changeState(Selecting);
+ controller.handle_message(msg_type, $event);
+
+ } else {
+ controller.delegate_channel.send(msg_type, $event);
+ }
+};
+_Ready.prototype.onMouseDown.transitions = ['Selecting'];
+
+_Ready.prototype.onMouseWheel = function (controller, msg_type, $event) {
+
+ if(controller.toolbox.enabled &&
+ controller.scope.mouseX > controller.toolbox.x &&
+ controller.scope.mouseY > controller.toolbox.y &&
+ controller.scope.mouseX < controller.toolbox.x + controller.toolbox.width &&
+ controller.scope.mouseY < controller.toolbox.y + controller.toolbox.height) {
+
+ controller.changeState(Scrolling);
+ controller.handle_message(msg_type, $event);
+
+ } else {
+ controller.delegate_channel.send(msg_type, $event);
+ }
+};
+_Ready.prototype.onMouseWheel.transitions = ['Scrolling'];
+
+_Ready.prototype.onToggleToolbox = function (controller, msg_type, message) {
+
+ controller.changeState(OffScreen);
+ controller.delegate_channel.send(msg_type, message);
+
+};
+_Ready.prototype.onToggleToolbox.transitions = ['OffScreen'];
+
+_Ready.prototype.onDisable = function (controller) {
+
+ controller.changeState(Disabled);
+
+};
+_Ready.prototype.onDisable.transitions = ['Disabled'];
+
+_Scrolling.prototype.onMouseWheel = function (controller, msg_type, $event) {
+
+ var delta = $event[1];
+ controller.toolbox.scroll_offset += -1 * delta;
+ controller.toolbox.scroll_offset = Math.min(controller.toolbox.scroll_offset, 0);
+ controller.toolbox.scroll_offset = Math.max(controller.toolbox.scroll_offset,
+ -1 * controller.toolbox.spacing * (controller.toolbox.items.length + 1) + controller.toolbox.height);
+
+
+ controller.changeState(Ready);
+
+};
+_Scrolling.prototype.onMouseWheel.transitions = ['Ready'];
+
+
+
+_Start.prototype.start = function (controller) {
+
+ controller.changeState(Ready);
+
+};
+_Start.prototype.start.transitions = ['Ready'];
+
+
+
+_Move.prototype.onMouseUp = function (controller) {
+
+ controller.changeState(Dropping);
+
+};
+_Move.prototype.onMouseUp.transitions = ['Dropping'];
+
+
+_Move.prototype.onMouseMove = function (controller) {
+
+ var diffX = controller.scope.mouseX - controller.scope.pressedX;
+ var diffY = controller.scope.mouseY - controller.scope.pressedY;
+
+ controller.toolbox.selected_item.x += diffX;
+ controller.toolbox.selected_item.y += diffY;
+
+ controller.scope.pressedX = controller.scope.mouseX;
+ controller.scope.pressedY = controller.scope.mouseY;
+};
+
+
+_OffScreen.prototype.onToggleToolbox = function (controller, msg_type, message) {
+
+ controller.changeState(Ready);
+ controller.delegate_channel.send(msg_type, message);
+
+};
+_OffScreen.prototype.onToggleToolbox.transitions = ['Ready'];
+
+
+_OffScreen.prototype.start = function (controller) {
+
+ controller.toolbox.enabled = false;
+
+};
+
+_OffScreen.prototype.end = function (controller) {
+
+ controller.toolbox.enabled = true;
+
+};
+
+_OffScreen.prototype.onDisable = function (controller) {
+
+ controller.changeState(OffScreen2);
+};
+_OffScreen.prototype.onDisable.transitions = ['OffScreen2'];
+
+_OffScreen2.prototype.onEnable = function (controller) {
+
+ controller.changeState(OffScreen);
+};
+_OffScreen2.prototype.onEnable.transitions = ['OffScreen'];
+
+_OffScreen2.prototype.onDisable = function () {
+
+};
+
+_OffScreen2.prototype.start = function (controller) {
+
+ controller.toolbox.enabled = false;
+};
+
+_OffScreen2.prototype.onToggleToolbox = function (controller, msg_type, message) {
+
+ controller.changeState(Disabled);
+ controller.delegate_channel.send(msg_type, message);
+};
+_OffScreen2.prototype.onToggleToolbox.transitions = ['Disabled'];
+
+_Disabled.prototype.onDisable = function () {
+
+};
+
+_Disabled.prototype.onEnable = function (controller) {
+
+ controller.changeState(Ready);
+};
+_Disabled.prototype.onEnable.transitions = ['Ready'];
+
+_Disabled.prototype.start = function (controller) {
+ if(controller.toolbox !== undefined){
+ controller.toolbox.enabled = false;
+ }
+};
+
+_Disabled.prototype.end = function (controller) {
+
+ controller.toolbox.enabled = true;
+
+};
+
+_Disabled.prototype.onToggleToolbox = function (controller, msg_type, message) {
+
+ controller.changeState(OffScreen2);
+ controller.delegate_channel.send(msg_type, message);
+};
+_Disabled.prototype.onToggleToolbox.transitions = ['OffScreen2'];
diff --git a/awx/ui/client/src/network-ui/tools/copy-layout.py b/awx/ui/client/src/network-ui/tools/copy-layout.py
new file mode 100755
index 0000000000..dd7cfbf947
--- /dev/null
+++ b/awx/ui/client/src/network-ui/tools/copy-layout.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018 Benjamin Thomasson
+
+"""
+Usage:
+ copy-layout [options]
+
+Options:
+ -h, --help Show this page
+ --debug Show debug logging
+ --verbose Show verbose logging
+"""
+from docopt import docopt
+import logging
+import sys
+import yaml
+
+logger = logging.getLogger('copy-layout')
+
+
+def main(args=None):
+ if args is None:
+ args = sys.argv[1:]
+ parsed_args = docopt(__doc__, args)
+ if parsed_args['--debug']:
+ logging.basicConfig(level=logging.DEBUG)
+ elif parsed_args['--verbose']:
+ logging.basicConfig(level=logging.INFO)
+ else:
+ logging.basicConfig(level=logging.WARNING)
+
+ with open(parsed_args['']) as f:
+ from_fsm = yaml.load(f.read())
+ with open(parsed_args['']) as f:
+ to_fsm = yaml.load(f.read())
+
+ to_states = {x['label']: x for x in to_fsm.get('states', [])}
+
+ to_fsm['name'] = from_fsm.get('name', '')
+ to_fsm['finite_state_machine_id'] = from_fsm.get('finite_state_machine_id', '')
+ to_fsm['diagram_id'] = from_fsm.get('diagram_id', '')
+
+ for state in from_fsm.get('states', []):
+ to_states.get(state['label'], {})['x'] = state.get('x', 0)
+ to_states.get(state['label'], {})['y'] = state.get('y', 0)
+
+ with open(parsed_args[''], 'w') as f:
+ f.write(yaml.safe_dump(to_fsm, default_flow_style=False))
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
diff --git a/awx/ui/client/src/network-ui/tools/fsm-diff b/awx/ui/client/src/network-ui/tools/fsm-diff
new file mode 100755
index 0000000000..5a400e96fe
--- /dev/null
+++ b/awx/ui/client/src/network-ui/tools/fsm-diff
@@ -0,0 +1,9 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from fsm_diff.cli import main
+
+if __name__ == '__main__':
+ import sys
+ sys.exit(main(sys.argv[1:]))
+
diff --git a/awx/ui/client/src/network-ui/tools/fsm_diff/__init__.py b/awx/ui/client/src/network-ui/tools/fsm_diff/__init__.py
new file mode 100755
index 0000000000..268c10ff40
--- /dev/null
+++ b/awx/ui/client/src/network-ui/tools/fsm_diff/__init__.py
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+__author__ = 'Ben Thomasson'
+__email__ = 'benthomasson@gmail.com'
+__version__ = '0.1.0'
diff --git a/awx/ui/client/src/network-ui/tools/fsm_diff/cli.py b/awx/ui/client/src/network-ui/tools/fsm_diff/cli.py
new file mode 100644
index 0000000000..d180676016
--- /dev/null
+++ b/awx/ui/client/src/network-ui/tools/fsm_diff/cli.py
@@ -0,0 +1,79 @@
+"""
+Usage:
+ fsm_diff [options] []
+
+Options:
+ -h, --help Show this page
+ --debug Show debug logging
+ --verbose Show verbose logging
+"""
+from docopt import docopt
+import logging
+import sys
+import yaml
+
+logger = logging.getLogger('cli')
+
+
+def fsm_diff(a_name, b_name, a, b, silent=True):
+
+ a_states = {x['label'] for x in a['states']}
+ b_states = {x['label'] for x in b['states']}
+
+ missing_in_a = b_states - a_states
+ missing_in_b = a_states - b_states
+
+
+ if (missing_in_b) and not silent:
+ print "Extra states in " + a_name + ":\n ", "\n ".join(list(missing_in_b))
+
+ if (missing_in_a) and not silent:
+ print "Extra states in " + b_name + ":\n ", "\n ".join(list(missing_in_a))
+
+ new_states = missing_in_b.union(missing_in_a)
+
+ a_transitions = {tuple(sorted(x.items())) for x in a['transitions']}
+ b_transitions = {tuple(sorted(x.items())) for x in b['transitions']}
+
+ missing_in_a = b_transitions - a_transitions
+ missing_in_b = a_transitions - b_transitions
+
+
+ if (missing_in_b) and not silent:
+ print "Extra transitions in " + a_name + ":\n ", "\n ".join(map(str, missing_in_b))
+
+ if (missing_in_a) and not silent:
+ print "Extra transitions in " + b_name + ":\n ", "\n ".join(map(str, missing_in_a))
+
+ new_transitions = missing_in_b.union(missing_in_a)
+
+ data = dict(states=[dict(label=x) for x in list(new_states)],
+ transitions=[dict(x) for x in list(new_transitions)])
+
+ return data
+
+
+def main(args=None):
+ if args is None:
+ args = sys.argv[1:]
+ parsed_args = docopt(__doc__, args)
+ if parsed_args['--debug']:
+ logging.basicConfig(level=logging.DEBUG)
+ elif parsed_args['--verbose']:
+ logging.basicConfig(level=logging.INFO)
+ else:
+ logging.basicConfig(level=logging.WARNING)
+
+ with open(parsed_args['']) as f:
+ a = yaml.load(f.read())
+
+ with open(parsed_args['']) as f:
+ b = yaml.load(f.read())
+
+ data = fsm_diff(parsed_args[''], parsed_args[''], a, b, silent=False)
+
+ if parsed_args['']:
+ with open(parsed_args[''], 'w') as f:
+ f.write(yaml.dump(data, default_flow_style=False))
+
+ return 0
diff --git a/awx/ui/client/src/network-ui/tools/fsm_generate_diffs.py b/awx/ui/client/src/network-ui/tools/fsm_generate_diffs.py
new file mode 100755
index 0000000000..b5df78c419
--- /dev/null
+++ b/awx/ui/client/src/network-ui/tools/fsm_generate_diffs.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017 Red Hat, Inc
+
+"""
+Usage:
+ fsm_generate_diffs [options]
+
+Options:
+ -h, --help Show this page
+ --debug Show debug logging
+ --verbose Show verbose logging
+ --append Append the newly generated code to the implementation.
+"""
+from docopt import docopt
+import logging
+import sys
+import fsm_diff.cli
+import transform_fsm
+import yaml
+
+from jinja2 import FileSystemLoader, Environment
+
+from subprocess import Popen, PIPE
+
+logger = logging.getLogger('fsm_generate_diffs')
+
+
+def main(args=None):
+ if args is None:
+ args = sys.argv[1:]
+ parsed_args = docopt(__doc__, args)
+ if parsed_args['--debug']:
+ logging.basicConfig(level=logging.DEBUG)
+ elif parsed_args['--verbose']:
+ logging.basicConfig(level=logging.INFO)
+ else:
+ logging.basicConfig(level=logging.WARNING)
+
+ implementation = parsed_args['']
+
+ p = Popen(['./extract.js', implementation], stdout=PIPE)
+ output = p.communicate()[0]
+ if p.returncode == 0:
+ b = yaml.load(output)
+ else:
+ return 1
+
+ with open(parsed_args['']) as f:
+ a = yaml.load(f.read())
+
+ data = fsm_diff.cli.fsm_diff(parsed_args[''], parsed_args[''], a, b)
+ data = transform_fsm.transform_fsm(data)
+
+ env = Environment(loader=FileSystemLoader("templates"))
+ template = env.get_template('fsm.jst')
+
+ if parsed_args['--append']:
+ with open(implementation, "a") as f:
+ f.write(template.render(**data))
+ else:
+ print (template.render(**data))
+
+
+
+ return 0
+
+if __name__ == '__main__':
+ import sys
+ sys.exit(main(sys.argv[1:]))
diff --git a/awx/ui/client/src/network-ui/tools/requirements.txt b/awx/ui/client/src/network-ui/tools/requirements.txt
new file mode 100644
index 0000000000..ec4c402207
--- /dev/null
+++ b/awx/ui/client/src/network-ui/tools/requirements.txt
@@ -0,0 +1,4 @@
+jinja2
+docopt
+pyyaml
+
diff --git a/awx/ui/client/src/network-ui/tools/transform_fsm.py b/awx/ui/client/src/network-ui/tools/transform_fsm.py
new file mode 100755
index 0000000000..9b5fd0a66a
--- /dev/null
+++ b/awx/ui/client/src/network-ui/tools/transform_fsm.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017 Red Hat, Inc
+
+"""
+Usage:
+ transform_fsm [options]
+
+Options:
+ -h, --help Show this page
+ --debug Show debug logging
+ --verbose Show verbose logging
+"""
+from docopt import docopt
+import logging
+import sys
+import yaml
+
+logger = logging.getLogger('transform_fsm')
+
+
+def transform_fsm(data):
+
+ state_map = dict()
+
+ for state in data['states']:
+ state_map[state['label']] = state
+ state['functions'] = dict()
+
+ for transition in data['transitions']:
+ state = state_map.get(transition['from_state'], dict(label=transition['from_state'], functions=dict()))
+ state_map[transition['from_state']] = state
+ if state not in data['states']:
+ data['states'].append(state)
+ function_transitions = state['functions'].get(transition['label'], list())
+ function_transitions.append(dict(to_state=transition['to_state']))
+ state['functions'][transition['label']] = function_transitions
+
+ for state in data['states']:
+ state['functions'] = sorted(state['functions'].items())
+
+ return data
+
+def main(args=None):
+ if args is None:
+ args = sys.argv[1:]
+ parsed_args = docopt(__doc__, args)
+ if parsed_args['--debug']:
+ logging.basicConfig(level=logging.DEBUG)
+ elif parsed_args['--verbose']:
+ logging.basicConfig(level=logging.INFO)
+ else:
+ logging.basicConfig(level=logging.WARNING)
+
+ with open(parsed_args[' ']) as f:
+ data = yaml.load(f.read())
+
+ data = transform_fsm(data)
+
+ with open(parsed_args[''], 'w') as f:
+ f.write(yaml.safe_dump(data, default_flow_style=False))
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
+
diff --git a/awx/ui/client/src/network-ui/tower.app.js b/awx/ui/client/src/network-ui/tower.app.js
new file mode 100644
index 0000000000..200d5078ce
--- /dev/null
+++ b/awx/ui/client/src/network-ui/tower.app.js
@@ -0,0 +1,8 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+
+var angular = require('angular');
+
+var tower = angular.module('tower', ['networkUI', 'ui.router']);
+
+exports.tower = tower;
+
diff --git a/awx/ui/client/src/network-ui/util.js b/awx/ui/client/src/network-ui/util.js
new file mode 100644
index 0000000000..f732f7bcf5
--- /dev/null
+++ b/awx/ui/client/src/network-ui/util.js
@@ -0,0 +1,182 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+var math = require('mathjs');
+
+function noop () {
+}
+exports.noop = noop;
+
+function natural_numbers (start) {
+ var counter = start;
+ return function () {return counter += 1;};
+}
+exports.natural_numbers = natural_numbers;
+
+
+function distance (x1, y1, x2, y2) {
+
+ return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
+}
+exports.distance = distance;
+
+function pDistanceLine(x, y, x1, y1, x2, y2) {
+ //Code from http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
+ //Joshua
+ // Find the dot product of two vectors ,
+ // Divide by the length squared of
+ // Use scalar project to find param
+ //
+
+ var A = x - x1;
+ var B = y - y1;
+ var C = x2 - x1;
+ var D = y2 - y1;
+
+ var dot = A * C + B * D;
+ var len_sq = C * C + D * D;
+ var param = -1;
+ if (len_sq !== 0) {
+ //in case of 0 length line
+ param = dot / len_sq;
+ }
+
+ var xx, yy;
+
+ //Find a point xx, yy where the projection and the vector intersect.
+ //If less than 0 use x1, y1 as the closest point.
+ //If less than 1 use x2, y2 as the closest point.
+ //If between 0 and 1 use the projection intersection xx, yy
+ if (param < 0) {
+ xx = x1;
+ yy = y1;
+ }
+ else if (param > 1) {
+ xx = x2;
+ yy = y2;
+ }
+ else {
+ xx = x1 + param * C;
+ yy = y1 + param * D;
+ }
+
+ return {x1:x, y1:y, x2: xx, y2: yy};
+}
+exports.pDistanceLine = pDistanceLine;
+
+function pDistance(x, y, x1, y1, x2, y2) {
+ //Code from http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
+ //Joshua
+ // Find the dot product of two vectors ,
+ // Divide by the length squared of
+ // Use scalar project to find param
+ //
+
+ var A = x - x1;
+ var B = y - y1;
+ var C = x2 - x1;
+ var D = y2 - y1;
+
+ var dot = A * C + B * D;
+ var len_sq = C * C + D * D;
+ var param = -1;
+ if (len_sq !== 0) {
+ //in case of 0 length line
+ param = dot / len_sq;
+ }
+
+ var xx, yy;
+
+ //Find a point xx, yy where the projection and the vector intersect.
+ //If less than 0 use x1, y1 as the closest point.
+ //If less than 1 use x2, y2 as the closest point.
+ //If between 0 and 1 use the projection intersection xx, yy
+ if (param < 0) {
+ xx = x1;
+ yy = y1;
+ }
+ else if (param > 1) {
+ xx = x2;
+ yy = y2;
+ }
+ else {
+ xx = x1 + param * C;
+ yy = y1 + param * D;
+ }
+
+ var dx = x - xx;
+ var dy = y - yy;
+ return Math.sqrt(dx * dx + dy * dy);
+}
+exports.pDistance = pDistance;
+
+
+function cross_z_pos(x, y, x1, y1, x2, y2) {
+ var A = x - x1;
+ var B = y - y1;
+ var C = x2 - x1;
+ var D = y2 - y1;
+
+ return math.cross([A, B, 0], [C, D, 0])[2] > 0;
+}
+exports.cross_z_pos = cross_z_pos;
+
+function intersection (x1, y1, x2, y2, x3, y3, x4, y4) {
+ //Find the point where lines through x1, y1, x2, y2 and x3, y3, x4, y4 intersect.
+ //
+
+ var Aslope;
+ var Aintercept;
+ var Bslope;
+ var Bintercept;
+
+ if ((x2 - x1) !== 0 && (x4 - x3) !== 0) {
+ Aslope = (y2 - y1)/(x2 - x1);
+ Aintercept = y1 - Aslope * x1;
+
+ Bslope = (y4 - y3)/(x4 - x3);
+ Bintercept = y3 - Bslope * x3;
+
+ var xi = (Bintercept - Aintercept) / (Aslope - Bslope);
+ var yi = Bslope * xi + Bintercept;
+ return {x: xi, y: yi};
+ }
+ if ((x2 - x1) === 0 && (x4 - x3) === 0) {
+ return {x: null, y: null};
+ }
+ if ((x2 - x1) === 0) {
+ Bslope = (y4 - y3)/(x4 - x3);
+ Bintercept = y3 - Bslope * x3;
+ return {x: x1, y: Bslope * x1 + Bintercept};
+ }
+ if ((x4 - x3) === 0) {
+ Aslope = (y2 - y1)/(x2 - x1);
+ Aintercept = y1 - Aslope * x1;
+ return {x: x3, y: Aslope * x3 + Aintercept};
+ }
+}
+exports.intersection = intersection;
+
+
+function pCase(x, y, x1, y1, x2, y2) {
+ //Code from http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
+ //Joshua
+ // Find the dot product of two vectors ,
+ // Divide by the length squared of
+ // Use scalar project to find param
+ //
+
+ var A = x - x1;
+ var B = y - y1;
+ var C = x2 - x1;
+ var D = y2 - y1;
+
+ var dot = A * C + B * D;
+ var len_sq = C * C + D * D;
+ var param = -1;
+ if (len_sq !== 0) {
+ //in case of 0 length line
+ param = dot / len_sq;
+ }
+
+ return param;
+}
+exports.pCase = pCase;
diff --git a/awx/ui/client/src/network-ui/vendor/svg-crowbar.js b/awx/ui/client/src/network-ui/vendor/svg-crowbar.js
new file mode 100644
index 0000000000..bba3093801
--- /dev/null
+++ b/awx/ui/client/src/network-ui/vendor/svg-crowbar.js
@@ -0,0 +1,250 @@
+/**
+ * @license svg-crowbar
+ * (c) 2013 The New York Times
+ * License: MIT
+ */
+function svg_crowbar () {
+ var doctype = '';
+
+ window.URL = (window.URL || window.webkitURL);
+
+ var body = document.body;
+
+ var prefix = {
+ xmlns: "http://www.w3.org/2000/xmlns/",
+ xlink: "http://www.w3.org/1999/xlink",
+ svg: "http://www.w3.org/2000/svg"
+ };
+
+ initialize();
+
+ function initialize() {
+ var documents = [window.document],
+ SVGSources = [],
+ iframes = document.querySelectorAll("iframe"),
+ objects = document.querySelectorAll("object");
+
+ [].forEach.call(iframes, function(el) {
+ try {
+ if (el.contentDocument) {
+ documents.push(el.contentDocument);
+ }
+ } catch(err) {
+ console.log(err);
+ }
+ });
+
+ [].forEach.call(objects, function(el) {
+ try {
+ if (el.contentDocument) {
+ documents.push(el.contentDocument);
+ }
+ } catch(err) {
+ console.log(err);
+ }
+ });
+
+ documents.forEach(function(doc) {
+ var styles = getStyles(doc);
+ var newSources = getSources(doc, styles);
+ // because of prototype on NYT pages
+ for (var i = 0; i < newSources.length; i++) {
+ SVGSources.push(newSources[i]);
+ }
+ });
+ if (SVGSources.length > 1) {
+ createPopover(SVGSources);
+ } else if (SVGSources.length > 0) {
+ download(SVGSources[0]);
+ } else {
+ alert("The Crowbar couldn’t find any SVG nodes.");
+ }
+ }
+
+ function createPopover(sources) {
+ cleanup();
+
+ sources.forEach(function(s1) {
+ sources.forEach(function(s2) {
+ if (s1 !== s2) {
+ if ((Math.abs(s1.top - s2.top) < 38) && (Math.abs(s1.left - s2.left) < 38)) {
+ s2.top += 38;
+ s2.left += 38;
+ }
+ }
+ });
+ });
+
+ var buttonsContainer = document.createElement("div");
+ body.appendChild(buttonsContainer);
+
+ buttonsContainer.setAttribute("class", "svg-crowbar");
+ buttonsContainer.style["z-index"] = 1e7;
+ buttonsContainer.style.position = "absolute";
+ buttonsContainer.style.top = 0;
+ buttonsContainer.style.left = 0;
+
+
+
+ var background = document.createElement("div");
+ body.appendChild(background);
+
+ background.setAttribute("class", "svg-crowbar");
+ background.style.background = "rgba(255, 255, 255, 0.7)";
+ background.style.position = "fixed";
+ background.style.left = 0;
+ background.style.top = 0;
+ background.style.width = "100%";
+ background.style.height = "100%";
+
+ sources.forEach(function(d, i) {
+ var buttonWrapper = document.createElement("div");
+ buttonsContainer.appendChild(buttonWrapper);
+ buttonWrapper.setAttribute("class", "svg-crowbar");
+ buttonWrapper.style.position = "absolute";
+ buttonWrapper.style.top = (d.top + document.body.scrollTop) + "px";
+ buttonWrapper.style.left = (document.body.scrollLeft + d.left) + "px";
+ buttonWrapper.style.padding = "4px";
+ buttonWrapper.style["border-radius"] = "3px";
+ buttonWrapper.style.color = "white";
+ buttonWrapper.style["text-align"] = "center";
+ buttonWrapper.style["font-family"] = "'Helvetica Neue'";
+ buttonWrapper.style.background = "rgba(0, 0, 0, 0.8)";
+ buttonWrapper.style["box-shadow"] = "0px 4px 18px rgba(0, 0, 0, 0.4)";
+ buttonWrapper.style.cursor = "move";
+ buttonWrapper.textContent = "SVG #" + i + ": " + (d.id ? "#" + d.id : "") + (d.class ? "." + d.class : "");
+
+ var button = document.createElement("button");
+ buttonWrapper.appendChild(button);
+ button.setAttribute("data-source-id", i);
+ button.style.width = "150px";
+ button.style["font-size"] = "12px";
+ button.style["line-height"] = "1.4em";
+ button.style.margin = "5px 0 0 0";
+ button.textContent = "Download";
+
+ button.onclick = function() {
+ // console.log(el, d, i, sources)
+ download(d);
+ };
+
+ });
+
+ }
+
+ function cleanup() {
+ var crowbarElements = document.querySelectorAll(".svg-crowbar");
+
+ [].forEach.call(crowbarElements, function(el) {
+ el.parentNode.removeChild(el);
+ });
+ }
+
+
+ function getSources(doc, styles) {
+ var svgInfo = [],
+ svgs = doc.querySelectorAll("svg");
+
+ styles = (styles === undefined) ? "" : styles;
+
+ [].forEach.call(svgs, function (svg) {
+
+ svg.setAttribute("version", "1.1");
+
+ var defsEl = document.createElement("defs");
+ svg.insertBefore(defsEl, svg.firstChild); //TODO .insert("defs", ":first-child")
+ // defsEl.setAttribute("class", "svg-crowbar");
+
+ var styleEl = document.createElement("style");
+ defsEl.appendChild(styleEl);
+ styleEl.setAttribute("type", "text/css");
+
+
+ // removing attributes so they aren't doubled up
+ svg.removeAttribute("xmlns");
+ svg.removeAttribute("xlink");
+
+ // These are needed for the svg
+ if (!svg.hasAttributeNS(prefix.xmlns, "xmlns")) {
+ svg.setAttributeNS(prefix.xmlns, "xmlns", prefix.svg);
+ }
+
+ if (!svg.hasAttributeNS(prefix.xmlns, "xmlns:xlink")) {
+ svg.setAttributeNS(prefix.xmlns, "xmlns:xlink", prefix.xlink);
+ }
+
+ var source = (new XMLSerializer()).serializeToString(svg).replace('', '');
+ var rect = svg.getBoundingClientRect();
+ svgInfo.push({
+ top: rect.top,
+ left: rect.left,
+ width: rect.width,
+ height: rect.height,
+ class: svg.getAttribute("class"),
+ id: svg.getAttribute("id"),
+ childElementCount: svg.childElementCount,
+ source: [doctype + source]
+ });
+ });
+ return svgInfo;
+ }
+
+ function download(source) {
+ var filename = "untitled";
+
+ if (source.id) {
+ filename = source.id;
+ } else if (source.class) {
+ filename = source.class;
+ } else if (window.document.title) {
+ filename = window.document.title.replace(/[^a-z0-9]/gi, '-').toLowerCase();
+ }
+
+ var url = window.URL.createObjectURL(new Blob(source.source, { "type" : "text\/xml" }));
+
+ var a = document.createElement("a");
+ body.appendChild(a);
+ a.setAttribute("class", "svg-crowbar");
+ a.setAttribute("download", filename + ".svg");
+ a.setAttribute("href", url);
+ a.style.display = "none";
+ a.click();
+
+ setTimeout(function() {
+ window.URL.revokeObjectURL(url);
+ }, 10);
+ }
+
+ function getStyles(doc) {
+ var styles = "",
+ styleSheets = doc.styleSheets;
+
+ if (styleSheets) {
+ for (var i = 0; i < styleSheets.length; i++) {
+ processStyleSheet(styleSheets[i]);
+ }
+ }
+
+ function processStyleSheet(ss) {
+ if (ss.cssRules) {
+ for (var i = 0; i < ss.cssRules.length; i++) {
+ var rule = ss.cssRules[i];
+ if (rule.type === 3) {
+ // Import Rule
+ processStyleSheet(rule.styleSheet);
+ } else {
+ // hack for illustrator crashing on descendent selectors
+ if (rule.selectorText) {
+ if (rule.selectorText.indexOf(">") === -1) {
+ styles += "\n" + rule.cssText;
+ }
+ }
+ }
+ }
+ }
+ }
+ return styles;
+ }
+
+}
+exports.svg_crowbar = svg_crowbar;
diff --git a/awx/ui/client/src/network-ui/view.fsm.js b/awx/ui/client/src/network-ui/view.fsm.js
new file mode 100644
index 0000000000..b1e047c1da
--- /dev/null
+++ b/awx/ui/client/src/network-ui/view.fsm.js
@@ -0,0 +1,124 @@
+/* Copyright (c) 2017 Red Hat, Inc. */
+var inherits = require('inherits');
+var fsm = require('./fsm.js');
+
+function _State () {
+}
+inherits(_State, fsm._State);
+
+function _Ready () {
+ this.name = 'Ready';
+}
+inherits(_Ready, _State);
+var Ready = new _Ready();
+exports.Ready = Ready;
+
+function _Start () {
+ this.name = 'Start';
+}
+inherits(_Start, _State);
+var Start = new _Start();
+exports.Start = Start;
+
+function _Scale () {
+ this.name = 'Scale';
+}
+inherits(_Scale, _State);
+var Scale = new _Scale();
+exports.Scale = Scale;
+
+function _Pressed () {
+ this.name = 'Pressed';
+}
+inherits(_Pressed, _State);
+var Pressed = new _Pressed();
+exports.Pressed = Pressed;
+
+function _Pan () {
+ this.name = 'Pan';
+}
+inherits(_Pan, _State);
+var Pan = new _Pan();
+exports.Pan = Pan;
+
+
+
+
+_Ready.prototype.onMouseDown = function (controller) {
+
+ controller.scope.pressedX = controller.scope.mouseX;
+ controller.scope.pressedY = controller.scope.mouseY;
+ controller.scope.lastPanX = controller.scope.panX;
+ controller.scope.lastPanY = controller.scope.panY;
+ controller.scope.closeDetailsPanel();
+ controller.changeState(Pressed);
+
+};
+_Ready.prototype.onMouseDown.transitions = ['Pressed'];
+
+_Ready.prototype.onMouseWheel = function (controller, msg_type, $event) {
+
+ controller.changeState(Scale);
+ controller.handle_message(msg_type, $event);
+};
+_Ready.prototype.onMouseWheel.transitions = ['Scale'];
+
+
+_Start.prototype.start = function (controller) {
+
+ controller.changeState(Ready);
+
+};
+_Start.prototype.start.transitions = ['Ready'];
+
+_Scale.prototype.onMouseWheel = function (controller, msg_type, message) {
+ var delta = message[1];
+ if (Math.abs(delta) > 100) {
+ delta = delta / 10;
+ }
+ var new_scale = Math.max(controller.scope.MIN_ZOOM,
+ Math.min(controller.scope.MAX_ZOOM,
+ (controller.scope.current_scale + delta / (100 / controller.scope.current_scale))));
+ var new_panX = controller.scope.mouseX - new_scale * ((controller.scope.mouseX - controller.scope.panX) / controller.scope.current_scale);
+ var new_panY = controller.scope.mouseY - new_scale * ((controller.scope.mouseY - controller.scope.panY) / controller.scope.current_scale);
+ controller.scope.updateScaledXY();
+ controller.scope.current_scale = new_scale;
+ controller.scope.panX = new_panX;
+ controller.scope.panY = new_panY;
+ var item = controller.scope.context_menus[0];
+ item.enabled = false;
+ controller.scope.$emit('awxNet-UpdateZoomWidget', controller.scope.current_scale, true);
+ controller.scope.updatePanAndScale();
+ controller.changeState(Ready);
+};
+_Scale.prototype.onMouseWheel.transitions = ['Ready'];
+
+
+_Pressed.prototype.onMouseUp = function (controller) {
+
+ controller.changeState(Ready);
+
+};
+_Pressed.prototype.onMouseUp.transitions = ['Ready'];
+
+_Pressed.prototype.onMouseMove = function (controller, msg_type, $event) {
+
+ controller.changeState(Pan);
+ controller.handle_message(msg_type, $event);
+};
+_Pressed.prototype.onMouseMove.transitions = ['Pan'];
+
+_Pan.prototype.onMouseMove = function (controller) {
+
+ controller.scope.panX = (controller.scope.mouseX - controller.scope.pressedX) + controller.scope.lastPanX;
+ controller.scope.panY = (controller.scope.mouseY - controller.scope.pressedY) + controller.scope.lastPanY;
+ controller.scope.updateScaledXY();
+ controller.scope.updatePanAndScale();
+};
+
+_Pan.prototype.onMouseUp = function (controller) {
+
+ controller.changeState(Ready);
+
+};
+_Pan.prototype.onMouseUp.transitions = ['Ready'];
diff --git a/awx/ui/client/src/network-ui/zoom-widget/main.js b/awx/ui/client/src/network-ui/zoom-widget/main.js
new file mode 100644
index 0000000000..518ec0976b
--- /dev/null
+++ b/awx/ui/client/src/network-ui/zoom-widget/main.js
@@ -0,0 +1,11 @@
+/*************************************************
+ * Copyright (c) 2018 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+import awxNetZoomWidget from './zoom.directive';
+
+export default
+ angular.module('networkZoomWidget', [])
+ .directive('awxNetZoomWidget', awxNetZoomWidget);
diff --git a/awx/ui/client/src/network-ui/zoom-widget/zoom.block.less b/awx/ui/client/src/network-ui/zoom-widget/zoom.block.less
new file mode 100644
index 0000000000..d827dcbe1e
--- /dev/null
+++ b/awx/ui/client/src/network-ui/zoom-widget/zoom.block.less
@@ -0,0 +1,59 @@
+.Networking-zoomPanel{
+ position: absolute;
+ bottom:0px;
+ right:0px;
+ border-top: 1px solid @btn-bord;
+ border-left: 1px solid @btn-bord;
+ width: 200px;
+ height:60px;
+}
+
+.Networking-zoomPanel--expanded{
+ right:400px;
+}
+
+.NetworkingControls-manualControls{
+ position: absolute;
+ background-color: @default-bg;
+ padding-left:15px;
+ width: 100%;
+}
+
+.NetworkingControls-Zoom{
+ display: flex;
+ flex: 1 0 auto;
+}
+
+.NetworkingControls-Zoom--button {
+ line-height: 60px;
+ color: @default-icon;
+}
+
+.NetworkingControls-Zoom--button:hover {
+ color: @default-link-hov;
+}
+
+.NetworkingControls-zoomSlider {
+ width: 150px;
+ padding-left: 8px;
+ padding-right: 8px;
+}
+
+.NetworkingControls-zoomPercentage {
+ text-align: center;
+ font-size: 0.7em;
+ height: 24px;
+ line-height: 24px;
+}
+
+.ui-slider-handle.ui-state-default.ui-corner-all {
+ border-radius: 50%;
+ border-color: @default-link;
+ background: @default-link;
+}
+
+.ui-slider-handle.ui-state-default.ui-corner-all:hover,
+.ui-slider-handle.ui-state-default.ui-corner-all:active {
+ border-color: @default-link-hov;
+ background: @default-link-hov;
+}
diff --git a/awx/ui/client/src/network-ui/zoom-widget/zoom.directive.js b/awx/ui/client/src/network-ui/zoom-widget/zoom.directive.js
new file mode 100644
index 0000000000..b4ba023988
--- /dev/null
+++ b/awx/ui/client/src/network-ui/zoom-widget/zoom.directive.js
@@ -0,0 +1,60 @@
+/*************************************************
+ * Copyright (c) 2018 Ansible, Inc.
+ *
+ * All Rights Reserved
+ *************************************************/
+
+// import detailsController from './details.controller';
+
+const templateUrl = require('~network-ui/zoom-widget/zoom.partial.html');
+
+export default [
+ function() {
+ return {
+ templateUrl,
+ restrict: 'E',
+ link(scope){
+
+ function init() {
+ scope.zoom = 100;
+ $( "#networking-slider" ).slider({
+ value:100,
+ min: 0,
+ max: 200,
+ step: 10,
+ slide: function( event, ui ) {
+ scope.zoom = ui.value;
+ scope.zoomTo();
+ }
+ });
+ }
+
+ init();
+
+ scope.$parent.$on('awxNet-UpdateZoomWidget', (e, scale, updateBoolean) => {
+ if(scale && updateBoolean){
+ // scale is included, meaning this was triggered by
+ // the view FSM's onMouseWheel transition
+ let sliderPercent = 120 * (Math.log10(scale) + 1);
+ scope.zoom = Math.round(sliderPercent / 10) * 10;
+ }
+ $("#networking-slider").slider('value', scope.zoom);
+ });
+
+ scope.zoomTo = function() {
+ scope.zoom = Math.ceil(scope.zoom / 10) * 10;
+ this.$parent.$broadcast('awxNet-zoom', scope.zoom);
+ };
+
+ scope.zoomOut = function(){
+ scope.zoom = scope.zoom - 10 > 0 ? scope.zoom - 10 : 0;
+ this.$parent.$broadcast('awxNet-zoom', scope.zoom);
+ };
+
+ scope.zoomIn = function(){
+ scope.zoom = scope.zoom + 10 < 200 ? scope.zoom + 10 : 200;
+ this.$parent.$broadcast('awxNet-zoom', scope.zoom);
+ };
+ }
+ };
+}];
diff --git a/awx/ui/client/src/network-ui/zoom-widget/zoom.partial.html b/awx/ui/client/src/network-ui/zoom-widget/zoom.partial.html
new file mode 100644
index 0000000000..09fa5acb70
--- /dev/null
+++ b/awx/ui/client/src/network-ui/zoom-widget/zoom.partial.html
@@ -0,0 +1,14 @@
+
diff --git a/awx/ui/client/src/shared/generator-helpers.js b/awx/ui/client/src/shared/generator-helpers.js
index 9792f37ecc..f01b7110af 100644
--- a/awx/ui/client/src/shared/generator-helpers.js
+++ b/awx/ui/client/src/shared/generator-helpers.js
@@ -194,6 +194,9 @@ angular.module('GeneratorHelpers', [systemStatus.name])
case 'insights':
icon = "fa-info";
break;
+ case 'network':
+ icon = "fa-sitemap";
+ break;
case 'cancel':
icon = "fa-minus-circle";
break;
diff --git a/awx/ui/client/src/vendor.js b/awx/ui/client/src/vendor.js
index 4b58d5953a..7ad8681b41 100644
--- a/awx/ui/client/src/vendor.js
+++ b/awx/ui/client/src/vendor.js
@@ -61,3 +61,11 @@ require('ng-toast-provider');
require('ng-toast-directives');
require('ng-toast');
require('lr-infinite-scroll');
+
+// Network Visualization
+require('angular-mousewheel');
+require('angular-xeditable');
+require('hamsterjs');
+require('titlecase');
+require('inherits');
+require('mathjs');
diff --git a/awx/ui/package.json b/awx/ui/package.json
index 335d033aa5..1ed19acee4 100644
--- a/awx/ui/package.json
+++ b/awx/ui/package.json
@@ -61,6 +61,7 @@
"html-loader": "^0.5.1",
"html-webpack-harddisk-plugin": "^0.1.0",
"html-webpack-plugin": "^2.30.1",
+ "istanbul-instrumenter-loader": "^3.0.0",
"jasmine-core": "^2.5.2",
"jshint": "^2.9.4",
"jshint-stylish": "^2.2.0",
@@ -83,6 +84,7 @@
"ngtemplate-loader": "^2.0.1",
"nightwatch": "^0.9.19",
"node-object-hash": "^1.3.0",
+ "nunjucks": "^3.1.2",
"phantomjs-prebuilt": "^2.1.12",
"time-grunt": "^1.4.0",
"uglifyjs-webpack-plugin": "^0.4.6",
@@ -124,6 +126,12 @@
"reconnectingwebsocket": "^1.0.0",
"rrule": "git+https://git@github.com/jkbrzt/rrule#4ff63b2f8524fd6d5ba6e80db770953b5cd08a0c",
"select2": "^4.0.2",
- "sprintf-js": "^1.0.3"
+ "sprintf-js": "^1.0.3",
+ "mathjs": "^3.15.0",
+ "hamsterjs": "^1.1.2",
+ "titlecase": "^1.1.2",
+ "inherits": "^1.0.2",
+ "angular-mousewheel": "^1.0.5",
+ "angular-xeditable": "~0.8.0"
}
}
diff --git a/awx/urls.py b/awx/urls.py
index 554eb2c813..57a97fdae5 100644
--- a/awx/urls.py
+++ b/awx/urls.py
@@ -15,6 +15,7 @@ urlpatterns = [
url(r'^api/', include('awx.api.urls', namespace='api')),
url(r'^sso/', include('awx.sso.urls', namespace='sso')),
url(r'^sso/', include('social_django.urls', namespace='social')),
+ url(r'^network_ui/', include('awx.network_ui.urls', namespace='network_uiui', app_name='network_ui')),
url(r'^(?:api/)?400.html$', handle_400),
url(r'^(?:api/)?403.html$', handle_403),
url(r'^(?:api/)?404.html$', handle_404),
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index 4b52a782a6..fca17117f8 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -106,6 +106,7 @@ constantly==15.1.0 # via twisted
cryptography==2.1.4 # via adal, azure-cosmosdb-table, azure-storage-common, pyopenssl, requests, secretstorage
daphne==1.3.0
decorator==4.2.1
+debtcollector==1.15.0 # via oslo.config, oslo.utils, python-designateclient, python-keystoneclient, python-neutronclient
defusedxml==0.4.1 # via python-saml
deprecation==2.0 # via openstacksdk
django-auth-ldap==1.2.8
diff --git a/tools/docker-compose.yml b/tools/docker-compose.yml
index ee6234ec8c..929eaa33c3 100644
--- a/tools/docker-compose.yml
+++ b/tools/docker-compose.yml
@@ -36,6 +36,8 @@ services:
# Postgres Database Container
postgres:
image: postgres:9.6
+ ports:
+ - "5432:5432"
memcached:
image: memcached:alpine
ports:
diff --git a/tools/docker-compose/nginx.vh.default.conf b/tools/docker-compose/nginx.vh.default.conf
index 7d74c2b443..e8c26e732a 100644
--- a/tools/docker-compose/nginx.vh.default.conf
+++ b/tools/docker-compose/nginx.vh.default.conf
@@ -30,7 +30,7 @@ server {
sendfile off;
}
- location /websocket {
+ location ~ ^/(websocket|network_ui) {
# Pass request to the upstream alias
proxy_pass http://daphne;
# Require http version 1.1 to allow for upgrade requests
@@ -90,7 +90,7 @@ server {
sendfile off;
}
- location /websocket {
+ location ~ ^/(websocket|network_ui) {
# Pass request to the upstream alias
proxy_pass http://daphne;
# Require http version 1.1 to allow for upgrade requests
diff --git a/tools/docker-compose/supervisor.conf b/tools/docker-compose/supervisor.conf
index cedb784324..159912af17 100644
--- a/tools/docker-compose/supervisor.conf
+++ b/tools/docker-compose/supervisor.conf
@@ -20,7 +20,7 @@ stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
[program:runworker]
-command = python manage.py runworker --only-channels websocket.*
+command = python manage.py runworker
autostart = true
autorestart = true
redirect_stderr=true
diff --git a/tools/test/test_network_ui.sh b/tools/test/test_network_ui.sh
new file mode 100755
index 0000000000..f67bfa1c9e
--- /dev/null
+++ b/tools/test/test_network_ui.sh
@@ -0,0 +1,20 @@
+#!/bin/bash -x
+TS="0.01"
+./manage.py ui_test --verbose --time-scale 0.1
+./manage.py replay_recording 143 recordings/test_create_two_switches.replay --time-scale ${TS} --delete-topology-at-start
+./manage.py replay_recording 143 recordings/test_edit_labels2.replay --time-scale ${TS} --delete-topology-at-start
+./manage.py replay_recording 143 recordings/test_undo_redo.replay --time-scale ${TS} --delete-topology-at-start
+./manage.py replay_recording 143 recordings/test_undo_redo_control_mouse_wheel.replay --time-scale ${TS} --delete-topology-at-start
+./manage.py replay_recording 143 recordings/test_deploy_destroy.replay --time-scale ${TS} --delete-topology-at-start
+./manage.py replay_recording 143 recordings/test_undo_redo_create_destroy.replay --time-scale ${TS} --delete-topology-at-start
+./manage.py replay_recording 143 recordings/test_multiple_viewers.replay --time-scale ${TS} --delete-topology-at-start
+./manage.py replay_recording 143 recordings/test_view_hotkeys.replay --time-scale ${TS} --delete-topology-at-start
+./manage.py replay_recording 143 recordings/test_recording.replay --time-scale ${TS} --delete-topology-at-start
+./manage.py replay_recording 143 recordings/test_mouse.replay --time-scale ${TS} --delete-topology-at-start
+./manage.py replay_recording 143 recordings/test_rack.replay --time-scale ${TS} --delete-topology-at-start
+./manage.py replay_recording 143 recordings/test_link_unconnected.replay --time-scale ${TS} --delete-topology-at-start
+./manage.py replay_recording 143 recordings/test_touches.replay --time-scale ${TS} --delete-topology-at-start
+./manage.py replay_recording 143 recordings/test_export.replay --delete-topology-at-start --time-scale 0.1
+./manage.py replay_recording 143 recordings/test_mouse_exit_enter.replay --delete-topology-at-start --time-scale ${TS}
+sleep 1
+istanbul report --root coverage --dir out text text-summary html