diff --git a/awx/network_ui/static/network_ui/.gitignore b/awx/network_ui/static/network_ui/.gitignore index 4b8b5ad138..a93ebcc8ab 100644 --- a/awx/network_ui/static/network_ui/.gitignore +++ b/awx/network_ui/static/network_ui/.gitignore @@ -5,3 +5,4 @@ /js /src-instrumented /index-instrumented.html +/extracted diff --git a/awx/network_ui/static/network_ui/CONTRIBUTING.md b/awx/network_ui/static/network_ui/CONTRIBUTING.md index 2b835bdae2..d466999583 100644 --- a/awx/network_ui/static/network_ui/CONTRIBUTING.md +++ b/awx/network_ui/static/network_ui/CONTRIBUTING.md @@ -1,10 +1,975 @@ +Building +======== + To build the UI: make - -To push the UI to tower code base: +To push the UI to AWX code base: make deploy + + + +Getting Started With Development +================================ + + +**Introduction** + +The Networking UI component of AWX works differently from the rest of the AWX +web UI to support high-scale interactive graphical design of networking +topologies. + +The Networking UI is a virtual graphical canvas where graphical elements are +drawn upon. This canvas supports panning (scrolling horizontally and +vertically) and scaling (zooming in and out), dynamic changing of modes, and +other features that would be very difficult or impossible to implement with +standard HTML events and rendering. + +This interface is more like computer graphics than it is building a styled text +document with interactive components. A good grasp of Cartesian coordinates, +trigonometry, and analytic geometry are useful when working with this code. + +* See: + +**Design choices** + +Certain design choices were made to make the UI performant and scale to a large +number of nodes in a diagram. These include the use of simple ES5 functions for +better performance over more advanced functions. For instance C-style for-loops +were many times faster than implementations of `forEach` or iterators which make +function calls during each iteration. This basic ES5 style should be followed +throughout the implementation of the Network UI. + +**AngularJS** + +The Networking UI component uses AngularJS 1.6.x for part of the rendering pipeline +but it is not a normal AngularJS web application. AngularJS makes use of +data-binding and watchers which I found do not scale to the number of elements +we are trying to support in the Networking UI. The Networking UI only uses +AngularJS for SVG rendering (using AngularJS templates) which does scale +sufficiently. + + +**AngularJS Controllers** + +Instead of creating many AngularJS controllers and directives the networking UI +uses one big controller to hold the state of the entire UI. Normally this is +an anti-pattern in AngularJS. Here is was necessary to scale to a large number +of on-screen elements. + +**AngularJS Directives** + +* See: + +AngularJS directives are used in the networking UI application using the element +matching style and the `templateUrl` option to include a template. A majority of +the directives are defined in `src/network.ui.app.js`. + +* See: [src/network.ui.app.js](src/network.ui.app.js) +``` + .directive('awxNetDeviceDetail', deviceDetail.deviceDetail) +``` + +* See: [src/device.detail.directive.js](src/device.detail.directive.js) +``` +function deviceDetail () { + return { restrict: 'A', templateUrl: '/static/network_ui/widgets/device_detail.html' }; +} +``` + +**AngularJS Templates** + +* See: + +Normal AngularJS templates are used with the networking UI controller. +The templates can be found in `/widgets`. Child +scopes are created for sub-templates using the `ng-repeat` directive. + +In this example the `awx-net-link` directive expects a Link model to be +passed to it. The Link model is defined in the `src/models.js` file. + +* See: [src/link.directive.js](src/link.directive.js) +* See: [widgets/link.html](widgets/link.html) + +* See: [widgets/network_ui.html](widgets/network_ui.html) +``` + + + +``` + +* See: [src/models.js](src/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: [widgets/inventory_toolbox.html](widgets/inventory_toolbox.html) +``` + +``` + + +**DOM (Document Object Model)** + +No state is stored in or attached to the DOM. All state is stored in +javascript objects attached to the network ui controller. + +Direct DOM manipulation should not be used in the network UI unless absolutely +necessary. JQuery should not be used. The DOM is generated through the use of +AngularJS templates. + +**SVG (Scalable Vector Graphics)** + +* See: + +The network UI is built as one large SVG element (the SVG canvas) with other +graphical elements (lines, circles, rectangles, paths, and text) absolutely +positioned within the outer most SVG element. The browser is not involved with +layout of the elements within the SVG. Each "widget" in the network UI needs +to track or calculate its own position on the SVG canvas. The z-level of the +elements are determined by the draw order on the canvas which is defined +in `widgets/network_ui.html`. Elements drawn first will be hidden behind +elements drawn later. + + + +**Rendering Pipeline** + +Event -> Javscript objects -> AngularJS templates -> SVG + +AngularJS is used to render the SVG inside the SVG canvas using directives +and templates. AngularJS is also used to schedule when the SVG canvas will +be updated. When an input event comes from the user, or an event is received +over the websocket, javascript objects will be updated according the the network +UI code. Then AngularJS will be notified that it needs to update the templates +either automatically for some events or explicitly using `$scope.$apply();` if +not handled automatically by AngularJS. The templates will render to SVG and be +included in the DOM for the rest of the AWX UI. + +Because the networking UI does not use watchers nor data-binding features of +AngularJS events flow in one way from event to javascript to angular to SVG. +Events do not flow backwards through this pipeline. + +Clicking on an SVG element will not send the event to that SVG element directly +from the browser. It must be routed through the network UI code first. + + +**SVG Primer** + +SVG uses tags to define graphical elements just like HTML uses tags to define +text documents. Commonly use tags include g, circle, rect, path, and text. +SVG elements are absolutely positioned within an SVG canvas. The group tag, g, +is similar to the div tag in HTML. Text in SVG must be contained in the text +tag and cannot be outside tags as in HTML. + +* See: + +Each tag that describes a visual element requires X and Y coordinates as input +to position that element. These coordinates are relative to position of the SVG +canvas. The network UI uses the entire page height and width for the SVG canvas +so that the position on the SVG on the canvas is the same as the position on +the page. + + +SVG supports graphical transformations on several tags to allow relative +positioning of sub-elements which makes calculating the X and Y positions +easier. The network UI uses transformations often for this purpose. +Transformations that are often used here are the translate, scale, and rotate +transforms. Translate moves the origin of the coordinate system to a new point +for the sub-elements. Scale multiplies the size of the units in a coordinate +system by some factor. Rotate performs a rotation about the origin by some +number of degrees. These functions are converted to a matrix operation on the +coordinate system which can be efficiently applied. It is often useful to use +the transforms to simplify the calculations of X and Y coordinates instead of +calculating those values in Javascript. Also these transforms make developing +widgets much easier since we only need to keep up with a single point for the +widget and all other points can be relatively positioned from that point. +Hard-coding positions in widget development is the normal case since transforms +can change the size and position of the widget when the widget is applied to +the canvas. Only when necessary should we calculate positions of parts of a +widget in javascript. + +* See: + + +SVG paths are a mini-language for defining graphics operations in one tag. It +is often used to create shapes that are more complex than lines, rectangles, +and circles. It is very useful for defining arcs. + +* See: + +**SVG and CSS** + +CSS and SVG work really nicely together for setting style, colors, and fonts in SVG. +The SVG uses different attributes for setting colors than does HTML elements. +Most SVG elements use `stroke` and `fill` to define the colors and `stroke-width` +to define the width of lines and curves. The attributes `font-family` and `font-size` +are used to set the font for text elements in SVG. The network UI uses the Less +CSS compiler and BEM naming conventions to simplify and organize CSS. + +* See: [src/style.less](src/style.less) +* See: +* See: + +**Events** + +All mouse and keyboard events are captured by the outer most element of the +network UI. Mouse movements, mouse clicks, and key presses are all routed by +the network UI code and not by the browser. This is done to implement +interactions with the virtual graphical canvas that are not supported by the +browser. "Simple" things like buttons and text fields have to be handled by +the network UI code instead of relying on the browser to route the mouse click +to the appropriate object. + + +The following code captures all the mouse movements, mouse clicks, mouse wheel, +and touch events and sends them to the corresponding network UI controller functions. + +* See: [widgets/network_ui.html](widgets/network_ui.html#L3) + +``` + +``` + + +Key events are captured by the following code: + +* See: [src/network.ui.controller.js](src/network.ui.controller.js) + +``` + $document.bind("keydown", $scope.onKeyDown); +``` + +**Describing Behavior with Finite State Machines** + +To implement complex UI interactions predictably and correctly is a tough +problem. Often the problem is solved by creating a large library of generic +reusable components that are rigorously tested and hardened by a large group of +developers over a period of several years. Eventually the myriad bugs are +hammered out at great expense. Only then can the UI components be reliably +used. This code does not follow that approach. + +The workflows this code supports require very specific UI components that are +not found in generic libraries. The interactions we want to support are not +available in generic libraries. This code develops from scratch only the +components that are necessary to implement the workflows of designing and +operating networks of devices. + +This code defines those elements using finite state machines to process the +events from user input and other software components. Programming with finite +state machines allows us to define formally complex behavior that would +normally be informally defined by branches, functions, object interactions, and +object inheritance. Formal definition eliminates much of the unexpected +behavior that causes defects in the software. + +* See: + +Finite state machines can be represented as a directed graph of labeled nodes and labeled edges +which can be both be represented visually and in machine readable code. + +The network UI uses finite state machines to describe what happens when the software receives +an input. + +**Link FSM** + +![Link FSM](designs/link.png) + +For example the link FSM describes how to connect devices with links. The FSM +diagram above maps out the states and events that will select a device to connect and another +device to connect to. FSMs traditionally start in the `Start` state. We get a free transition +to the `Ready` state by handling the special event called `start` and changing state to `Ready`. +Then when the `NewLink` event is received from a hot key or button click the FSM changes +state to the `Selecting` state. On a `MouseUp` event the FSM changes to the the `Connecting` state +if the mouse is over a device icon otherwise it stays in the `Selecting` state. In the `Connecting` +state the FSM changes to the `Connected` state when it receives a `MouseUp` event and the +mouse is over another device otherwise it goes back to the `Ready` state since the user cancelled +the connecting operation. Finally in the `Connected` state the FSM changes to the `Ready` state +for free using the `start` event so that the user can connect another set of devices. + +* See: [designs/link.yml](designs/link.yml) +* See: [src/link.js](src/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 `src/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 ./src/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 ./src/link.js > ./extracted/link.yml + ./tools/fsm-diff designs/link.yml extracted/link.yml +``` + + +**Validating That Design Matches Implementation** + +Use the `make extract` and `make diff` Makefile targets to do a mass extact of the +FSMs from the implementation and a mass comparison against the designs. Take +note of any differences between design and implementation and update the appropriate +files as outlined in the workflows above. + +``` + make extract; make diff +``` + + +**Finite State Machine Implementation** + +The implementation of a finite state machine in the network UI is split into +two parts: the declaration of the states and the event-handlers which may cause +FSM transitions using `controller.changeState`. + +**FSM States** + +* See: +* See: + +States are implemented using an object-oriented style in ES5 using the +flyweight and singleton patterns. This means that the state objects store no +information on themselves and that there is only one instance of each state +class. All states should provide a `start` and `end` function which will be +called when a FSM state is entered and exited respectively. Subclassing +[fsm.State](src/fsm.js#L36) will provide empty `start` and `end` functions that +can be overridden as necessary. + +* See: [src/fsm.js](src/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: [src/models.js](src/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: [src/link.js](src/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: [src/link.js](src/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: [src/models.js](src/models.js) + +The models defined in [src/models.js](src/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: [src/messages.js](src/messages.js) + +The messages defined are [src/messages.js](src/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 + +**Message Passing** + +Messages are passed along channels between FSMs and over the websocket to and from the +server. Messages from the server over the web socket and user input events from the web +browser are passed to the `first_controller` where they are handled and discarded or +passed along the chain of FSMControllers until they reach the end with `null_controller` +or they are handled and the models are updated. + +* See: [src/network.ui.controller.js](src/network.ui.controller.js#L115) + +The order (from first to last) of message handling is: + +* Mode FSM +* Site Toolbox FSM +* Rack Toolbox FSM +* Inventory Toolbox FSM +* App Toolbox FSM +* Time FSM +* Buttons FSM +* Site FSM +* Rack FSM +* Group FSM +* Stream FSM +* Link FSM +* Move FSM +* Device Detail FSM +* View FSM +* Null FSM + + +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 [widgets/inventory_toolbox.html](widgets/inventory_toolbox.html) + +``` + + + + + ... + +``` + +* Add a directive that loads that template in `src` with name [src/inventory.toolbox.directive.js](src/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 [src/network.ui.app.js](src/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 `widgets` in [widgets/network_ui.html](widgets/network_ui.html#L94) + +``` + +``` + +* Test that the directive is loaded when the page renders in a browser +* Iterate on the template for the new widget until the UI look matches the mockup +* Design the interaction behavior using [fsm-designer-svg](https://github.com/benthomasson/fsm-designer-svg) + +![Toolbox](designs/toolbox.png) + +* Export the FSM design to `designs` in a file named `designs/toolbox.yml` + +``` + finite_state_machine_id: 14 + name: toolbox + states: + - id: 2 + label: Selected + x: 1180 + y: 959 + - id: 6 + label: Move + x: 1409 + y: 741 + - id: 3 + label: Ready + x: 892 + y: 429 + - id: 4 + label: Scrolling + x: 567 + y: 431 + - id: 5 + label: Start + x: 892 + y: 216 + - id: 7 + label: Selecting + x: 888 + y: 710 + - id: 1 + label: Dropping + x: 1358 + y: 431 + transitions: + - from_state: Selecting + label: onMouseDown + to_state: Selected + - from_state: Selected + label: onMouseMove + to_state: Move + - from_state: Selecting + label: onMouseDown + to_state: Ready + - from_state: Selected + label: onMouseUp + to_state: Ready + - from_state: Dropping + label: start + to_state: Ready + - from_state: Start + label: start + to_state: Ready + - from_state: Scrolling + label: onMouseWheel + to_state: Ready + - from_state: Ready + label: onMouseWheel + to_state: Scrolling + - from_state: Ready + label: onMouseDown + to_state: Selecting + - from_state: Move + label: onMouseUp + to_state: Dropping +``` + +* Create a new empty FSM implementation file in `src` named `src/toolbox.fsm.js` + +``` + touch src/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 [src/messages.js](src/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: [src/toolbox.fsm.js](src/toolbox.fsm.js) + +* Add the FSM implementation to a FSMController in [src/network.ui.controller.js](src/network.ui.controller.js#L145) + +``` + $scope.inventory_toolbox_controller = new fsm.FSMController($scope, toolbox_fsm.Start, $scope.app_toolbox_controller); +``` + +* Test the interaction manually in a browser +* Iterate on changing the event handlers until the desired interaction is achieved +* Update the design to match the implementation diff --git a/awx/network_ui/static/network_ui/Makefile b/awx/network_ui/static/network_ui/Makefile index 36a0fce41e..4ef346b42f 100644 --- a/awx/network_ui/static/network_ui/Makefile +++ b/awx/network_ui/static/network_ui/Makefile @@ -58,18 +58,18 @@ extract: diff: - fsm_diff designs/button.yml extracted/button.yml - fsm_diff designs/buttons.yml extracted/buttons.yml - fsm_diff designs/device_detail.yml extracted/device_detail.yml - fsm_diff designs/group.yml extracted/group.yml - fsm_diff designs/hotkeys.yml extracted/hotkeys.yml - fsm_diff designs/link.yml extracted/link.yml - fsm_diff designs/mode.yml extracted/mode.yml - fsm_diff designs/move.yml extracted/move.yml - fsm_diff designs/null.yml extracted/null.yml - fsm_diff designs/rack.yml extracted/rack.yml - fsm_diff designs/site.yml extracted/site.yml - fsm_diff designs/stream.yml extracted/stream.yml - fsm_diff designs/time.yml extracted/time.yml - fsm_diff designs/toolbox.yml extracted/toolbox.yml - fsm_diff designs/view.yml extracted/view.yml + ./tools/fsm-diff designs/button.yml extracted/button.yml + ./tools/fsm-diff designs/buttons.yml extracted/buttons.yml + ./tools/fsm-diff designs/device_detail.yml extracted/device_detail.yml + ./tools/fsm-diff designs/group.yml extracted/group.yml + ./tools/fsm-diff designs/hotkeys.yml extracted/hotkeys.yml + ./tools/fsm-diff designs/link.yml extracted/link.yml + ./tools/fsm-diff designs/mode.yml extracted/mode.yml + ./tools/fsm-diff designs/move.yml extracted/move.yml + ./tools/fsm-diff designs/null.yml extracted/null.yml + ./tools/fsm-diff designs/rack.yml extracted/rack.yml + ./tools/fsm-diff designs/site.yml extracted/site.yml + ./tools/fsm-diff designs/stream.yml extracted/stream.yml + ./tools/fsm-diff designs/time.yml extracted/time.yml + ./tools/fsm-diff designs/toolbox.yml extracted/toolbox.yml + ./tools/fsm-diff designs/view.yml extracted/view.yml diff --git a/awx/network_ui/static/network_ui/README.md b/awx/network_ui/static/network_ui/README.md new file mode 100644 index 0000000000..6e514ec7d3 --- /dev/null +++ b/awx/network_ui/static/network_ui/README.md @@ -0,0 +1,2 @@ + +* See: [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/awx/network_ui/static/network_ui/designs/README.md b/awx/network_ui/static/network_ui/designs/README.md new file mode 100644 index 0000000000..3a20f73bcd --- /dev/null +++ b/awx/network_ui/static/network_ui/designs/README.md @@ -0,0 +1,161 @@ + +Finite State Machine Designs +============================ + +This directory contains the finite state machine designs that were used to +generate the skeleton of the javascript implementations and can be used to +check that the implementations still match the designs. + + +**Machine Readable FSM Schema** + +The machine readable FSM schema contains three top-level elements: `name`, `states`, and `transitions`. +* The `name` element is a string. +* The `states` element contains a list of `state` elements which have attributes `id`, `label`, and `x`, and `y`. +* The `transitions` element contains a list of `transition` elements which have attributes `from_state`, `to_state`, and `label`. + + +**Design Diagrams** + +The diagrams below are visual representations of the finite state machine designs in this directory. +The equivalent machine readable representations are linked as well. + +--- + +**Null FSM** +* See: null.yml + +The null FSM is an FSM that ignores all events. + +![Null FSM](null.png) + +--- + +**Button FSM** +* See: button.yml + +The button FSM describes how a button works. The key insight here is that a button is not +clicked if the mouse is not over the button on both the `MouseDown` and `MouseUp` events. Moving +the mouse off the button before `MouseUp` is not a click. + +![Button FSM](button.png) + +--- + +**Buttons FSM** +* See: buttons.yml + +The buttons FSM distributes events to the buttons which each have their own FSM. + +![Buttons FSM](buttons.png) + +--- + +**Device Detail FSM** +* See: device_detail.yml + +The device detail FSM describes interactions when zoomed into a device. + +![Device Detail FSM](device_detail.png) + +--- + +**Group FSM** +* See: group.yml + +The group FSM describes how to organize multiple devices together in a group. + +![Group FSM](group.png) + +--- + +**Hot Keys FSM** +* See: hotkeys.yml + +The hot keys FSM handles key events and generates new events like `NewLink` to implement +hot keys. + +![Hot Keys FSM](hotkeys.png) + +--- + +**Link FSM** +* See: link.yml + +The link FSM connects two devices together with a link. + +![Link](link.png) + +--- + +**Mode FSM** +* See: mode.yml + +The mode FSM controls the overall mode of the network UI application. + +![Mode](mode.png) + +--- + +**Move FSM** +* See: move.yml + +The move FSM controls placement of devices as well as editing the device labels. + +![Move](move.png) + +--- + +**Rack FSM** +* See: rack.yml + +The rack FSM controls organizing devices into a special group called a rack. + +![Rack](rack.png) + +--- + +**Site FSM** +* See: site.yml + +The site FSM controls organizing devices into a special group called a site. + +![Site](site.png) + +--- + +**Stream FSM** +* See: stream.yml + +The stream FSM controls how streams are defined between devices. + +![Stream](stream.png) + +--- + +**Time FSM** +* See: time.yml + +The time FSM controls undo/redo functionality of the network UI. + +![Time](time.png) + +--- + +**Toolbox FSM** +* See: toolbox.yml + +The toolbox FSM controls the drag-and-drop toolboxes and allow placement of new devices, applications, +racks, and sites onto the canvas. + +![Toolbox](toolbox.png) + +--- + +**View FSM** +* See: view.yml + +The view FSM controls the panning and scaling of the the virtual canvas through clicking-and-dragging +of the background and scrolling the mousewheel. + +![View](view.png) diff --git a/awx/network_ui/static/network_ui/designs/button.png b/awx/network_ui/static/network_ui/designs/button.png new file mode 100644 index 0000000000..5a1bc7aaa1 Binary files /dev/null and b/awx/network_ui/static/network_ui/designs/button.png differ diff --git a/awx/network_ui/static/network_ui/designs/buttons.png b/awx/network_ui/static/network_ui/designs/buttons.png new file mode 100644 index 0000000000..8538b06614 Binary files /dev/null and b/awx/network_ui/static/network_ui/designs/buttons.png differ diff --git a/awx/network_ui/static/network_ui/designs/device_detail.png b/awx/network_ui/static/network_ui/designs/device_detail.png new file mode 100644 index 0000000000..0f11ac8ffe Binary files /dev/null and b/awx/network_ui/static/network_ui/designs/device_detail.png differ diff --git a/awx/network_ui/static/network_ui/designs/group.png b/awx/network_ui/static/network_ui/designs/group.png new file mode 100644 index 0000000000..5ca5ead538 Binary files /dev/null and b/awx/network_ui/static/network_ui/designs/group.png differ diff --git a/awx/network_ui/static/network_ui/designs/hotkeys.png b/awx/network_ui/static/network_ui/designs/hotkeys.png new file mode 100644 index 0000000000..d21ff09cc6 Binary files /dev/null and b/awx/network_ui/static/network_ui/designs/hotkeys.png differ diff --git a/awx/network_ui/static/network_ui/designs/link.png b/awx/network_ui/static/network_ui/designs/link.png new file mode 100644 index 0000000000..4058fd5519 Binary files /dev/null and b/awx/network_ui/static/network_ui/designs/link.png differ diff --git a/awx/network_ui/static/network_ui/designs/messages.yml b/awx/network_ui/static/network_ui/designs/messages.yml new file mode 100644 index 0000000000..ad58b27dd5 --- /dev/null +++ b/awx/network_ui/static/network_ui/designs/messages.yml @@ -0,0 +1,45 @@ +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]} + - {msg_type: DeviceDestroy, fields: [msg_type, sender, id, previous_x, previous_y, previous_name, previous_type]} + - {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: Undo, fields: [msg_type, sender, original_message]} + - {msg_type: Redo, fields: [msg_type, sender, original_message]} + - {msg_type: Deploy, fields: [msg_type, sender]} + - {msg_type: Destroy, fields: [msg_type, sender]} + - {msg_type: Discover, fields: [msg_type, sender]} + - {msg_type: Layout, fields: [msg_type, sender]} + - {msg_type: MultipleMessage, fields: [msg_type, sender, messages]} + - {msg_type: Coverage, fields: [msg_type, sender, coverage]} + - {msg_type: MouseEvent, fields: [msg_type, sender, x, y, type]} + - {msg_type: MouseWheelEvent, fields: [msg_type, sender, delta, deltaX, deltaY, type, originalEvent]} + - {msg_type: KeyEvent, fields: [msg_type, sender, key, keyCode, type, altKey, shiftKey, ctrlKey, metaKey]} + - {msg_type: TouchEvent, fields: [msg_type, sender, type, touches]} + - {msg_type: StartRecording, fields: [msg_type, sender]} + - {msg_type: StopRecording, fields: [msg_type, sender]} + - {msg_type: ViewPort, fields: [msg_type, sender, scale, panX, panY]} + - {msg_type: CopySite, fields: [msg_type, site]} + - {msg_type: GroupMove, fields: [msg_type, sender, id, x1, y1, x2, y2, previous_x1, previous_y1, previous_x2, previous_y2]} + - {msg_type: GroupCreate, fields: [msg_type, sender, id, x1, y1, x2, y2, name, type]} + - {msg_type: GroupDestroy, fields: [msg_type, sender, id, previous_x1, previous_y1, previous_x2, previous_y2, previous_name, previous_type]} + - {msg_type: GroupLabelEdit, fields: [msg_type, sender, id, name, previous_name]} + - {msg_type: GroupSelected, fields: [msg_type, sender, id]} + - {msg_type: GroupUnSelected, fields: [msg_type, sender, id]} + - {msg_type: GroupMembership, fields: [msg_type, sender, id, members]} + - {msg_type: TableCellEdit, fields: [msg_type, sender, sheet, col, row, old_value, new_value]} + - {msg_type: ProcessCreate, fields: [msg_type, id, name, type, device_id, x, y]} + - {msg_type: StreamCreate, fields: [msg_type, sender, id, from_id, to_id, label]} + - {msg_type: StreamDestroy, fields: [msg_type, sender, id, from_id, to_id, label]} + - {msg_type: StreamLabelEdit, fields: [msg_type, sender, id, label, previous_label]} + - {msg_type: StreamSelected, fields: [msg_type, sender, id]} + - {msg_type: StreamUnSelected, fields: [msg_type, sender, id]} + diff --git a/awx/network_ui/static/network_ui/designs/mode.png b/awx/network_ui/static/network_ui/designs/mode.png new file mode 100644 index 0000000000..b8dff5610d Binary files /dev/null and b/awx/network_ui/static/network_ui/designs/mode.png differ diff --git a/awx/network_ui/static/network_ui/designs/move.png b/awx/network_ui/static/network_ui/designs/move.png new file mode 100644 index 0000000000..04eb438255 Binary files /dev/null and b/awx/network_ui/static/network_ui/designs/move.png differ diff --git a/awx/network_ui/static/network_ui/designs/null.png b/awx/network_ui/static/network_ui/designs/null.png new file mode 100644 index 0000000000..f095a1cad4 Binary files /dev/null and b/awx/network_ui/static/network_ui/designs/null.png differ diff --git a/awx/network_ui/static/network_ui/designs/rack.png b/awx/network_ui/static/network_ui/designs/rack.png new file mode 100644 index 0000000000..505cfdda6b Binary files /dev/null and b/awx/network_ui/static/network_ui/designs/rack.png differ diff --git a/awx/network_ui/static/network_ui/designs/site.png b/awx/network_ui/static/network_ui/designs/site.png new file mode 100644 index 0000000000..72d256f552 Binary files /dev/null and b/awx/network_ui/static/network_ui/designs/site.png differ diff --git a/awx/network_ui/static/network_ui/designs/stream.png b/awx/network_ui/static/network_ui/designs/stream.png new file mode 100644 index 0000000000..3a00d21e97 Binary files /dev/null and b/awx/network_ui/static/network_ui/designs/stream.png differ diff --git a/awx/network_ui/static/network_ui/designs/time.png b/awx/network_ui/static/network_ui/designs/time.png new file mode 100644 index 0000000000..cec5fe69ee Binary files /dev/null and b/awx/network_ui/static/network_ui/designs/time.png differ diff --git a/awx/network_ui/static/network_ui/designs/toolbox.png b/awx/network_ui/static/network_ui/designs/toolbox.png new file mode 100644 index 0000000000..14f676bddc Binary files /dev/null and b/awx/network_ui/static/network_ui/designs/toolbox.png differ diff --git a/awx/network_ui/static/network_ui/designs/view.png b/awx/network_ui/static/network_ui/designs/view.png new file mode 100644 index 0000000000..e0d7f261b2 Binary files /dev/null and b/awx/network_ui/static/network_ui/designs/view.png differ diff --git a/awx/network_ui/static/network_ui/extract_messages.js b/awx/network_ui/static/network_ui/extract_messages.js new file mode 100755 index 0000000000..914e5c4cb0 --- /dev/null +++ b/awx/network_ui/static/network_ui/extract_messages.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node +var YAML = require('yamljs'); + +function Iterator(o){ + var k=Object.keys(o); + return { + next:function(){ + return k.shift(); + } + }; +} + +var myArgs = process.argv.slice(2); +var implementation = require(myArgs[0]); +var messages = []; +var data = {messages: messages}; +var message_iter = Iterator(implementation); +var field_iter = null; +var next_message = message_iter.next(); +var next_field = null; +var message = null; +var message_instance = null; +var field = null; +var fields = null; +var i = 0; +while(next_message !== undefined) { + message = implementation[next_message]; + try { + message_instance = new message() + } catch(err) { + next_message = message_iter.next(); + continue; + } + fields = []; + field_iter = Iterator(message_instance) + next_field = field_iter.next(); + while (next_field !== undefined) { + fields.push(next_field); + // field = message.constructor.prototype[next_field]; + // if (field.transitions !== undefined) { + // for (i = 0; i < field.transitions.length; i++) { + // transitions.push({from_message: next_message, + // to_message:field.transitions[i], + // label:next_field}); + // } + // } + next_field = field_iter.next(); + } + if(message_instance.msg_type !== null && message_instance.msg_type !== undefined) { + messages.push({msg_type: message_instance.msg_type, + fields: fields}); + } + next_message = message_iter.next(); +} + + +console.log(YAML.stringify(data)); diff --git a/awx/network_ui/static/network_ui/tools/fsm-diff b/awx/network_ui/static/network_ui/tools/fsm-diff new file mode 100755 index 0000000000..5a400e96fe --- /dev/null +++ b/awx/network_ui/static/network_ui/tools/fsm-diff @@ -0,0 +1,9 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from fsm_diff.cli import main + +if __name__ == '__main__': + import sys + sys.exit(main(sys.argv[1:])) +