remove the network UI

This commit is contained in:
Ryan Petrello 2018-07-24 17:40:45 -04:00
parent 681d64c96f
commit 6f5259d017
No known key found for this signature in database
GPG Key ID: F2AA5F2122351777
208 changed files with 16 additions and 14091 deletions

View File

@ -374,7 +374,7 @@ awx-link:
sed -i "s/placeholder/$(shell git describe --long | sed 's/\./\\./g')/" /awx_devel/awx.egg-info/PKG-INFO
cp /tmp/awx.egg-link /venv/awx/lib/python2.7/site-packages/awx.egg-link
TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests awx/network_ui/tests/unit
TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests
# Run all API unit tests.
test:
@ -389,7 +389,7 @@ test_unit:
@if [ "$(VENV_BASE)" ]; then \
. $(VENV_BASE)/awx/bin/activate; \
fi; \
py.test awx/main/tests/unit awx/conf/tests/unit awx/sso/tests/unit awx/network_ui/tests/unit
py.test awx/main/tests/unit awx/conf/tests/unit awx/sso/tests/unit
test_ansible:
@if [ "$(VENV_BASE)" ]; then \

View File

@ -1,5 +1,4 @@
from channels.routing import route
from awx.network_ui.routing import channel_routing as network_ui_routing
channel_routing = [
@ -7,6 +6,3 @@ 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

View File

@ -1,132 +0,0 @@
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, 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
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_id` param. 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.

View File

@ -1,2 +0,0 @@
# Copyright (c) 2017 Red Hat, Inc

View File

@ -1,330 +0,0 @@
# Copyright (c) 2017 Red Hat, Inc
import channels
from channels.auth import channel_session_user, channel_session_user_from_http
from awx.network_ui.models import Topology, Device, Link, Interface
from awx.network_ui.models import TopologyInventory
from awx.main.models.inventory import Inventory
import urlparse
from django.db.models import Q
from collections import defaultdict
import logging
import uuid
import six
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
except IndexError:
inventory_id = None
except TypeError:
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.warning("Message has no sender")
return None, None
if isinstance(message_value, dict) and client_id != message_value.get('sender'):
logger.warning("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')
if topology_id is None:
logger.warning("Unsupported message %s: no topology", message)
return
client_id = message.get('client')
if client_id is None:
logger.warning("Unsupported message %s: no client", message)
return
if not message.get('can_edit'):
logger.warning("Client {0} does not have permission to edit topology {1}".format(client_id, topology_id))
return
if 'text' not in message:
logger.warning("Unsupported message %s: no data", message)
return
message_type, message_value = self.parse_message_text(message['text'], client_id)
if message_type is None:
logger.warning("Unsupported message %s: no message type", message)
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 created %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):
logger.info("Device removed %s", device)
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 onDeviceLabelEdit(self, device, topology_id, client_id):
logger.debug("Device label edited %s", device)
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):
logger.debug("Link label edited %s", link)
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):
logger.debug("Link created %s", link)
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:
logger.warning('Device not found')
return
if link['to_device_id'] not in device_map:
logger.warning('Device not found')
return
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):
logger.debug("Link deleted %s", link)
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:
logger.warning('Device not found')
return
if link['to_device_id'] not in device_map:
logger.warning('Device not found')
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_user_from_http
def ws_connect(message):
if not message.user.is_authenticated():
logger.error("Request user is not authenticated to use websocket.")
message.reply_channel.send({"close": True})
return
else:
message.reply_channel.send({"accept": True})
data = urlparse.parse_qs(message.content['query_string'])
inventory_id = parse_inventory_id(data)
try:
inventory = Inventory.objects.get(id=inventory_id)
except Inventory.DoesNotExist:
logger.error("User {} attempted connecting inventory_id {} that does not exist.".format(
message.user.id, inventory_id)
)
message.reply_channel.send({"close": True})
return
if message.user not in inventory.read_role:
logger.warn("User {} attempted connecting to inventory_id {} without permission.".format(
message.user.id, inventory_id
))
message.reply_channel.send({"close": True})
return
message.channel_session['can_edit'] = message.user in inventory.admin_role
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=0.7, 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
channels.Group("topology-%s" % topology_id).add(message.reply_channel)
client_id = six.text_type(uuid.uuid4())
message.channel_session['client_id'] = client_id
channels.Group("client-%s" % client_id).add(message.reply_channel)
message.reply_channel.send({"text": json.dumps(["id", client_id])})
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_user
def ws_message(message):
# Send to all clients editing the topology
if message.channel_session['can_edit']:
channels.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'],
"can_edit": message.channel_session['can_edit']})
@channel_session_user
def ws_disconnect(message):
if 'topology_id' in message.channel_session:
channels.Group("topology-%s" % message.channel_session['topology_id']).discard(message.reply_channel)

View File

@ -1,8 +0,0 @@
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)

View File

@ -1,19 +0,0 @@
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]}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

View File

@ -1,123 +0,0 @@
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
- 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

View File

@ -1,83 +0,0 @@
# -*- 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', '0027_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'),
),
]

View File

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.11 on 2018-05-30 17:18
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('network_ui', '0001_initial'),
]
operations = [
migrations.DeleteModel(
name='Client',
),
]

View File

@ -1,60 +0,0 @@
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 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')

View File

@ -1,9 +0,0 @@
# 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/"),
]

View File

@ -1,9 +0,0 @@
import pytest
from mock import PropertyMock
@pytest.fixture(autouse=True)
def _disable_database_settings(mocker):
m = mocker.patch('awx.conf.settings.SettingsWrapper.all_supported_settings', new_callable=PropertyMock)
m.return_value = []

View File

@ -1,246 +0,0 @@
import mock
import logging
import json
import imp
from mock import patch
patch('channels.auth.channel_session_user', lambda x: x).start()
patch('channels.auth.channel_session_user_from_http', lambda x: x).start()
from awx.main.models import Inventory # noqa
from awx.network_ui.consumers import parse_inventory_id, networking_events_dispatcher, send_snapshot # noqa
from awx.network_ui.models import Topology, Device, Link, Interface, TopologyInventory # noqa
import awx # noqa
import awx.network_ui # noqa
import awx.network_ui.consumers # noqa
imp.reload(awx.network_ui.consumers)
def test_parse_inventory_id():
assert parse_inventory_id({}) is None
assert parse_inventory_id({'inventory_id': ['1']}) == 1
assert parse_inventory_id({'inventory_id': ['0']}) is None
assert parse_inventory_id({'inventory_id': ['X']}) is None
assert parse_inventory_id({'inventory_id': []}) is None
assert parse_inventory_id({'inventory_id': 'x'}) is None
assert parse_inventory_id({'inventory_id': '12345'}) == 1
assert parse_inventory_id({'inventory_id': 1}) is None
def test_network_events_handle_message_incomplete_message1():
logger = logging.getLogger('awx.network_ui.consumers')
with mock.patch.object(logger, 'warning') as log_mock:
networking_events_dispatcher.handle({})
log_mock.assert_called_once_with(
'Unsupported message %s: no topology', {})
def test_network_events_handle_message_incomplete_message2():
logger = logging.getLogger('awx.network_ui.consumers')
with mock.patch.object(logger, 'warning') as log_mock:
networking_events_dispatcher.handle({'topology': [0]})
log_mock.assert_called_once_with(
'Unsupported message %s: no client', {'topology': [0]})
def test_network_events_handle_message_incomplete_message3():
logger = logging.getLogger('awx.network_ui.consumers')
with mock.patch.object(logger, 'warning') as log_mock:
networking_events_dispatcher.handle({'topology': [1]})
log_mock.assert_called_once_with(
'Unsupported message %s: no client', {'topology': [1]})
def test_network_events_handle_message_incomplete_message4():
logger = logging.getLogger('awx.network_ui.consumers')
with mock.patch.object(logger, 'warning') as log_mock:
networking_events_dispatcher.handle({'topology': 1, 'client': 1})
log_mock.assert_called_once_with('Unsupported message %s: no data', {
'client': 1, 'topology': 1})
def test_network_events_handle_message_incomplete_message5():
logger = logging.getLogger('awx.network_ui.consumers')
with mock.patch.object(logger, 'warning') as log_mock:
message = ['DeviceCreate']
networking_events_dispatcher.handle(
{'topology': 1, 'client': 1, 'text': json.dumps(message)})
log_mock.assert_called_once_with('Unsupported message %s: no message type', {
'text': '["DeviceCreate"]', 'client': 1, 'topology': 1})
def test_network_events_handle_message_incomplete_message6():
logger = logging.getLogger('awx.network_ui.consumers')
with mock.patch.object(logger, 'warning') as log_mock:
message = ['DeviceCreate', []]
networking_events_dispatcher.handle(
{'topology': 1, 'client': 1, 'text': json.dumps(message)})
log_mock.assert_has_calls([
mock.call('Message has no sender'),
mock.call('Unsupported message %s: no message type', {'text': '["DeviceCreate", []]', 'client': 1, 'topology': 1})])
def test_network_events_handle_message_incomplete_message7():
logger = logging.getLogger('awx.network_ui.consumers')
with mock.patch.object(logger, 'warning') as log_mock:
message = ['DeviceCreate', {}]
networking_events_dispatcher.handle(
{'topology': 1, 'client': 1, 'text': json.dumps(message)})
log_mock.assert_has_calls([
mock.call('client_id mismatch expected: %s actual %s', 1, None),
mock.call('Unsupported message %s: no message type', {'text': '["DeviceCreate", {}]', 'client': 1, 'topology': 1})])
def test_network_events_handle_message_incomplete_message8():
logger = logging.getLogger('awx.network_ui.consumers')
with mock.patch.object(logger, 'warning') as log_mock:
message = ['Unsupported', {'sender': 1}]
networking_events_dispatcher.handle(
{'topology': 1, 'client': 1, 'text': json.dumps(message)})
log_mock.assert_called_once_with(
'Unsupported message %s: no handler', u'Unsupported')
def test_send_snapshot_empty():
channel = mock.MagicMock()
logger = logging.getLogger('awx.network_ui.consumers')
with mock.patch.object(logger, 'warning') as log_mock,\
mock.patch.object(Device, 'objects'),\
mock.patch.object(Link, 'objects'),\
mock.patch.object(Interface, 'objects'),\
mock.patch.object(Topology, 'objects'):
send_snapshot(channel, 1)
log_mock.assert_not_called()
channel.send.assert_called_once_with(
{'text': '["Snapshot", {"links": [], "devices": [], "sender": 0}]'})
def test_send_snapshot_single():
channel = mock.MagicMock()
logger = logging.getLogger('awx.network_ui.consumers')
with mock.patch.object(logger, 'warning') as log_mock,\
mock.patch.object(Device, 'objects') as device_objects_mock,\
mock.patch.object(Link, 'objects'),\
mock.patch.object(Interface, 'objects') as interface_objects_mock:
interface_objects_mock.filter.return_value.values.return_value = [
dict(cid=1, device_id=1, id=1, name="eth0")]
device_objects_mock.filter.return_value.values.return_value = [
dict(cid=1, id=1, device_type="host", name="host1", x=0, y=0,
interface_id_seq=1, host_id=1)]
send_snapshot(channel, 1)
device_objects_mock.filter.assert_called_once_with(topology_id=1)
device_objects_mock.filter.return_value.values.assert_called_once_with()
interface_objects_mock.filter.assert_called_once_with(
device__topology_id=1)
interface_objects_mock.filter.return_value.values.assert_called_once_with()
log_mock.assert_not_called()
channel.send.assert_called_once_with(
{'text': '''["Snapshot", {"links": [], "devices": [{"interface_id_seq": 1, \
"name": "host1", "interfaces": [{"id": 1, "device_id": 1, "name": "eth0", "interface_id": 1}], \
"device_type": "host", "host_id": 1, "y": 0, "x": 0, "id": 1, "device_id": 1}], "sender": 0}]'''})
def test_ws_disconnect():
message = mock.MagicMock()
message.channel_session = dict(topology_id=1)
message.reply_channel = 'foo'
with mock.patch('channels.Group') as group_mock:
awx.network_ui.consumers.ws_disconnect(message)
group_mock.assert_called_once_with('topology-1')
group_mock.return_value.discard.assert_called_once_with('foo')
def test_ws_disconnect_no_topology():
message = mock.MagicMock()
with mock.patch('channels.Group') as group_mock:
awx.network_ui.consumers.ws_disconnect(message)
group_mock.assert_not_called()
def test_ws_message():
message = mock.MagicMock()
message.channel_session = dict(topology_id=1, client_id=1)
message.__getitem__.return_value = json.dumps([])
print (message['text'])
with mock.patch('channels.Group') as group_mock:
awx.network_ui.consumers.ws_message(message)
group_mock.assert_called_once_with('topology-1')
group_mock.return_value.send.assert_called_once_with({'text': '[]'})
def test_ws_connect_unauthenticated():
message = mock.MagicMock()
message.user.is_authenticated.return_value = False
logger = logging.getLogger('awx.network_ui.consumers')
with mock.patch.object(logger, 'error') as log_mock:
awx.network_ui.consumers.ws_connect(message)
log_mock.assert_called_once_with('Request user is not authenticated to use websocket.')
def test_ws_connect_new_topology():
mock_user = mock.Mock()
message = mock.MagicMock(user=mock_user)
logger = logging.getLogger('awx.network_ui.consumers')
with mock.patch('awx.network_ui.consumers.uuid') as client_mock,\
mock.patch('awx.network_ui.consumers.Topology') as topology_mock,\
mock.patch('channels.Group'),\
mock.patch('awx.network_ui.consumers.send_snapshot') as send_snapshot_mock,\
mock.patch.object(logger, 'warning'),\
mock.patch.object(TopologyInventory, 'objects'),\
mock.patch.object(TopologyInventory, 'save'),\
mock.patch.object(Topology, 'save'),\
mock.patch.object(Topology, 'objects'),\
mock.patch.object(Device, 'objects'),\
mock.patch.object(Link, 'objects'),\
mock.patch.object(Interface, 'objects'),\
mock.patch.object(Inventory, 'objects') as inventory_objects:
client_mock.uuid4 = mock.MagicMock(return_value="777")
topology_mock.return_value = Topology(
name="topology", scale=0.7, panX=0, panY=0, pk=999)
inventory_objects.get.return_value = mock.Mock(admin_role=[mock_user])
awx.network_ui.consumers.ws_connect(message)
message.reply_channel.send.assert_has_calls([
mock.call({'text': '["id", "777"]'}),
mock.call({'text': '["topology_id", 999]'}),
mock.call(
{'text': '["Topology", {"scale": 1.0, "name": "topology", "device_id_seq": 0, "panY": 0, "panX": 0, "topology_id": 999, "link_id_seq": 0}]'}),
])
send_snapshot_mock.assert_called_once_with(message.reply_channel, 999)
def test_ws_connect_existing_topology():
mock_user = mock.Mock()
message = mock.MagicMock(user=mock_user)
logger = logging.getLogger('awx.network_ui.consumers')
with mock.patch('awx.network_ui.consumers.uuid') as client_mock,\
mock.patch('awx.network_ui.consumers.send_snapshot') as send_snapshot_mock,\
mock.patch('channels.Group'),\
mock.patch.object(logger, 'warning'),\
mock.patch.object(TopologyInventory, 'objects') as topology_inventory_objects_mock,\
mock.patch.object(TopologyInventory, 'save'),\
mock.patch.object(Topology, 'save'),\
mock.patch.object(Topology, 'objects') as topology_objects_mock,\
mock.patch.object(Device, 'objects'),\
mock.patch.object(Link, 'objects'),\
mock.patch.object(Interface, 'objects'),\
mock.patch.object(Inventory, 'objects') as inventory_objects:
topology_inventory_objects_mock.filter.return_value.values_list.return_value = [
1]
client_mock.uuid4 = mock.MagicMock(return_value="888")
topology_objects_mock.get.return_value = Topology(pk=1001,
id=1,
name="topo",
panX=0,
panY=0,
scale=1.0,
link_id_seq=1,
device_id_seq=1)
inventory_objects.get.return_value = mock.Mock(admin_role=[mock_user])
awx.network_ui.consumers.ws_connect(message)
message.reply_channel.send.assert_has_calls([
mock.call({'text': '["id", "888"]'}),
mock.call({'text': '["topology_id", 1001]'}),
mock.call(
{'text': '["Topology", {"scale": 1.0, "name": "topo", "device_id_seq": 1, "panY": 0, "panX": 0, "topology_id": 1001, "link_id_seq": 1}]'}),
])
send_snapshot_mock.assert_called_once_with(message.reply_channel, 1001)

View File

@ -1,54 +0,0 @@
import pytest
import inspect
from awx.network_ui.models import Device, Topology, Interface, Link
from awx.main.models import Organization, Inventory
from awx.main.tasks import delete_inventory
from django.db.models import Model
def test_device():
assert str(Device(name="foo")) == "foo"
def test_topology():
assert str(Topology(name="foo")) == "foo"
def test_interface():
assert str(Interface(name="foo")) == "foo"
@pytest.mark.django_db
def test_deletion():
org = Organization.objects.create(name='Default')
inv = Inventory.objects.create(name='inv', organization=org)
host1 = inv.hosts.create(name='foo')
host2 = inv.hosts.create(name='bar')
topology = Topology.objects.create(
name='inv', scale=0.7, panX=0.0, panY=0.0
)
inv.topologyinventory_set.create(topology=topology)
device1 = topology.device_set.create(name='foo', host=host1, x=0.0, y=0.0, cid=1)
interface1 = Interface.objects.create(device=device1, name='foo', cid=2)
device2 = topology.device_set.create(name='bar', host=host2, x=0.0, y=0.0, cid=3)
interface2 = Interface.objects.create(device=device2, name='bar', cid=4)
Link.objects.create(
from_device=device1, to_device=device2,
from_interface=interface1, to_interface=interface2,
cid=10
)
network_ui_models = []
from awx.network_ui import models as network_models
for name, model in vars(network_models).items():
if not inspect.isclass(model) or not issubclass(model, Model):
continue
network_ui_models.append(model)
delete_inventory.run(inv.pk, None)
for cls in network_ui_models:
assert cls.objects.count() == 0, cls

View File

@ -1,451 +0,0 @@
import mock
import json
import logging
from awx.network_ui.consumers import networking_events_dispatcher
from awx.network_ui.models import Topology, Device, Link, Interface
def message(message):
def wrapper(fn):
fn.tests_message = message
return fn
return wrapper
@message('DeviceMove')
def test_network_events_handle_message_DeviceMove():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['DeviceMove', dict(
msg_type='DeviceMove',
sender=1,
id=1,
x=100,
y=100,
previous_x=0,
previous_y=0
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock,\
mock.patch.object(Device, 'objects') as device_objects_mock:
networking_events_dispatcher.handle(message)
device_objects_mock.filter.assert_called_once_with(
cid=1, topology_id=1)
device_objects_mock.filter.return_value.update.assert_called_once_with(
x=100, y=100)
log_mock.assert_not_called()
@message('DeviceCreate')
def test_network_events_handle_message_DeviceCreate():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['DeviceCreate', dict(msg_type='DeviceCreate',
sender=1,
id=1,
x=0,
y=0,
name="test_created",
type='host',
host_id=None)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock,\
mock.patch.object(Topology.objects, 'filter') as topology_objects_mock,\
mock.patch.object(Device.objects, 'get_or_create') as device_objects_mock:
device_mock = mock.MagicMock()
filter_mock = mock.MagicMock()
device_objects_mock.return_value = [device_mock, True]
topology_objects_mock.return_value = filter_mock
networking_events_dispatcher.handle(message)
device_objects_mock.assert_called_once_with(
cid=1,
defaults={'name': u'test_created', 'cid': 1, 'device_type': u'host',
'x': 0, 'y': 0, 'host_id': None},
topology_id=1)
device_mock.save.assert_called_once_with()
topology_objects_mock.assert_called_once_with(
device_id_seq__lt=1, pk=1)
filter_mock.update.assert_called_once_with(device_id_seq=1)
log_mock.assert_not_called()
@message('DeviceLabelEdit')
def test_network_events_handle_message_DeviceLabelEdit():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['DeviceLabelEdit', dict(
msg_type='DeviceLabelEdit',
sender=1,
id=1,
name='test_changed',
previous_name='test_created'
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock,\
mock.patch.object(Device.objects, 'filter') as device_objects_filter_mock:
networking_events_dispatcher.handle(message)
device_objects_filter_mock.assert_called_once_with(
cid=1, topology_id=1)
log_mock.assert_not_called()
@message('DeviceSelected')
def test_network_events_handle_message_DeviceSelected():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['DeviceSelected', dict(
msg_type='DeviceSelected',
sender=1,
id=1
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock:
networking_events_dispatcher.handle(message)
log_mock.assert_not_called()
@message('DeviceUnSelected')
def test_network_events_handle_message_DeviceUnSelected():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['DeviceUnSelected', dict(
msg_type='DeviceUnSelected',
sender=1,
id=1
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock:
networking_events_dispatcher.handle(message)
log_mock.assert_not_called()
@message('DeviceDestroy')
def test_network_events_handle_message_DeviceDestory():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['DeviceDestroy', dict(
msg_type='DeviceDestroy',
sender=1,
id=1,
previous_x=0,
previous_y=0,
previous_name="",
previous_type="host",
previous_host_id="1")]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock,\
mock.patch.object(Device, 'objects') as device_objects_mock:
networking_events_dispatcher.handle(message)
device_objects_mock.filter.assert_called_once_with(
cid=1, topology_id=1)
device_objects_mock.filter.return_value.delete.assert_called_once_with()
log_mock.assert_not_called()
@message('InterfaceCreate')
def test_network_events_handle_message_InterfaceCreate():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['InterfaceCreate', dict(
msg_type='InterfaceCreate',
sender=1,
device_id=1,
id=1,
name='eth0'
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock,\
mock.patch.object(Device, 'objects') as device_objects_mock,\
mock.patch.object(Interface, 'objects') as interface_objects_mock:
device_objects_mock.get.return_value.pk = 99
networking_events_dispatcher.handle(message)
device_objects_mock.get.assert_called_once_with(cid=1, topology_id=1)
device_objects_mock.filter.assert_called_once_with(
cid=1, interface_id_seq__lt=1, topology_id=1)
interface_objects_mock.get_or_create.assert_called_once_with(
cid=1, defaults={'name': u'eth0'}, device_id=99)
log_mock.assert_not_called()
@message('InterfaceLabelEdit')
def test_network_events_handle_message_InterfaceLabelEdit():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['InterfaceLabelEdit', dict(
msg_type='InterfaceLabelEdit',
sender=1,
id=1,
device_id=1,
name='new name',
previous_name='old name'
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock,\
mock.patch.object(Interface, 'objects') as interface_objects_mock:
networking_events_dispatcher.handle(message)
interface_objects_mock.filter.assert_called_once_with(
cid=1, device__cid=1, device__topology_id=1)
interface_objects_mock.filter.return_value.update.assert_called_once_with(
name=u'new name')
log_mock.assert_not_called()
@message('LinkLabelEdit')
def test_network_events_handle_message_LinkLabelEdit():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['LinkLabelEdit', dict(
msg_type='LinkLabelEdit',
sender=1,
id=1,
name='new name',
previous_name='old name'
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock,\
mock.patch.object(Link, 'objects') as link_objects_mock:
networking_events_dispatcher.handle(message)
link_objects_mock.filter.assert_called_once_with(
cid=1, from_device__topology_id=1)
link_objects_mock.filter.return_value.update.assert_called_once_with(
name=u'new name')
log_mock.assert_not_called()
@message('LinkCreate')
def test_network_events_handle_message_LinkCreate():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['LinkCreate', dict(
msg_type='LinkCreate',
id=1,
sender=1,
name="",
from_device_id=1,
to_device_id=2,
from_interface_id=1,
to_interface_id=1
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock,\
mock.patch.object(Device, 'objects') as device_objects_mock,\
mock.patch.object(Link, 'objects') as link_objects_mock,\
mock.patch.object(Interface, 'objects') as interface_objects_mock,\
mock.patch.object(Topology, 'objects') as topology_objects_mock:
values_list_mock = mock.MagicMock()
values_list_mock.values_list.return_value = [(1,1), (2,2)]
interface_objects_mock.get.return_value = mock.MagicMock()
interface_objects_mock.get.return_value.pk = 7
device_objects_mock.filter.return_value = values_list_mock
topology_objects_mock.filter.return_value = mock.MagicMock()
networking_events_dispatcher.handle(message)
device_objects_mock.filter.assert_called_once_with(
cid__in=[1, 2], topology_id=1)
values_list_mock.values_list.assert_called_once_with('cid', 'pk')
link_objects_mock.get_or_create.assert_called_once_with(
cid=1, from_device_id=1, from_interface_id=7, name=u'',
to_device_id=2, to_interface_id=7)
topology_objects_mock.filter.assert_called_once_with(
link_id_seq__lt=1, pk=1)
topology_objects_mock.filter.return_value.update.assert_called_once_with(
link_id_seq=1)
log_mock.assert_not_called()
@message('LinkCreate')
def test_network_events_handle_message_LinkCreate_bad_device1():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['LinkCreate', dict(
msg_type='LinkCreate',
id=1,
sender=1,
name="",
from_device_id=1,
to_device_id=2,
from_interface_id=1,
to_interface_id=1
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock,\
mock.patch.object(Device, 'objects') as device_objects_mock,\
mock.patch.object(Link, 'objects'),\
mock.patch.object(Interface, 'objects') as interface_objects_mock,\
mock.patch.object(Topology, 'objects') as topology_objects_mock:
values_list_mock = mock.MagicMock()
values_list_mock.values_list.return_value = [(9,1), (2,2)]
interface_objects_mock.get.return_value = mock.MagicMock()
interface_objects_mock.get.return_value.pk = 7
device_objects_mock.filter.return_value = values_list_mock
topology_objects_mock.filter.return_value = mock.MagicMock()
networking_events_dispatcher.handle(message)
device_objects_mock.filter.assert_called_once_with(
cid__in=[1, 2], topology_id=1)
values_list_mock.values_list.assert_called_once_with('cid', 'pk')
log_mock.assert_called_once_with('Device not found')
@message('LinkCreate')
def test_network_events_handle_message_LinkCreate_bad_device2():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['LinkCreate', dict(
msg_type='LinkCreate',
id=1,
sender=1,
name="",
from_device_id=1,
to_device_id=2,
from_interface_id=1,
to_interface_id=1
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock,\
mock.patch.object(Device, 'objects') as device_objects_mock,\
mock.patch.object(Link, 'objects'),\
mock.patch.object(Interface, 'objects') as interface_objects_mock,\
mock.patch.object(Topology, 'objects') as topology_objects_mock:
values_list_mock = mock.MagicMock()
values_list_mock.values_list.return_value = [(1,1), (9,2)]
interface_objects_mock.get.return_value = mock.MagicMock()
interface_objects_mock.get.return_value.pk = 7
device_objects_mock.filter.return_value = values_list_mock
topology_objects_mock.filter.return_value = mock.MagicMock()
networking_events_dispatcher.handle(message)
device_objects_mock.filter.assert_called_once_with(
cid__in=[1, 2], topology_id=1)
values_list_mock.values_list.assert_called_once_with('cid', 'pk')
log_mock.assert_called_once_with('Device not found')
@message('LinkDestroy')
def test_network_events_handle_message_LinkDestroy():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['LinkDestroy', dict(
msg_type='LinkDestroy',
id=1,
sender=1,
name="",
from_device_id=1,
to_device_id=2,
from_interface_id=1,
to_interface_id=1
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock,\
mock.patch.object(Device.objects, 'filter') as device_filter_mock,\
mock.patch.object(Link.objects, 'filter') as link_filter_mock,\
mock.patch.object(Interface.objects, 'get') as interface_get_mock:
values_mock = mock.MagicMock()
interface_get_mock.return_value = mock.MagicMock()
interface_get_mock.return_value.pk = 7
device_filter_mock.return_value = values_mock
values_mock.values_list.return_value = [(1,1), (2,2)]
networking_events_dispatcher.handle(message)
device_filter_mock.assert_called_once_with(
cid__in=[1, 2], topology_id=1)
values_mock.values_list.assert_called_once_with('cid', 'pk')
link_filter_mock.assert_called_once_with(
cid=1, from_device_id=1, from_interface_id=7, to_device_id=2, to_interface_id=7)
log_mock.assert_not_called()
@message('LinkDestroy')
def test_network_events_handle_message_LinkDestroy_bad_device_map1():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['LinkDestroy', dict(
msg_type='LinkDestroy',
id=1,
sender=1,
name="",
from_device_id=1,
to_device_id=2,
from_interface_id=1,
to_interface_id=1
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock,\
mock.patch.object(Device.objects, 'filter') as device_filter_mock,\
mock.patch.object(Link.objects, 'filter'),\
mock.patch.object(Interface.objects, 'get') as interface_get_mock:
values_mock = mock.MagicMock()
interface_get_mock.return_value = mock.MagicMock()
interface_get_mock.return_value.pk = 7
device_filter_mock.return_value = values_mock
values_mock.values_list.return_value = [(9,1), (2,2)]
networking_events_dispatcher.handle(message)
log_mock.assert_called_once_with('Device not found')
@message('LinkDestroy')
def test_network_events_handle_message_LinkDestroy_bad_device_map2():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['LinkDestroy', dict(
msg_type='LinkDestroy',
id=1,
sender=1,
name="",
from_device_id=1,
to_device_id=2,
from_interface_id=1,
to_interface_id=1
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock,\
mock.patch.object(Device.objects, 'filter') as device_filter_mock,\
mock.patch.object(Link.objects, 'filter'),\
mock.patch.object(Interface.objects, 'get') as interface_get_mock:
values_mock = mock.MagicMock()
interface_get_mock.return_value = mock.MagicMock()
interface_get_mock.return_value.pk = 7
device_filter_mock.return_value = values_mock
values_mock.values_list.return_value = [(1,1), (9,2)]
networking_events_dispatcher.handle(message)
log_mock.assert_called_once_with('Device not found')
@message('LinkSelected')
def test_network_events_handle_message_LinkSelected():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['LinkSelected', dict(
msg_type='LinkSelected',
sender=1,
id=1
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock:
networking_events_dispatcher.handle(message)
log_mock.assert_not_called()
@message('LinkUnSelected')
def test_network_events_handle_message_LinkUnSelected():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['LinkUnSelected', dict(
msg_type='LinkUnSelected',
sender=1,
id=1
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock:
networking_events_dispatcher.handle(message)
log_mock.assert_not_called()
@message('MultipleMessage')
def test_network_events_handle_message_MultipleMessage_unsupported_message():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['MultipleMessage', dict(
msg_type='MultipleMessage',
sender=1,
messages=[dict(msg_type="Unsupported")]
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock:
networking_events_dispatcher.handle(message)
log_mock.assert_called_once_with(
'Unsupported message %s', u'Unsupported')
@message('MultipleMessage')
def test_network_events_handle_message_MultipleMessage():
logger = logging.getLogger('awx.network_ui.consumers')
message_data = ['MultipleMessage', dict(
msg_type='MultipleMessage',
sender=1,
messages=[dict(msg_type="DeviceSelected")]
)]
message = {'topology': 1, 'client': 1, 'text': json.dumps(message_data)}
with mock.patch.object(logger, 'warning') as log_mock:
networking_events_dispatcher.handle(message)
log_mock.assert_not_called()

View File

@ -1,9 +0,0 @@
import awx.network_ui.routing
def test_routing():
'''
Tests that the number of routes in awx.network_ui.routing is 3.
'''
assert len(awx.network_ui.routing.channel_routing) == 3

View File

@ -1,65 +0,0 @@
import mock
from awx.network_ui.views import topology_data, NetworkAnnotatedInterface, json_topology_data, yaml_topology_data
from awx.network_ui.models import Topology, Device, Link, Interface
def test_topology_data():
with mock.patch.object(Topology, 'objects'),\
mock.patch.object(Device, 'objects') as device_objects_mock,\
mock.patch.object(Link, 'objects') as link_objects_mock,\
mock.patch.object(Interface, 'objects'),\
mock.patch.object(NetworkAnnotatedInterface, 'filter'):
device_objects_mock.filter.return_value.order_by.return_value = [
Device(pk=1), Device(pk=2)]
link_objects_mock.filter.return_value = [Link(from_device=Device(name='from', cid=1),
to_device=Device(
name='to', cid=2),
from_interface=Interface(
name="eth0", cid=1),
to_interface=Interface(
name="eth0", cid=1),
name="",
pk=1
)]
data = topology_data(1)
assert len(data['devices']) == 2
assert len(data['links']) == 1
def test_json_topology_data():
request = mock.MagicMock()
request.GET = dict(topology_id=1)
with mock.patch('awx.network_ui.views.topology_data') as topology_data_mock:
topology_data_mock.return_value = dict()
json_topology_data(request)
topology_data_mock.assert_called_once_with(1)
def test_yaml_topology_data():
request = mock.MagicMock()
request.GET = dict(topology_id=1)
with mock.patch('awx.network_ui.views.topology_data') as topology_data_mock:
topology_data_mock.return_value = dict()
yaml_topology_data(request)
topology_data_mock.assert_called_once_with(1)
def test_json_topology_data_no_topology_id():
request = mock.MagicMock()
request.GET = dict()
with mock.patch('awx.network_ui.views.topology_data') as topology_data_mock:
topology_data_mock.return_value = dict()
json_topology_data(request)
topology_data_mock.assert_not_called()
def test_yaml_topology_data_no_topology_id():
request = mock.MagicMock()
request.GET = dict()
with mock.patch('awx.network_ui.views.topology_data') as topology_data_mock:
topology_data_mock.return_value = dict()
yaml_topology_data(request)
topology_data_mock.assert_not_called()

View File

@ -1,10 +0,0 @@
# 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'),
]

View File

@ -1,6 +0,0 @@
# 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()}

View File

@ -1,89 +0,0 @@
# Copyright (c) 2017 Red Hat, Inc
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
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
class TopologyForm(forms.Form):
topology_id = forms.IntegerField()
def json_topology_data(request):
form = TopologyForm(request.GET)
if form.is_valid():
response = JsonResponse(topology_data(form.cleaned_data['topology_id']),
content_type='application/force-download')
response['Content-Disposition'] = 'attachment; filename="{}"'.format('topology.json')
return response
else:
return HttpResponseBadRequest(form.errors)
def yaml_topology_data(request):
form = TopologyForm(request.GET)
if form.is_valid():
response = HttpResponse(yaml.safe_dump(topology_data(form.cleaned_data['topology_id']),
default_flow_style=False),
content_type='application/force-download')
response['Content-Disposition'] = 'attachment; filename="{}"'.format('topology.yaml')
return response
else:
return HttpResponseBadRequest(form.errors)

View File

@ -289,8 +289,7 @@ INSTALLED_APPS = (
'awx.api',
'awx.ui',
'awx.sso',
'solo',
'awx.network_ui'
'solo'
)
INTERNAL_IPS = ('127.0.0.1',)

View File

@ -22,7 +22,6 @@ 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');
@ -208,7 +207,6 @@ 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',

View File

@ -77,12 +77,6 @@ const watch = {
target: TARGET,
secure: false,
ws: true
},
{
context: '/network_ui',
target: TARGET,
secure: false,
ws: true
}]
}
};

View File

@ -19,7 +19,6 @@
</head>
<body data-user-agent="{{userAgent}}">
<div ui-view="networking" ng-class="{'NetworkingUIView' : vm.networkUIisOpen}"></div>
<at-layout>
<bread-crumb></bread-crumb>
<toast></toast>

View File

@ -1,7 +1,7 @@
<div class="at-Layout-side"
ng-class="{'at-Layout-side--expanded': vm.isExpanded && layoutVm.isLoggedIn}" ng-show="layoutVm.isLoggedIn && !layoutVm.licenseIsMissing && layoutVm.currentState !== 'inventories.edit.networking'">
ng-class="{'at-Layout-side--expanded': vm.isExpanded && layoutVm.isLoggedIn}" ng-show="layoutVm.isLoggedIn && !layoutVm.licenseIsMissing">
<div class="at-Layout-sideNavItem at-Layout-sideNavToggle" ng-click="vm.toggleExpansion()"
ng-show="layoutVm.isLoggedIn && !layoutVm.licenseIsMissing && layoutVm.currentState !== 'inventories.edit.networking'">
ng-show="layoutVm.isLoggedIn && !layoutVm.licenseIsMissing">
<i class="fa fa-bars"></i>
</div>
<ng-transclude></ng-transclude>

View File

@ -166,9 +166,3 @@
* the transition.
*/
@import '_resets';
/**
* Network Visualization Style
*
*/
@import '../../src/network-ui/style.less';

View File

@ -42,8 +42,6 @@ import atLibComponents from '~components';
import atLibModels from '~models';
import atLibServices from '~services';
import networkUI from '~network-ui/network.ui.app';
start.bootstrap(() => {
angular.bootstrap(document.body, ['awApp']);
});
@ -89,7 +87,6 @@ angular
users.name,
projects.name,
scheduler.name,
networkUI.name,
'Utilities',
'templates',

View File

@ -5,7 +5,7 @@
*************************************************/
export default ['i18n', 'awxNetStrings' , function(i18n, awxNetStrings) {
export default ['i18n', function(i18n) {
return {
name: 'inventories',
@ -117,13 +117,6 @@ export default ['i18n', 'awxNetStrings' , function(i18n, awxNetStrings) {
dataPlacement: 'top',
ngShow: '!inventory.summary_fields.user_capabilities.edit'
},
network: {
label: awxNetStrings.get('feature.ACTION_BUTTON'),
ngClick: 'goToGraph(inventory)',
awToolTip: awxNetStrings.get('feature.ACTION_BUTTON'),
dataPlacement: 'top',
ngShow: '!inventory.pending_deletion'
},
"delete": {
label: i18n._('Delete'),
ngClick: "deleteInventory(inventory.id, inventory.name)",

View File

@ -107,15 +107,6 @@ function InventoriesList($scope,
}
};
$scope.goToGraph = function(inventory){
if(inventory.kind && inventory.kind === 'smart') {
$state.go('inventories.editSmartInventory.networking', {smartinventory_id: inventory.id, inventory_name: inventory.name});
}
else {
$state.go('inventories.edit.networking', {inventory_id: inventory.id, inventory_name: inventory.name});
}
};
$scope.editInventory = function (inventory, reload) {
const goOptions = reload ? { reload: true } : null;
if(inventory.kind && inventory.kind === 'smart') {

View File

@ -46,7 +46,6 @@ import groupNestedGroupsAssociateRoute from './related/groups/related/nested-gro
import nestedHostsAssociateRoute from './related/groups/related/nested-hosts/group-nested-hosts-associate.route';
import nestedHostsAddRoute from './related/groups/related/nested-hosts/group-nested-hosts-add.route';
import hostCompletedJobsRoute from '~features/jobs/routes/hostCompletedJobs.route.js';
import networkUIRoute from '../../network-ui/network.ui.route.js';
export default
angular.module('inventory', [
@ -295,9 +294,6 @@ angular.module('inventory', [
let relatedHostCompletedJobs = _.cloneDeep(hostCompletedJobsRoute);
relatedHostCompletedJobs.name = 'inventories.edit.hosts.edit.completed_jobs';
let smartInvNetworkUI = _.cloneDeep(networkUIRoute);
smartInvNetworkUI.name = 'inventories.editSmartInventory.networking';
return Promise.all([
standardInventoryAdd,
standardInventoryEdit,
@ -346,9 +342,7 @@ angular.module('inventory', [
stateExtender.buildDefinition(nestedHostsAssociateRoute),
stateExtender.buildDefinition(nestedGroupsAdd),
stateExtender.buildDefinition(nestedHostsAddRoute),
stateExtender.buildDefinition(relatedHostCompletedJobs),
stateExtender.buildDefinition(networkUIRoute),
stateExtender.buildDefinition(smartInvNetworkUI)
stateExtender.buildDefinition(relatedHostCompletedJobs)
])
};
});

View File

@ -95,10 +95,6 @@ function SmartInventoryEdit($scope, $location,
});
};
$scope.goToGraph = function(){
$state.go('inventories.editSmartInventory.networking', {smartinventory_id: $scope.inventory_obj.id, inventory_name: $scope.inventory_obj.name});
};
$scope.formCancel = function() {
$state.go('inventories');
};

View File

@ -4,7 +4,7 @@
* All Rights Reserved
*************************************************/
export default ['i18n', 'awxNetStrings', function(i18n, awxNetStrings) {
export default ['i18n', function(i18n) {
return {
addTitle: i18n._('NEW SMART INVENTORY'),
@ -156,14 +156,6 @@ export default ['i18n', 'awxNetStrings', function(i18n, awxNetStrings) {
skipGenerator: true,
ngClick: "$state.go('inventories.editSmartInventory.completed_jobs')"
}
},
relatedButtons: {
network: {
ngClick: 'goToGraph()',
label: awxNetStrings.get('feature.ACTION_BUTTON'),
class: 'Form-primaryButton',
ngShow: "$state.is('inventories.editSmartInventory')"
}
}
};

View File

@ -99,10 +99,6 @@ function InventoriesEdit($scope, $location,
});
};
$scope.goToGraph = function(){
$state.go('inventories.edit.networking', {inventory_id: $scope.inventory_obj.id, inventory_name: $scope.inventory_obj.name});
};
$scope.formCancel = function() {
$state.go('inventories');
};

View File

@ -10,8 +10,8 @@
* @description This form is for adding/editing an inventory
*/
export default ['i18n', 'awxNetStrings',
function(i18n, awxNetStrings) {
export default ['i18n',
function(i18n) {
return {
addTitle: i18n._('NEW INVENTORY'),
@ -174,12 +174,6 @@ function(i18n, awxNetStrings) {
}
},
relatedButtons: {
network: {
ngClick: 'goToGraph()',
ngShow: "$state.is('inventories.edit')",
label: awxNetStrings.get('feature.ACTION_BUTTON'),
class: 'Form-primaryButton'
},
remediate_inventory: {
ngClick: 'remediateInventory(id, insights_credential)',
ngShow: "is_insights && mode !== 'add' && canRemediate && ($state.is('inventories.edit') || $state.is('inventories.edit.hosts'))",

View File

@ -1 +0,0 @@
/extracted

View File

@ -1,951 +0,0 @@
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: <https://en.wikipedia.org/wiki/Analytic_geometry>
**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: <https://docs.angularjs.org/guide/directive>
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: <https://docs.angularjs.org/guide/templates>
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)
```
<g ng-repeat="link in links">
<g awx-net-link></g>
</g>
```
* 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)
```
<g ng-repeat="item in [toolbox.selected_item]">
```
**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: <https://developer.mozilla.org/en-US/docs/Web/SVG>
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: <https://developer.mozilla.org/en-US/docs/Web/SVG/Element>
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: <https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform>
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: <https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths>
**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: <http://lesscss.org/>
* See: <http://getbem.com/introduction/>
**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)
```
<svg id="frame" class="NetworkUI"
ng-attr-height="{{graph.height}}"
ng-attr-width="{{graph.width}}"
ng-mousedown="onMouseDown($event)"
ng-mouseup="onMouseUp($event)"
ng-mouseenter="onMouseEnter($event)"
ng-mouseleave="onMouseLeave($event)"
ng-mousemove="onMouseMove($event)"
ng-mouseover="onMouseOver($event)"
ng-touchstart="onTouchStart($event)"
ng-touchmove="onTouchMove($event)"
ng-touchend="onTouchEnd($event)"
ng-tap="onTap($event)"
msd-wheel="onMouseWheel($event, $delta, $deltaX, $deltaY)">
```
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: <https://en.wikipedia.org/wiki/Finite-state_machine>
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: <https://en.wikipedia.org/wiki/Flyweight_pattern>
* See: <https://en.wikipedia.org/wiki/Singleton_pattern>
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)
```
<!-- Copyright (c) 2017 Red Hat, Inc. -->
<g ng-if="toolbox.enabled">
<rect class="NetworkUI__toolbox"
ng-attr-x="{{toolbox.x}}"
ng-attr-y="{{toolbox.y}}"
ng-attr-width="{{toolbox.width}}"
ng-attr-height="{{toolbox.height}}"
rx=5></rect>
...
</g> <!-- ng-if toolbox.enabled -->
```
* 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)
```
<g awx-net-inventory-toolbox></g>
```
* 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

View File

@ -1,19 +0,0 @@
.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

View File

@ -1,81 +0,0 @@
/* 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);
};

View File

@ -1,23 +0,0 @@
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;

View File

@ -1,107 +0,0 @@
/* 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;
};

View File

@ -1,96 +0,0 @@
/* 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'];

View File

@ -1,15 +0,0 @@
/* 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;

View File

@ -1,14 +0,0 @@
/* 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;

View File

@ -1,23 +0,0 @@
<!-- Copyright (c) 2017 Red Hat, Inc. -->
<filter id="shadowFilter" x="0" y="0" width="102%" height="105%">
<feOffset result="offOut" in="SourceGraphic" dx="20" dy="20" />
<feColorMatrix result = "matrixOut" in = "offOut" type = "matrix" values = "0.2 0 0 0 0 0 0.2 0 0 0 0 0 0.2 0 0 0 0 0 1 0"/>
<feGaussianBlur result="blurOut" in="matrixOut" stdDeviation="10" />
<feBlend in="SourceGraphic" in2="blurOut" mode="normal" />
</filter>
<rect ng-attr-class="{{contextMenu.is_pressed ? 'NetworkUI__contextMenu--button-pressed' : contextMenu.mouse_over ? 'NetworkUI__contextMenu--button-hover' : 'NetworkUI__contextMenu'}}"
x=0
y=0
ng-attr-width={{contextMenu.width}}
ng-attr-height={{contextMenu.height}}
rx=5 ry=5
filter="url(#shadowFilter)">
</rect>
<g>
<g> <!-- context menu buttons -->
<g ng-repeat="button in contextMenu.buttons">
<g awx-net--context-menu-button context-menu-button="button" context-menu="contextMenu"></g>
</g>
</g> <!-- end context menu buttons -->

View File

@ -1,19 +0,0 @@
<!-- Copyright (c) 2017 Red Hat, Inc. -->
<rect ng-attr-class="{{contextMenuButton.is_pressed ? 'NetworkUI__contextMenuButton-pressed' : contextMenuButton.mouse_over ? 'NetworkUI__contextMenuButton-hover' : 'NetworkUI__contextMenuButton'}}"
x= 1
ng-attr-y="{{(contextMenuButton.height * $parent.$index) + 5}}"
ng-attr-width={{contextMenuButton.width-2}}
ng-attr-height={{contextMenuButton.height}}>
</rect>
<text ng-show="contextMenuButton.name !=='Remove'" ng-attr-class="{{contextMenuButton.is_pressed ? 'NetworkUI__contextMenuButtonText-pressed' : contextMenuButton.mouse_over ? 'NetworkUI__contextMenuButtonText-hover' : 'NetworkUI__contextMenuButtonText'}}"
x=15
ng-attr-y="{{(contextMenuButton.height * $parent.$index) + 18}}"
dy=".3em"
text-anchor="left">{{contextMenuButton.name}}
</text>
<text ng-show="contextMenuButton.type ==='remove'" ng-attr-class="{{contextMenuButton.is_pressed ? 'NetworkUI__contextMenuRemoveButtonText-pressed' : contextMenuButton.mouse_over ? 'NetworkUI__contextMenuRemoveButtonText-hover' : 'NetworkUI__contextMenuRemoveButtonText'}}"
x=15
ng-attr-y="{{(contextMenuButton.height * $parent.$index) + 18}}"
dy=".3em"
text-anchor="left">{{contextMenuButton.name}}
</text>

View File

@ -1,8 +0,0 @@
/* Copyright (c) 2017 Red Hat, Inc. */
const templateUrl = require('~network-ui/cursor.partial.svg');
function cursor () {
return { restrict: 'A', templateUrl};
}
exports.cursor = cursor;

View File

@ -1,5 +0,0 @@
<!-- Copyright (c) 2017 Red Hat, Inc. -->
<g ng-attr-transform="translate({{cursor.x}},{{cursor.y}})" ng-attr-class="{{cursor.hidden && 'NetworkUI--hidden' || ''}}" >
<line x1="-15" y1="0" x2="15" y2="0" class="NetworkUI__cursor"/>
<line x1="0" y1="-15" x2="0" y2="15" class="NetworkUI__cursor"/>
</g>

View File

@ -1,20 +0,0 @@
/* 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;

View File

@ -1,60 +0,0 @@
<!-- Copyright (c) 2017 Red Hat, Inc. -->
<g ng-attr-class="{{debug.hidden && 'NetworkUI--hidden' || ''}}">
<g transform="translate(0, 100)">
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">view_port.x: {{view_port.x}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">view_port.y: {{view_port.y}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">view_port.width: {{view_port.width}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">view_port.height: {{view_port.height}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">width: {{graph.width}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">height: {{graph.height}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">rc: {{graph.right_column}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Mouse down: {{onMouseDownResult}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Mouse up: {{onMouseUpResult}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Mouse move: {{onMouseMoveResult}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Mouse over: {{onMouseOverResult}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Mouse enter: {{onMouseEnterResult}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Mouse leave: {{onMouseLeaveResult}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Current scale: {{current_scale.toFixed(4)}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Pan X: {{panX.toFixed(2)}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Pan Y: {{panY.toFixed(2)}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">View State: {{view_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Mouse X: {{mouseX.toFixed(2)}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Mouse Y: {{mouseY.toFixed(2)}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Scaled X: {{scaledX.toFixed(2)}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Scaled Y: {{scaledY.toFixed(2)}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Key: {{last_key}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Key Code: {{last_key_code}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Move State: {{move_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Move Readonly State: {{move_readonly_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Selected devices: {{selected_devices.length}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Selected links: {{selected_links.length}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Link State: {{link_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Buttons State: {{buttons_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Time State: {{time_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Time Pointer: {{time_pointer}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Group State: {{group_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Hotkeys State: {{hotkeys_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Mode State: {{mode_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Device Detail State: {{device_detail_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Site State: {{site_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Rack State: {{rack_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Stream State: {{stream_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">App Toolbox State: {{app_toolbox_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Inventory Toolbox State: {{inventory_toolbox_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Rack Toolbox State: {{rack_toolbox_controller.state.name}}</text>
<text ng-attr-x="{{graph.right_column}}" class="NetworkUI__debug-text">Site Toolbox State: {{site_toolbox_controller.state.name}}</text>
</g>
<rect x=10 y=10 ng-attr-width="{{graph.width - 20}}" ng-attr-height="{{graph.height - 20}}" class="NetworkUI--debug"></rect>
<line x1="-10000"
ng-attr-y1="{{graph.height/2}}"
x2="10000"
ng-attr-y2="{{graph.height/2}}"
ng-attr-class="{{debug.hidden && 'NetworkUI--hidden' || 'NetworkUI--debug'}}" />
<line ng-attr-x1="{{graph.width/2}}"
y1="-10000"
ng-attr-x2="{{graph.width/2}}"
y2="10000"
ng-attr-class="{{debug.hidden && 'NetworkUI--hidden' || 'NetworkUI--debug'}}" />
</g>

View File

@ -1,8 +0,0 @@
/* Copyright (c) 2017 Red Hat, Inc. */
const templateUrl = require('~network-ui/default.partial.svg');
function defaultd () {
return { restrict: 'A', templateUrl};
}
exports.defaultd = defaultd;

View File

@ -1,58 +0,0 @@
<!-- Copyright (c) 2017 Red Hat, Inc. -->
<g transform="scale(0.75)">
<g ng-if="item.moving">
<line ng-attr-x1="{{-50 - 100}}"
ng-attr-y1="0"
ng-attr-x2="{{50 + 100}}"
ng-attr-y2="0"
class="NetworkUI--construction"></line>
<line ng-attr-x1="0"
ng-attr-y1="{{-50 - 100}}"
ng-attr-x2="0"
ng-attr-y2="{{50 + 100}}"
class="NetworkUI--construction"></line>
</g>
<g ng-if="!debug.hidden">
<line ng-attr-x1="{{-50 - 10}}"
ng-attr-y1="0"
ng-attr-x2="{{50 + 10}}"
ng-attr-y2="0"
class="NetworkUI--debug"></rect>
<line ng-attr-x1="0"
ng-attr-y1="{{-50 - 10}}"
ng-attr-x2="0"
ng-attr-y2="{{50 + 10}}"
class="NetworkUI--debug"></rect>
<rect ng-attr-x="{{-50}}"
ng-attr-y="{{-50}}"
ng-attr-width="{{50 * 2}}"
ng-attr-height="{{50 * 2}}"
class="NetworkUI--debug"></rect>
</g>
<g class="NetworkUI__device">
<circle
cx="0"
cy="0"
ng-attr-r="{{50 + 2}}"
ng-attr-class="{{item.selected || item.remote_selected ? item.selected && item.remote_selected ? 'NetworkUI__device--selected-conflict' : item.selected ? 'NetworkUI__device--selected' : 'NetworkUI__device--remote-selected' : 'NetworkUI--hidden'}}">
</circle>
<circle
cx="0"
cy="0"
ng-attr-r="{{50}}">
</circle>
</g>
<g ng-show="item.icon || current_scale > 0.5">
<text ng-attr-class="{{item.selected && ! item.edit_label ? 'NetworkUI__device-text--selected' : 'NetworkUI--hidden'}}"
filter="url(#background)"
text-anchor="middle"
x="0"
y="73"> <tspan x="0" dy="20" ng-repeat="chunk in item.name|chunk:25">{{chunk}}</tspan>
</text>
<text class="NetworkUI__device-text" filter="url(#background)" text-anchor="middle" x="0" y="73">
<tspan x="0" dy="20" ng-repeat="chunk in item.name|chunk:25">{{chunk}}</tspan>
</text>
</g>
</g>

View File

@ -1,161 +0,0 @@
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)

View File

@ -1,29 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

@ -1,43 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@ -1,28 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

View File

@ -1,26 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

@ -1,19 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

View File

@ -1,119 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

@ -1,25 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

View File

@ -1,38 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@ -1,42 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

View File

@ -1,96 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

View File

@ -1,65 +0,0 @@
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

View File

@ -1,107 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@ -1,15 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

View File

@ -1,233 +0,0 @@
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: []

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

View File

@ -1,83 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

View File

@ -1,83 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

View File

@ -1,42 +0,0 @@
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

View File

@ -1,50 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

View File

@ -1,37 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

View File

@ -1,98 +0,0 @@
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

View File

@ -1,45 +0,0 @@
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

View File

@ -1,62 +0,0 @@
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'];

View File

@ -1,47 +0,0 @@
#!/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));

View File

@ -1,57 +0,0 @@
#!/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));

Some files were not shown because too many files have changed in this diff Show More