remove the network UI
4
Makefile
@ -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 \
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
@ -1,2 +0,0 @@
|
||||
# Copyright (c) 2017 Red Hat, Inc
|
||||
|
||||
@ -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)
|
||||
@ -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.
|
||||
|
||||

|
||||
@ -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]}
|
||||
|
Before Width: | Height: | Size: 207 KiB |
@ -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
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -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')
|
||||
@ -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/"),
|
||||
]
|
||||
@ -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 = []
|
||||
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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()
|
||||
@ -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
|
||||
@ -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()
|
||||
@ -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'),
|
||||
]
|
||||
@ -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()}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -289,8 +289,7 @@ INSTALLED_APPS = (
|
||||
'awx.api',
|
||||
'awx.ui',
|
||||
'awx.sso',
|
||||
'solo',
|
||||
'awx.network_ui'
|
||||
'solo'
|
||||
)
|
||||
|
||||
INTERNAL_IPS = ('127.0.0.1',)
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -77,12 +77,6 @@ const watch = {
|
||||
target: TARGET,
|
||||
secure: false,
|
||||
ws: true
|
||||
},
|
||||
{
|
||||
context: '/network_ui',
|
||||
target: TARGET,
|
||||
secure: false,
|
||||
ws: true
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -166,9 +166,3 @@
|
||||
* the transition.
|
||||
*/
|
||||
@import '_resets';
|
||||
|
||||
/**
|
||||
* Network Visualization Style
|
||||
*
|
||||
*/
|
||||
@import '../../src/network-ui/style.less';
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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)",
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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)
|
||||
])
|
||||
};
|
||||
});
|
||||
|
||||
@ -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');
|
||||
};
|
||||
|
||||
@ -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')"
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@ -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');
|
||||
};
|
||||
|
||||
@ -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'))",
|
||||
|
||||
1
awx/ui/client/src/network-ui/.gitignore
vendored
@ -1 +0,0 @@
|
||||
/extracted
|
||||
@ -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.
|
||||
|
||||

|
||||
|
||||
|
||||
**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**
|
||||
|
||||

|
||||
|
||||
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)
|
||||
|
||||

|
||||
|
||||
* 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
|
||||
@ -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
|
||||
@ -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);
|
||||
};
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
};
|
||||
@ -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'];
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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 -->
|
||||
@ -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>
|
||||
@ -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;
|
||||
@ -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>
|
||||
@ -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;
|
||||
@ -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>
|
||||
@ -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;
|
||||
@ -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>
|
||||
@ -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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Button FSM**
|
||||
* See: button.yml
|
||||
|
||||
The button FSM describes how a button works. The key insight here is that a button is not
|
||||
clicked if the mouse is not over the button on both the `MouseDown` and `MouseUp` events. Moving
|
||||
the mouse off the button before `MouseUp` is not a click.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Buttons FSM**
|
||||
* See: buttons.yml
|
||||
|
||||
The buttons FSM distributes events to the buttons which each have their own FSM.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Device Detail FSM**
|
||||
* See: device_detail.yml
|
||||
|
||||
The device detail FSM describes interactions when zoomed into a device.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Group FSM**
|
||||
* See: group.yml
|
||||
|
||||
The group FSM describes how to organize multiple devices together in a group.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Hot Keys FSM**
|
||||
* See: hotkeys.yml
|
||||
|
||||
The hot keys FSM handles key events and generates new events like `NewLink` to implement
|
||||
hot keys.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Link FSM**
|
||||
* See: link.yml
|
||||
|
||||
The link FSM connects two devices together with a link.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Mode FSM**
|
||||
* See: mode.yml
|
||||
|
||||
The mode FSM controls the overall mode of the network UI application.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Move FSM**
|
||||
* See: move.yml
|
||||
|
||||
The move FSM controls placement of devices as well as editing the device labels.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Rack FSM**
|
||||
* See: rack.yml
|
||||
|
||||
The rack FSM controls organizing devices into a special group called a rack.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Site FSM**
|
||||
* See: site.yml
|
||||
|
||||
The site FSM controls organizing devices into a special group called a site.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Stream FSM**
|
||||
* See: stream.yml
|
||||
|
||||
The stream FSM controls how streams are defined between devices.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Time FSM**
|
||||
* See: time.yml
|
||||
|
||||
The time FSM controls undo/redo functionality of the network UI.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**Toolbox FSM**
|
||||
* See: toolbox.yml
|
||||
|
||||
The toolbox FSM controls the drag-and-drop toolboxes and allow placement of new devices, applications,
|
||||
racks, and sites onto the canvas.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
**View FSM**
|
||||
* See: view.yml
|
||||
|
||||
The view FSM controls the panning and scaling of the the virtual canvas through clicking-and-dragging
|
||||
of the background and scrolling the mousewheel.
|
||||
|
||||

|
||||
@ -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
|
||||
|
Before Width: | Height: | Size: 55 KiB |
@ -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
|
||||
|
Before Width: | Height: | Size: 34 KiB |
@ -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
|
||||
|
Before Width: | Height: | Size: 63 KiB |
@ -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
|
||||
|
Before Width: | Height: | Size: 41 KiB |
@ -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
|
||||
|
Before Width: | Height: | Size: 113 KiB |
@ -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
|
||||
|
Before Width: | Height: | Size: 46 KiB |
@ -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
|
||||
|
Before Width: | Height: | Size: 161 KiB |
@ -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
|
||||
|
Before Width: | Height: | Size: 42 KiB |
@ -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
|
||||
|
Before Width: | Height: | Size: 53 KiB |
@ -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
|
||||
|
Before Width: | Height: | Size: 329 KiB |
|
Before Width: | Height: | Size: 148 KiB |
@ -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
|
||||
@ -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
|
||||
|
Before Width: | Height: | Size: 32 KiB |
@ -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
|
||||
|
Before Width: | Height: | Size: 141 KiB |
@ -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: []
|
||||
|
Before Width: | Height: | Size: 102 KiB |
@ -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
|
||||
|
Before Width: | Height: | Size: 104 KiB |
@ -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
|
||||
|
Before Width: | Height: | Size: 75 KiB |
@ -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
|
||||
@ -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
|
||||
|
Before Width: | Height: | Size: 68 KiB |
@ -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
|
||||
|
Before Width: | Height: | Size: 58 KiB |
@ -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
|
||||
|
Before Width: | Height: | Size: 69 KiB |
@ -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
|
||||
@ -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'];
|
||||
@ -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));
|
||||
@ -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));
|
||||