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. + +![Models](designs/models.png) + +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. + +![Models](models.png) 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. + +![Event Pipeline](designs/pipeline.png) + + +**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** + +![Link FSM](designs/link.png) + +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) + +![Toolbox](designs/toolbox.png) + +* 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. + +![Null FSM](null.png) + +--- + +**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. + +![Button FSM](button.png) + +--- + +**Buttons FSM** +* See: buttons.yml + +The buttons FSM distributes events to the buttons which each have their own FSM. + +![Buttons FSM](buttons.png) + +--- + +**Device Detail FSM** +* See: device_detail.yml + +The device detail FSM describes interactions when zoomed into a device. + +![Device Detail FSM](device_detail.png) + +--- + +**Group FSM** +* See: group.yml + +The group FSM describes how to organize multiple devices together in a group. + +![Group FSM](group.png) + +--- + +**Hot Keys FSM** +* See: hotkeys.yml + +The hot keys FSM handles key events and generates new events like `NewLink` to implement +hot keys. + +![Hot Keys FSM](hotkeys.png) + +--- + +**Link FSM** +* See: link.yml + +The link FSM connects two devices together with a link. + +![Link](link.png) + +--- + +**Mode FSM** +* See: mode.yml + +The mode FSM controls the overall mode of the network UI application. + +![Mode](mode.png) + +--- + +**Move FSM** +* See: move.yml + +The move FSM controls placement of devices as well as editing the device labels. + +![Move](move.png) + +--- + +**Rack FSM** +* See: rack.yml + +The rack FSM controls organizing devices into a special group called a rack. + +![Rack](rack.png) + +--- + +**Site FSM** +* See: site.yml + +The site FSM controls organizing devices into a special group called a site. + +![Site](site.png) + +--- + +**Stream FSM** +* See: stream.yml + +The stream FSM controls how streams are defined between devices. + +![Stream](stream.png) + +--- + +**Time FSM** +* See: time.yml + +The time FSM controls undo/redo functionality of the network UI. + +![Time](time.png) + +--- + +**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. + +![Toolbox](toolbox.png) + +--- + +**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. + +![View](view.png) 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 @@ +
+
+ DETAILS | {{item.name}} +
+
+ +
+ {{item.type}} DETAILS NOT AVAILABLE +
+ +
+
+
+ +
+ +
Please enter a value.
+
+
+
+
+ +
+ +
+
+
+
+ +
+
+
+ +
+
+ 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 @@ +
+
+
+
{{vm.panelTitle}}
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+ KEY +
+
+
d
+
DEBUG MODE
+
+
+
p
+
HIDE CURSOR
+
+
+
b
+
HIDE BUTTONS
+
+
+
i
+
HIDE INTERFACES
+
+
+
0
+
RESET ZOOM
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+ 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 @@ +
+
+
+ +
+
+
{{zoom}}%
+
+
+
+ +
+
+
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