MediaWiki API result

This is the HTML representation of the JSON format. HTML is good for debugging, but is unsuitable for application use.

Specify the format parameter to change the output format. To see the non-HTML representation of the JSON format, set format=json.

See the complete documentation, or the API help for more information.

{
    "batchcomplete": "",
    "continue": {
        "gapcontinue": "Supported_Software",
        "continue": "gapcontinue||"
    },
    "warnings": {
        "main": {
            "*": "Subscribe to the mediawiki-api-announce mailing list at <https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce> for notice of API deprecations and breaking changes."
        },
        "revisions": {
            "*": "Because \"rvslots\" was not specified, a legacy format has been used for the output. This format is deprecated, and in the future the new format will always be used."
        }
    },
    "query": {
        "pages": {
            "20": {
                "pageid": 20,
                "ns": 0,
                "title": "Realtime Messaging API",
                "revisions": [
                    {
                        "contentformat": "text/x-wiki",
                        "contentmodel": "wikitext",
                        "*": "= AMI Backend Interface =\n\n== Goal ==\nThe 3Forge Application Management Interface (AMI) provides applications with a simple mechanism for connecting to the 3Forge Relay. 3Forge AMI's consolidated GUI lets users:\n\n* View the application's health statistics\n* Receive and manage objects though a simple workflow procedure\n* Interact with applications to call routines inside the application\n\n== Conventions ==\n\n* This document is written from the perspective of the application. \"Outbound\" is the application ''sending'' data, and \"inbound\" is the Application ''receiving'' data.\n* All key words are in a \"<span style=\"font-family: courier new; color: red;\">courier</span>\" font.\n* Trailing text is indicated with an ellipses or \"...\".\n* Special ASCII chars are qualified inside parenthesis.\n* Brackets \"[]\" indicate optionally supplied data.\n* Examples are in <span style=\"color: blue;\">blue</span>.\n\n== Overview ==\nThis interface is an extension of the 3Forge AMI product. Applications interact with AMI through the relays only, and not the central server nor the front end servers. Each AMI relay, upon startup, establishes a server socket on a well-known configurable port. As each application starts up, it should connect to the server socket of the relay running on its local host. Multiple applications can connect to one relay.\n\nApplications then interact with the relay by sending and receiving \"instructions.\" Instructions are well-defined, atomic, sequential and transactional messages. The first instruction an application sends after connecting must be a login (<span style=\"color: red;\">L</span>) instruction. Following that, applications can send instructions arbitrarily and should listen for incoming instructions. Optionally, applications can send a logout (<span style=\"color: red;\">X</span>) message to initiate a graceful shutdown.\n\n= Instruction Format =\nThis protocol is designed to be flexible, compact and human readable. Each instruction must have a type and may contain a sequence number and timestamp. The general format for each message is: '''<span style=\"font-family: courier new; \">TYPE[#SEQNUM][@NOW][|PARAMS...]\\n</span>'''\n\nTYPE: The type of message. Please see following sections for details on each type.\n\nValid ''outbound'' types are:\n\n* <span style=\"font-family: courier new; color: red;\">L</span> (login)\n* <span style=\"font-family: courier new; color: red;\">S</span> (status)\n* <span style=\"font-family: courier new; color: red;\">S</span> (alert) DEPRECATED\n* <span style=\"font-family: courier new; color: red;\">O</span> (object)\n* <span style=\"font-family: courier new; color: red;\">C</span> (command definition)\n* <span style=\"font-family: courier new; color: red;\">R</span> (response to execute command)\n* <span style=\"font-family: courier new; color: red;\">D</span> (delete objects)\n* <span style=\"font-family: courier new; color: red;\">X</span> (exit)\n* <span style=\"font-family: courier new; color: red;\">H</span> (help)\n* <span style=\"font-family: courier new; color: red;\">P</span> (pause)\n* <span style=\"font-family: courier new; color: red;\">D</span> (delete objects)\n\nValid ''inbound'' types are:\n\n* <span style=\"font-family: courier new; color: red;\">M</span> (status message)\n* <span style=\"font-family: courier new; color: red;\">E</span> (execute command)\n''SEQNUM'': (optional) The sequence number of the instruction. If supplied, the first instruction (Login) must have a sequence number of 0, the following message must have a sequence number of 1, and so on. This sequence number will be used when AMI is ack-ing.\n\n''NOW'': (optional) Current time in milliseconds since the UNIX epoch. This will aid AMI in determining if there is a lag.\n\n''PARAMS'': (optional) Should be in the format key=value|key2=value|... where entries are pipe (|) delimited and keys are unique. \n\nNotes:\n\n* Backslashes (\\), quotes (\"), single quotes ('), (\\n,\\r,\\t,\\f,\\b) \u00a0must be escaped via backslash (\\).\n* Unicode within strings must be expressed in 4 digit hex notation such as: \\uFFFF\n* Keys must be alphanumeric.\n* All 1 and 2 letter fully upper case keys are reserved.\n* Values that are not numeric must be surrounded in quotes.\n* ''\\n'': Each instruction must end with a linefeed (0x0A) or linefeed + carriage (0x0A0x0D).\n* The Syntax determines the parameter type, please note some types have multiple syntaxes:\n\n{| class=\"wikitable\"\n!Type\n!Syntax\n!Example\n!Notes\n|-\n|Integer\n|''nnn''\n|<span style=\"color: blue;\">-234</span>\n|Whole numbers default to integer\n|-\n|Long\n|''nnn''<span style=\"color: red;\">L</span>\n|<span style=\"color: blue;\">234L</span>\n|\n|-\n|Double\n|''nnn.nnn''<span style=\"color: red;\">D</span>\n|<span style=\"color: blue;\">123.123D</span>\n|Decimal if optional\n|-\n|Float\n|''nnn''F\n|<span style=\"color: blue;\">123F</span>\n|Decimals in a number default to float\n|-\n|\n|''nnn.nnn''\n|<span style=\"color: blue;\">123.123</span>\n|Decimals default to float\n|-\n|String\n|\"''sss''\"\n|<span style=\"color: blue;\">\"what\"</span>\n|Quotes must be escaped with a backslash (\\)\n|-\n|Enum\n|'<nowiki/>''sss''<nowiki/>'\n|<span style=\"color: blue;\">'this'</span>\n|Quotes must be escaped with a backslash (\\)\n|-\n|UTC\n|''nnn''T\n|<span style=\"color: blue;\">1422059533454T</span>\n|Unix epoch\n|-\n|\n|\"''sss''\"<span style=\"color: red;\">T</span>(''fff'')\n|<span style=\"color: blue;\">\"20140503\"T(yyyyMMdd)</span>\n|Uses date format (fff) to parse string (sss) to a utc\n|-\n|JSON\n|\"''sss''\"<span style=\"color: red;\">J</span>\n|<span style=\"color: blue;\">\"{this:\\\"that\\\"}J</span>\n|Base 64 UU Encoded\n|-\n|Binary\n|\"''ssss''\"<span style=\"color: red;\">U</span>\n|<span style=\"color: blue;\">\"12fs1323\"U</span>\n|Base UUEncoded\n|-\n|Boolean\n|<span style=\"color: red;\">true false</span>\n|<span style=\"color: blue;\">true</span>\n|Case sensitive\n|-\n|Null\n|null\n|<span style=\"color: blue;\">Null</span>\n|\n|}\n*''nnn'' represents 0-9. Numbers must be base 10 and can be signed\n*''sss'' represents alpha numeric\n*''fff'' represents java syntax: '''<nowiki>http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html</nowiki>'''\n\n= Outbound Instruction Type =\n\n== Outbound Instruction Type - Login (<span style=\"font-family: courier new; color: red;\">L</span>) ==\nMust be the first instruction sent from the application after connecting to the relay. It is used to establish identity and confirm a proper login.\n\n'''Required fields supplied by application'''\n\n<span style=\"font-family: courier new; color: red;\">I</span>: A universally unique string identifying the process. Multiple runs of the same application should have the same ID (I). If an application were to be shut down and restarted, the ID (<span style=\"font-family: courier new; color: red;\">I</span>) should not change.\n\n'''Optional parameters supplied by application'''\n\nOptional parameters are used to provide metrics about the application.\n\n<span style=\"font-family: courier new; color: red;\">P</span>: Used to supply a fully qualified java class name to an AMI relay plugin. The class must implement the com.f1.ami.relay.AmiRelayPlugin interface.\n\n<span style=\"font-family: courier new; color: red;\">O</span>: Used to supply options about the current session. The following options are available and can be used in conjunction by comma delimiting:\n\n* <span style=\"font-family: courier new; color: red;\">QUIET</span> - AMI will not send any information back to the client (statuses, etc). Note execute commands (E) will still be send to the client\n* <span style=\"font-family: courier new; color: red;\">LOG</span> - Force AMI relay \u00a0to log data to / from this session (default file = ''AmiSession.log'')\n* <span style=\"font-family: courier new; color: red;\">UNIX</span> - Force AMI to not send \\r on messages\n* <span style=\"font-family: courier new; color: red;\">WINDOWS</span> - Force AMI to send \\r on messages\n* <span style=\"font-family: courier new; color: red;\">TTY</span> - teletype terminal (for UNIX) is for interactively working with AMI backend\n'''Example'''\n\nIn the below example, we see the optional attributes APP, MEM and STAT are supplied to shed light on the application name, memory used and status. Note that string values are surrounded in quotes. The options are forcing ami to use UNIX formatting (no \\r) and force logging at the ami relay.\n\n<span style=\"font-family: courier new; color: red;\">L</span><span style=\"font-family: courier new; color: blue;\">|</span><span style=\"font-family: courier new; color: red;\">I</span><span style=\"font-family: courier new; color: blue;\">=\"12n3f321g19\"|APP=\"SOR\"|MEM=12000|STAT=\"OKAY\"|O=\"UNIX,LOG\"</span>\n\n== Outbound Instruction Type - Status (<span style=\"font-family: courier new; color: red;\">S</span>) ==\nThis has been deprecated\n\n== Outbound Instruction Type - Object (<span style=\"font-family: courier new; color: red;\">O</span>) ==\nUsed to create an object (row) that gets inserted into AMI DB\n\n'''Required fields supplied by application'''\n\n<span style=\"font-family: courier new; color: red;\">T</span>: Table name\n\n'''Optional parameters supplied by application'''\n\n<span style=\"font-family: courier new; color: red;\">I</span>: A unique string ID for the object used for updating the object later. Subsequent messages with the same object ID (<span style=\"font-family: courier new; color: red;\">I</span>) and same running process ID (<span style=\"font-family: courier new; color: red;\">I</span>) provided in the login (<span style=\"font-family: courier new; color: red;\">L</span>) will cause an update.\n\n<span style=\"font-family: courier new; color: red;\">E</span>: Expires on; ''negative'' for how far into the future from the time of message receipt in AMI center and ''positive'' numbers are exact dates in the future based on Epoch time. Units are milliseconds\n\n<span style=\"font-family: courier new; color: red;\">A</span>: Associated alert ID (<span style=\"color: red;\">DEPRECATED</span>)\n\n'''Example'''\n\nThe below example inserts a record into the Order table. The key-value pair maps value to column, e.g. the first column is qty, and the value is 1223. The last column represents an AMI reserved column, E, that indicates the expiry time of this row.\n\n<span style=\"font-family: courier new; color: red;\">O</span><span style=\"font-family: courier new; color: red;\">|</span><span style=\"font-family: courier new; color: red;\">I</span><span style=\"font-family: courier new; \">=\"Order1374\"|</span><span style=\"font-family: courier new; color: red;\">T</span><span style=\"font-family: courier new; \">=\"Order\"|qty=1223|filled=13222|status=\"OverFill\"|E=-100000L</span>\n\n== Outbound Instruction Type - Command Definition (<span style=\"font-family: courier new; color: red;\">C</span>) ==\nUsed to create a command that will allow the user to right click on an application or object, and perform a command it.\n\n'''Required fields supplied by application'''\n\n<span style=\"font-family: courier new; color: red;\">I</span>: Command ID\n\n<span style=\"font-family: courier new; color: red;\">N</span>: Name that is displayed to user in the right-click context menu. Note: Use periods (.) to create submenus\n\n'''Example'''\n\n<span style=\"font-family: courier new; \">N=\"order.cancel\"</span>: will place the ''cancel'' command under the ''order'' menu\n\n[[File:RTAPI.Name.jpg]]\n\n'''Optional parameters supplied by application'''\n\n<span style=\"font-family: courier new; color: red;\">L</span>: Permissions Level of 0 means remove command, and any other number is used for entitlements as part of the AMI entitlement engine.\n\n<span style=\"font-family: courier new; color: red;\">A</span>: Configuration for input form described in JSON format. ''See'' '''[[Description of AMI JSON Form definition fields.htm|appendix]]''' ''for JSON Form layout''\n\n<span style=\"font-family: courier new; color: red;\">W</span>: An expression that will determine which rows the command will be available at a row/node level. \u00a0You may also reference user-specific variables:\n\n* <span style=\"font-family: courier new; color: red;\">__USERNAME</span> - login name of user that is executing the command\n* <span style=\"font-family: courier new; color: red;\">user.''xxxxx''</span> - a property, associated with the user's entitlements that are prefixed w/ amivar_.\n* To use legacy variables names such as <span style=\"font-family: courier new; color: red;\">user.username</span>, set the following property: ami.web.support.legacy.amiscript.varnames=true\n'''Example'''\n\nIf the user has an attribute in the entitlements server \"<span style=\"color: blue;\">amivar_group=sales</span>\" then the variable <span style=\"color: blue;\">user.group</span> will have the value sales\n\n<span style=\"font-family: courier new; color: red;\">T</span>: An expression that will determine which rows the command will be available at a panel level. \u00a0You may also reference user-specific variables and panel specific variables:\n\n* <span style=\"font-family: courier new; color: red;\">__USERNAME</span> - login name of user that is executing the command\n* <span style=\"font-family: courier new; color: red;\">user.</span>''<span style=\"font-family: courier new; color: blue;\">xxxxx</span>'' - (''see'' '''W clause''' ''for details'')\n* <span style=\"font-family: courier new; color: red;\">panel.title</span> -The title name of the panel\n* <span style=\"font-family: courier new; color: red;\">panel.types</span> - A comma (,) delimited list of types (T) shown in the panel\n* <span style=\"font-family: courier new; color: red;\">panel.visualization</span> - The type of visualization. Visualizations include: <span style=\"font-family: courier new; color: blue;\">table, form, treemap, chart, chart_3d</span>\n* <span style=\"font-family: courier new; color: red;\">panel.id</span> - The ID of the panel (shown above the panel configuration button). To edit the panel ID, open the panel's '''Settings''' menu\n<span style=\"font-family: courier new; color: red;\">H</span>: Help, gets displayed in the top of the display box.\n\n<span style=\"font-family: courier new; color: red;\">P</span>: Priority for display in the menu. Commands with a higher priority are listed in the context menu above those with lower priority, 0 = highest priority, 1 = 2<sup>nd</sup> highest, etc.\n\n<span style=\"font-family: courier new; color: red;\">E</span>: Enabled where (expression)\n\n'''Example'''\n\n<span style=\"font-family: courier new; color: red;\">E</span><span style=\"font-family: courier new; color: blue;\">=\"Quantity==300\"</span>; the command will only be enabled where the Quantity = 300\n\n<span style=\"font-family: courier new; color: red;\">F</span>: Fields; returns the values of specified fields.\n\n'''Example'''\n\nUsing a command with the following parameter: <span style=\"font-family: courier new; color: red;\">F</span><span style=\"font-family: courier new; color: blue;\">=\"Price\"</span> will return:\n\n<span style=\"font-family: courier new; color: red;\">E</span><span style=\"font-family: courier new; color: blue;\">@1414512334864|</span><span style=\"font-family: courier new; color: red;\">C</span><span style=\"font-family: courier new; color: blue;\">=\"Test\"|</span><span style=\"font-family: courier new; color: red;\">I</span><span style=\"font-family: courier new; color: blue;\">=\"8vJfyRmzkir7EwkQIpnnrA\"|</span><span style=\"font-family: courier new; color: red;\">U</span><span style=\"font-family: courier new; color: blue;\">\"david\"|</span><span style=\"font-family: courier new; color: red;\">V</span><span style=\"font-family: courier new; color: blue;\">=\"[{\"O\":\"23\",\"Price\":70.0},{\"O\":\"24\",\"Price\":95.0},{\"O\":\"25\",\"Price\":60.0},{\"O\":\"26\",\"Price\":50.0}]\"J</span>\n\n<span style=\"font-family: courier new; color: red;\">M</span>: Multiple; constrains the number of rows that can be selected when running the command. The syntax is n-m where n is min and m is max. If m is not supplied than there is no upper limit.\n\n'''Example'''\n\n* \"0\" = available when no records are selected.\n* \"1\" = available only when a single record is selected ('''default''').\n* \"0-1\" = available when no records or a single record is selected.\n* \"1-\" = available when one or more records are selected.\n* \"3-5\" = available when 3, 4, or 5 records are selected.\n\n<span style=\"font-family: courier new; color: red;\">S</span>: Style of the menu item in JSON\n\n'''Example'''\n\n<span style=\"font-family: courier new; \">s='{\"separator\":\"TOP|BOTTOM|BOTH\"}'</span> will add a separator between the commands\n\n[[File:RTAPI.Multi.jpg]]\n\n<span style=\"font-family: courier new; color: red;\">C</span>: Declare what Condition(s) will cause command to be evaluated, comma (,) delimited list. Options include:\n\n* \"now\" = Immediately (when the command is declared)\n* \"user_click\" = when the user clicks on a row (this is the default)\n* \"user_close_layout\" = when the user closes a layout\n* \"user_open_layout\" = when the user opens a layout (or logs in w/ a default layout assigned)\n\n'''Example'''\n\n<span style=\"font-family: courier new; \">C='now,user_open_layout'</span> will cause the command to be run on all current sessions and any new sessions that are created.\n\n<span style=\"font-family: courier new; color: red;\">X</span>: Execute AmiScript when the command is evaluated and the various criteria are met. ''See'' '''AMI Script Doc''' in AMI Web under Help for details.\n\n'''Example'''\n\n<span style=\"font-family: courier new; color: red;\">C</span><span style=\"font-family: courier new; color: blue;\">|<span style=\"font-family: courier new; color: red;\">I</span>=\"alert\"|<span style=\"font-family: courier new; color: red;\">X</span>='session.alert(\"hi\");'|<span style=\"font-family: courier new; color: red;\">C</span>=\"now\"</span>\n\n'''Example'''\n\nIn the below example, a command is created to allow a user to bust all orders for an app and a single order. With the bst2 command the user must supply a comment.\n\n<span style=\"font-family: courier new; color: red;\">C</span><span style=\"font-family: courier new; color: blue;\">|</span><span style=\"font-family: courier new; color: red;\">I</span><span style=\"font-family: courier new; color: blue;\">=\"bst\"|</span><span style=\"font-family: courier new; color: red;\">N</span><span style=\"font-family: courier new; color: blue;\">=\"Bust Every Order\"|</span><span style=\"font-family: courier new; color: red;\">H</span><span style=\"font-family: courier new; color: blue;\">=\"this is used to bust all orders\"|</span><span style=\"font-family: courier new; color: red;\">L</span><span style=\"font-family: courier new; color: blue;\">=2|</span><span style=\"font-family: courier new; color: red;\">W</span><span style=\"font-family: courier new; color: blue;\">='T==\"__CONNECTION\"'</span>\n\n<span style=\"font-family: courier new; color: red;\">C</span><span style=\"font-family: courier new; color: blue;\">|</span><span style=\"font-family: courier new; color: red;\">I</span><span style=\"font-family: courier new; color: blue;\">=\"bst2\"|</span><span style=\"font-family: courier new; color: red;\">N</span><span style=\"font-family: courier new; color: blue;\">=\"Bust This Order\"|</span><span style=\"font-family: courier new; color: red;\">H</span><span style=\"font-family: courier new; color: blue;\">=\"this is used to bust an order\"|</span><span style=\"font-family: courier new; color: red;\">L</span><span style=\"font-family: courier new; color: blue;\">=1|</span><span style=\"font-family: courier new; color: red;\">T</span><span style=\"font-family: courier new; color: blue;\">='panel.types==\"FEED:Orders\" || panel.types==\"FEED:Executions\"'|<span style=\"font-family: courier new; color: red;\">W</span>='qty>0 && user.canbust==\"true\"'|</span><span style=\"font-family: courier new; color: red;\">A</span><span style=\"font-family: courier new; color: blue;\">='{\"form\":{\"inputs\":[{\"label\": \"Comment\",\"var\":\n\"comment\",\"required\":true}]},\"timeout\":10000}'</span>\n\n== Outbound Instruction Type - Response to Execute Command (<span style=\"font-family: courier new; color: red;\">R</span>) ==\nRespond to a request by the relay to execute a command. ''Please see section on'' \"Inbound Instruction type - Execute Command (E)\" ''as these work in conjunction''.\n\n'''Required fields supplied by application'''\n\n<span style=\"font-family: courier new; color: red;\">I</span>: An ID uniquely identifying the command as sent from the relay's execute command (E). This ID is generated by AMI and the application must use this ID when sending a response.\n\n<span style=\"font-family: courier new; color: red;\">S</span>: The status of the command. 0 = Okay, 1 = Close dialog box, 2 = Leave dialog box open, 3 = Close dialog box and modify data\n\n<span style=\"font-family: courier new; color: red;\">M</span> (optional): A string message to send to source that requested command be run\n\n<span style=\"font-family: courier new; color: red;\">X</span> (optional): Execute AmiScript on the user session that requested command be run. See AmiScript User Guide for details\n\n'''Example'''\n\nThe below example is a potential response to the sample in the execute command example found in the \"Inbound Instruction type - Execute Command (<span style=\"font-family: courier new; color: red;\">E</span>)\" section.\n\n<span style=\"font-family: courier new; color: red;\">R</span><span style=\"font-family: courier new; color: blue;\">#13@1374353639915|</span><span style=\"font-family: courier new; color: red;\">M</span><span style=\"font-family: courier new; color: blue;\">=\"Order missing: OR-11323\"|I=\"ad5462sf55\"|</span><span style=\"font-family: courier new; color: red;\">S</span><span style=\"font-family: courier new; color: blue;\">=2</span>\n\nThe following example is a potential response to a command on an object where the quantity=300 and must be changed to 200:\n\n<span style=\"font-family: courier new; color: red;\">R</span><span style=\"font-family: courier new; color: blue;\">|I=\"2yXxPizK5oi02hp7jpgZJ5\"|</span><span style=\"font-family: courier new; color: red;\">S</span><span style=\"font-family: courier new; color: blue;\">=3|Quantity=200|</span><span style=\"font-family: courier new; color: red;\">M</span><span style=\"font-family: courier new; color: blue;\">=\"Quantity Modified\"</span>\n\n== Outbound Instruction Type - Delete an Object (<span style=\"font-family: courier new; color: red;\">D</span>) ==\nThis is a simple command for deleting objects that have not expired.\n\n'''Required fields supplied by application'''\n\n<span style=\"font-family: courier new; color: red;\">I</span>: A unique string ID for the object. Each new object must have a unique object ID (I) for scope of the running process ID (I). For deleting an object, the same object ID (I) must be supplied.\n\n<span style=\"font-family: courier new; color: red;\">T</span>: To delete an object, the object type must be supplied.\n\n'''Example'''\n\nIn the below example, an \"order\" object is deleted:\n\n<span style=\"font-family: courier new; color: red;\">D</span><span style=\"font-family: courier new; color: blue;\">|</span><span style=\"font-family: courier new; color: red;\">I</span><span style=\"font-family: courier new; color: blue;\">=\"ad5462sf55\"|</span><span style=\"font-family: courier new; color: red;\">T</span>=\"order\"</span>\n\n== Outbound Instruction Type - Exit (<span style=\"font-family: courier new; color: red;\">X</span>) ==\nNotify the relay of a clean shutdown. The relay will close the socket after receiving this message.\n\n'''Optional parameters supplied by application'''\n\nOptional parameters are used to provide metrics about the application at time of shutdown.\n\n'''Example'''\n\nIn the below example, notice not all metrics need be supplied for a particular status update.\n\n<span style=\"font-family: courier new; color: red;\">X</span><span style=\"font-family: courier new; color: blue;\">#1@1374353639915|STAT=\"GOODBYE\"</span>\n\n== Outbound Instruction Type - Pause (<span style=\"font-family: courier new; color: red;\">P</span>) ==\nUsed to create a pause in the connection for a specified period of time. This is used for testing scripts.\n\n'''Required fields supplied by application'''\n\n<span style=\"font-family: courier new; color: red;\">D</span>: Delay\n\n'''Example'''\n\nThis example pauses the application for 1 second:\n\n<span style=\"font-family: courier new; color: red;\">P</span><span style=\"font-family: courier new; color: blue;\">|</span><span style=\"font-family: courier new; color: red;\">D</span><span style=\"font-family: courier new; color: blue;\">=1000</span>\n\n== Inbound Instruction Type - Status Message (<span style=\"font-family: courier new; color: red;\">M</span>) ==\nFor each outbound message, the relay will respond with a response instruction which will notify the application of the acceptance of rejection of the message. The message will contain the sequence number of the supplied outbound message. These messages will not contain a timestamp or a sequence number.\n\n'''Parameters always supplied by relay'''\n\n<span style=\"font-family: courier new; color: red;\">S</span>: The status of the received instruction. A value of 0 indicates it was successfully processed by the relay; a value of 1 to 255 indicates an error.\n\n<span style=\"font-family: courier new; color: red;\">Q</span>: The sequence number of the instruction sent from the relay.\n\n<span style=\"font-family: courier new; color: red;\">M</span>: A human-readable message for the status.\n\n'''Examples'''\n\nIn the following example, the login message was rejected due to a missing ID.\n\n<span style=\"font-family: courier new; color: red;\">M</span><span style=\"font-family: courier new; color: blue;\">|</span><span style=\"font-family: courier new; color: red;\">Q</span><span style=\"font-family: courier new; color: blue;\">=0|</span><span style=\"font-family: courier new; color: red;\">S</span><span style=\"font-family: courier new; color: blue;\">=1|</span><span style=\"font-family: courier new; color: red;\">M</span><span style=\"font-family: courier new; color: blue;\">=\"Missing required Fields: I\"</span>\n\n== Inbound Instruction Type - Execute Command (<span style=\"font-family: courier new; color: red;\">E</span>) ==\nThis will allow users to execute commands on the application. A timestamp and sequence number will always be included.\n\n'''Parameters are always supplied by the relay'''\n\n<span style=\"font-family: courier new; color: red;\">I</span>: Unique ID as a string. This ID is generated by AMI\n\n<span style=\"font-family: courier new; color: red;\">C</span>: The ID of the command\n\n<span style=\"font-family: courier new; color: red;\">U</span>: User name of the person doing the command.\n\n'''Parameters optionally supplied by relay'''\n\n<span style=\"font-family: courier new; color: red;\">O</span>: Associated object ID. This will be supplied if the command is executed on an object.\n\n'''Examples'''\nIn the following example we can see the user is running the command \"cancel\" with the arguments \"OR-11323.\" The ID (I) of \"f123f56a\" will be unique for this process and should be included in the applications response.\n\n<span style=\"font-family: courier new; color: red;\">E</span><span style=\"font-family: courier new; color: blue;\">#872@1374353639915|</span><span style=\"font-family: courier new; color: red;\">I</span><span style=\"font-family: courier new; color: blue;\">=\"f123f56a\"|</span><span style=\"font-family: courier new; color: red;\">C</span><span style=\"font-family: courier new; color: blue;\">=\"cancel OR-11323\"|</span><span style=\"font-family: courier new; color: red;\">U</span><span style=\"font-family: courier new; color: blue;\">=\"JSmith\"</span>\n\n== Outbound Instruction Type - Alert (<span style=\"font-family: courier new; color: red;\">A</span>) ==\n'''<<span style=\"color: red;\">Deprecated but supported, use Object (O) instead</span>>'''\n\n== Outbound Instruction Type - Help (<span style=\"font-family: courier new; color: red;\">H</span>) ==\nPrints out general help\n\n= Cheat Sheet =\n'''Valid outbound types ''and required parameters (<span style=\"color: green;\">optional pre-defined parameters in green</span>)'''''\n\n* <span style=\"font-family: courier new; color: red;\">L</span> (login)\n** ''<span style=\"font-family: courier new; color: red;\">I</span> (app ID)''\n** ''<span style=\"font-family: courier new; color: green;\">O</span> <span style=\"color: green;\">(session options)</span>''\n** ''<span style=\"font-family: courier new; color: green;\">PL</span> <span style=\"color: green;\">(plugin options)</span>''\n\n* <span style=\"font-family: courier new; color: red;\">S</span> (status)\n* <span style=\"font-family: courier new; color: red;\">A</span> (alert) <span style=\"color: red;\">DEPRECATED</span>\n** ''<span style=\"font-family: courier new; color: red;\">I</span> (alert ID)''\n** ''<span style=\"font-family: courier new; color: red;\">L</span> (level)''\n** ''<span style=\"font-family: courier new; color: red;\">T</span> (alert type)''\n** ''<span style=\"font-family: courier new; color: green;\">E</span> <span style=\"color: green;\">(expires on)</span>''\n** ''<span style=\"font-family: courier new; color: green;\">U</span> <span style=\"color: green;\">(user assignment)</span>''\n\n* <span style=\"font-family: courier new; color: red;\">O</span> (object)\n** ''<span style=\"font-family: courier new; color: red;\">I</span> (object ID)''\n** ''<span style=\"font-family: courier new; color: red;\">T</span> (object Type)''\n** ''<span style=\"font-family: courier new; color: green;\">E</span> <span style=\"color: green;\">(expires on)</span>''\n\n* <span style=\"font-family: courier new; color: red;\">C</span> (command definition)\n** ''<span style=\"font-family: courier new; color: red;\">I</span> (command ID)''\n** ''<span style=\"font-family: courier new; color: green;\">N</span> <span style=\"color: green;\">(name)</span>''\n** ''<span style=\"font-family: courier new; color: green;\">W</span> <span style=\"color: green;\">(where - row level)</span>''\n** ''<span style=\"font-family: courier new; color: green;\">T</span> <span style=\"color: green;\">(where - panel level)</span>''\n** ''<span style=\"font-family: courier new; color: green;\">L</span> <span style=\"color: green;\">(permissions Level)</span>''\n** ''<span style=\"font-family: courier new; color: green;\">A</span> <span style=\"color: green;\">(form definition)</span>''\n** ''<span style=\"font-family: courier new; color: green;\">H</span> <span style=\"color: green;\">(help)</span>''\n** ''<span style=\"font-family: courier new; color: green;\">P</span> <span style=\"color: green;\">(priority)</span>''\n** ''<span style=\"font-family: courier new; color: green;\">E</span> <span style=\"color: green;\">(enabled)</span>''\n** ''<span style=\"font-family: courier new; color: green;\">F</span> <span style=\"color: green;\">(fields)</span>''\n** ''<span style=\"font-family: courier new; color: green;\">M</span> <span style=\"color: green;\">(multiple)</span>''\n** ''<span style=\"font-family: courier new; color: green;\">S</span> <span style=\"color: green;\">(style)</span>''\n** ''<span style=\"font-family: courier new; color: green;\">C</span> <span style=\"color: green;\">(condition)</span>''\n** ''<span style=\"font-family: courier new; color: green;\">X</span> <span style=\"color: green;\">(execute AmiScript)</span>''\n\n* <span style=\"font-family: courier new; color: red;\">R</span> (response to execute command) \u00a0\n** ''<span style=\"font-family: courier new; color: red;\">I</span> (Unique ID)''\n** ''<span style=\"font-family: courier new; color: red;\">S</span> (status)''\n** ''<span style=\"font-family: courier new; color: green;\">M</span> <span style=\"color: green;\">(message)</span>''\n** ''<span style=\"font-family: courier new; color: green;\">X</span> <span style=\"color: green;\">(execute AmiScript)</span>''\n\n* <span style=\"font-family: courier new; color: red;\">D</span> (delete object)\n** ''<span style=\"font-family: courier new; color: red;\">I</span> (object ID)''\n** ''<span style=\"font-family: courier new; color: red;\">T</span> (object Type)''\n\n* <span style=\"font-family: courier new; color: red;\">X</span> (exit)\n* <span style=\"font-family: courier new; color: red;\">H</span> (help)\n* <span style=\"font-family: courier new; color: red;\">P</span> (pause) only to be used in testing\n** ''<span style=\"font-family: courier new; color: red;\">D</span> (delay)''\n\n'''Valid inbound types ''and parameters always supplied <span style=\"color: green;\">(occasionally supplied parameters in green)</span>'''''\n\n* <span style=\"font-family: courier new; color: red;\">M</span> (status message)\n** ''<span style=\"font-family: courier new; color: red;\">S</span> (status)''\n** ''<span style=\"font-family: courier new; color: red;\">Q</span> (sequence number)''\n** ''<span style=\"font-family: courier new; color: green;\">M</span> <span style=\"color: green;\">(message)</span>''\n\n* <span style=\"font-family: courier new; color: red;\">E</span> (execute command)\n** ''<span style=\"font-family: courier new; color: red;\">I</span> (unique ID)''\n** ''<span style=\"font-family: courier new; color: red;\">C</span> (a string command)''\n** ''<span style=\"font-family: courier new; color: red;\">U</span> (user name)''\n** ''<span style=\"font-family: courier new; color: green;\">O</span> <span style=\"color: green;\">(associated object ID)</span>''\n\n= Data Storage (Advanced) =\n'''Data sizes Overview.''' Each parameter added to an object \u00a0and application status message has a '''3 byte overhead''', plus additional bytes, depending on the data type. Please see the table below.\n\n[[File:RTAPI.DataStorage.jpg|560x560px]]\n\n'''Keys'''. The length of a key has virtually no impact on memory. \u00a0This is because all key names are indexed. Ami Supports up to 65,536 unique key names plus data types (T=). This is a hard limit per AMI center and going beyond that will result in data loss. For example, the following 2 messages would result in 5 unique keys. Note that when key names and types (T=) are repeated then they do not count as a new unique key.\n\n<span style=\"font-family: courier new; color: red;\">O</span><span style=\"font-family: courier new; color: blue;\">|</span><span style=\"font-family: courier new; color: red;\">T</span><span style=\"font-family: courier new; color: blue;\">=\"</span><span style=\"font-family: courier new; color: blue;\">Order\"|quantity=100|symbol='ABC'|</span><span style=\"font-family: courier new; color: red;\">I</span><span style=\"font-family: courier new; color: blue;\">=\"Ord123\"</span>\n\n<span style=\"font-family: courier new; color: red;\">O</span><span style=\"font-family: courier new; color: blue;\">|</span><span style=\"font-family: courier new; color: red;\">T</span><span style=\"font-family: courier new; color: blue;\">=\"</span><span style=\"font-family: courier new; color: blue;\">\"Execution\"|quantity=50|symbol='XYZ'|price=45.3d|</span><span style=\"font-family: courier new; color: red;\">I</span><span style=\"font-family: courier new; color: blue;\">=\"exec123\"</span>\n\n<span style=\"font-family: courier new; color: red;\">O</span><span style=\"font-family: courier new; color: blue;\">|</span><span style=\"font-family: courier new; color: red;\">T</span><span style=\"font-family: courier new; color: blue;\">=\"</span><span style=\"font-family: courier new; color: blue;\">Execution\"|quantity=150|symbol='DEF'|price=13d|</span><span style=\"font-family: courier new; color: red;\">I</span><span style=\"font-family: courier new; color: blue;\">=\"exec456\"</span>\n\n= Enums (Advanced) =\n'''Clarification on Denoting Enums vs. Strings:''' Enums are surrounded using single quotes (') while strings are denoted using double quotes (\"). For example (note the quote types):\n\n<span style=\"font-family: courier new; color: blue;\">O|T=\"sample\"|my_enum='hello'|my_string=\"Hello there Mr. Jones\"</span>\n\n'''Enum Storage Methodology''': Using enums will dramatically reduce the cost of storing text that is often repeated. Instead of storing the ''actual'' value, just an index (binary number) is stored and regardless of how many times it is referenced only a single instance of the string is kept in a lookup table. The first 255 unique enum values received by the AMI central server will be indexed using a single byte index. The following 65,281 unique enum values received will be indexed using 2 bytes and the last 16,711,935 unique entries will use a 3 byte index. Be aware that the order in which unique enum values are introduced also determines the storage requirements for repeat entries.\n\n'''Enum Scope''': The scope of a particular enum is for an AmiCenter. This means that the same index will be used regardless of application sending the enum, type of message it is in or parameter it is associated with. For example, the following will result in 3 enums (presuming these are the first 3 messages then all 3 of these would be indexed using a single byte):\n\n<span style=\"font-family: courier new; color: red;\">O</span><span style=\"font-family: courier new; color: blue;\">|</span><span style=\"font-family: courier new; color: red;\">T</span><span style=\"font-family: courier new; color: blue;\">=\"Order\"|symbol='ABC'|</span><span style=\"font-family: courier new; color: red;\">I</span><span style=\"font-family: courier new; color: blue;\">=\"Ord123\"|name='DEF'</span>\n\n<span style=\"font-family: courier new; color: red;\">O</span><span style=\"font-family: courier new; color: blue;\">|</span><span style=\"font-family: courier new; color: red;\">T</span><span style=\"font-family: courier new; color: blue;\">=\"Execution\"|name='ABC'|</span><span style=\"font-family: courier new; color: red;\">I</span><span style=\"font-family: courier new; color: blue;\">=\"exec123\"</span>\n\n<span style=\"font-family: courier new; color: red;\">O</span><span style=\"font-family: courier new; color: blue;\">|</span><span style=\"font-family: courier new; color: red;\">T</span><span style=\"font-family: courier new; color: blue;\">=\"Execution\"|symbol='DEF'|Orig='ZZZ'|</span><span style=\"font-family: courier new; color: red;\">I</span><span style=\"font-family: courier new; color: blue;\">=\"exec456\"</span>\n\nNote: Exceeding 16,777,216 unique enums will cause the Center to treat remaining Enums as Strings.\n\n'''Understanding Cost:'''\n\nThe first instance of an enum has significant overhead verses a String, but following instances will usually have a highly reduced overhead. \u00a0\n{| class=\"wikitable\"\n|Description\n|Enum Cost\n\n(Best case)\n|String \u00a0Cost\n\n(Best Case)\n|Enum Cost\n\n(Worst \u00a0case)\n|String \u00a0Cost\n\n(Worst Case)\n|-\n|First entry for a string\n|19 + (length x 2)\n|4 + length\n|21 + (length x 2)\n|7 + length * 2\n|-\n|repeat entries for string\n|'''4'''\n|4 + length\n|'''6'''\n|7 + length * 2\n|}\n''Notes: (1) The length variable is the number of characters in the string. (2) Costs are in bytes.''\n\nFrom the table above you can see there is a large cost for the first entry for an enum, but additional entries have a highly reduced cost which is regardless of string size.\n\n= JSON AMI Command Invocation User Form Definition =\n\n=== Structure ===\nThe below structure demonstrates how to construct a well-formed input user form. Forms have support for input fields which can contain any number of input arguments. \u00a0Note that a form definition with neither a '''<span style=\"font-family: courier new; color: red;\">form</span>''' stanza nor a '''<span style=\"font-family: courier new; color: red;\">timeout</span>''' stanza will result in no dialog and immediate execution of the command when the user selects the command.\n\n[[File:RTAPI.JSONAMI.jpg|361x361px]]\n\n'''Example'''\n\n<span style=\"font-family: courier new; \">C|I=\"Test\"|N=\"Testing Command\"|L=3|H=\"This is a test\"|W='T==\"Test Executions\" && __USERNAME==\"jsmith\" && Quantity>100'|M=\"0-1\"|A='{\"form\":{\"inputs\":[{\"label\":\"Symbol\",\"required\":true, \"var\":\"symbol\",\"type\":\"text\"},{\"label\":\"Reason\",\"pattern\":\"[0-9a-z]+\",\"required\": true, \"var\": \"reason\",\"type\":\"textarea\"},{\"label\":\"Employee ID\",\"required\":true, \"var\":\"ID\",\"type\":\"text\"},{\"label\":\"Other\",\"required\": false, \"var\":\"other\",\"type\":\"textarea\"},{\"label\": \"Team\",\"required\": true, \"var\": \"team\",\"type\":\"select\", \"options\":[{\"value\":\"a\",\"text\":\"A\"},{\"value\":\"b\",\"text\":\"B\"},{\"value\":\"c\",\"text\":\"C\"}]},{\"label\": \"Mode\",\"required\": true, \"var\": \"mode\",\"type\":\"select\", \"options\":[{\"value\":\"emergency\",\"text\":\"Emergency\"},{\"value\":\"normal\",\"text\":\"Normal\"},{\"value\":\"urgent\",\"text\":\"Urgent\"}],\"enabled\":true}],\"buttons\":[{\"type\":\"cancel\",\"label\":\"Ignore\"},{\"type\":\"submit\",\"label\":\"Confirm\"}],\"width\":500,\"height\":500},\"timeout\":100000}'</span>\n\n\n[[File:RTAPI.JSONAMIExample.jpg|431x431px]]\n\n= Description of AMI JSON Form Definition Fields =\n{| class=\"wikitable\"\n|'''Name'''\n|'''Type'''\n|'''Description'''\n|'''Req.'''\n|-\n|<span style=\"font-family: courier new; \">form</span>\n|object\n|Object which identifies the form presented to the user\n|\n|-\n|<span style=\"font-family: courier new; \">form.inputs</span>\n|array\n|Array which lists the input widgets inside the form\n|\n|-\n|<span style=\"font-family: courier new; \">form.inputs.label</span>\n|string\n|Label displayed next to the input\n|'''X'''\n|-\n|<span style=\"font-family: courier new; \">form.inputs.var</span>\n|string\n|key associate with the value that is sent to the back end\n|'''X'''\n|-\n|<span style=\"font-family: courier new; \">form.inputs.required</span>\n|boolean\n|If true, user must supply a non-blank value, default is false\n|\n|-\n|<span style=\"font-family: courier new; \">form.inputs.pattern</span>\n|string\n|The pattern that the user-supplied value must match to\n|\n|-\n|<span style=\"font-family: courier new; \">form.inputs.type</span>\n|String\n\n(enum)\n|The type of user input, default is text. Permissible values:\n\n\u00a0 \u00a0<span style=\"font-family: courier new; \">text</span> - regular, single line text input\n\n\u00a0 \u00a0<span style=\"font-family: courier new; \">textarea</span> - multi-line text input\n\n\u00a0 \u00a0<span style=\"font-family: courier new; \">select</span> - multi option select field\n\n\u00a0 \u00a0<span style=\"font-family: courier new; \">hidden</span> - field is hidden from user\n\n\u00a0 \u00a0<span style=\"font-family: courier new; \">password</span> - hides letters\n|\n|-\n|<span style=\"font-family: courier new; \">form.inputs.value</span>\n|string\n|Reference a variable on selected record\n|\n|-\n|<span style=\"font-family: courier new; \">form.inputs.options</span>\n|array\n|List of options, required & only applicable if type=select\n|\n|-\n|<span style=\"font-family: courier new; \">form.inputs.options.value</span>\n|string\n|Value sent to backend if option is elected.\n|'''X'''\n|-\n|<span style=\"font-family: courier new; \">form.inputs.options.text</span>\n|string\n|Text displayed to user, if not supplied value is used\n|\n|-\n|<span style=\"font-family: courier new; \">form.buttons</span>\n|Array\n|Array which lists the buttons at the bottom of the form\n|\n|-\n|<span style=\"font-family: courier new; \">form.buttons.type</span>\n|string\n|The type of button. \u00a0Permissible values:\n\nsubmit - submit the form to backend\n\ncancel - cancel and close the form\n|'''X'''\n|-\n|<span style=\"font-family: courier new; \">form.buttons.label</span>\n|string\n|The text displayed on the button\n|\n|-\n|<span style=\"font-family: courier new; \">form.width</span>\n|number\n|Width of form in pixels\n|\n|-\n|<span style=\"font-family: courier new; \">form.height</span>\n|number\n|Height of form in pixels\n|\n|-\n|<span style=\"font-family: courier new; \">form.labelWidth</span>\n|number\n|Width of the labels column in pixels\n|\n|-\n|<span style=\"font-family: courier new; \">Timeout</span>\n|number\n|Max time the user will wait for a response from the backend after submitting form, in milliseconds\n|\n|}"
                    }
                ]
            },
            "394": {
                "pageid": 394,
                "ns": 0,
                "title": "Support FAQ",
                "revisions": [
                    {
                        "contentformat": "text/x-wiki",
                        "contentmodel": "wikitext",
                        "*": "= Upload Files and Contact Support From 3forge Portal =\nStep1: Log in to the account: [https://3forge.com/login.html Log in ] <br>\nStep2: '''''Click Account''''' -> '''''Help''''', where you can detail your support questions and send attachments  to the support team.<br>\nStep3: Click '''''Contact Us'''''. The support team will be notified of the support ticket you just created.<br>\n[[File:ContactSupport.png]]\n= Helpful Tricks =\n== Inserting Data from Datamodel to AMI DB ==\nIn this example, we will show how to insert data from one datasource to another by using the Datamodel. Here the Country table exists in a datasource called WORLD. We want to take this table and create a copy of it in AMI datasource. To create a copy of the Country table, we need the schema which we get using the DESCRIBE clause.\n\n<syntaxhighlight lang=\"amiscript\">\n{\n  CREATE TABLE Country AS USE  EXECUTE SELECT * FROM `Country` WHERE ${WHERE};\n  string s = SELECT SQL from DESCRIBE TABLE Country;\n  s = strReplace(s, \"CREATE TABLE\", \"CREATE PUBLIC TABLE IF NOT EXISTS\");\n  // session.log(s);\n  USE ds=\"AMI\" LIMIT = -1 EXECUTE ${s};\n  // USE ds=\"AMI\" EXECUTE DELETE FROM Country;\n  USE ds=\"AMI\" INSERT INTO Country FROM SELECT * FROM Country;\n}\n</syntaxhighlight>\n\nFirstly, create a Datamodel that is attached to the WORLD Datasource. Copy and paste the above script and run. \n\n[[File:Datamodel.png|500px|frameless|center]]\n\nThis will now give you the Country table in the AMI datasource.\n\n[[File:AMI Data Modeler.png|500px|frameless|center]]\n\n== String Template (and Procedures) ==\nFor this example we will only use the back-end. First let us create a simple table and add 4 values to it:\n\n<span style=\"font-family: courier new; color: blue;\">CREATE PUBLIC TABLE A (ID String, Price Double);</span>\n\n<span style=\"font-family: courier new; color: blue;\">INSERT INTO A VALUES (\"I001\",100),(\"I002\",200),(\"I003\",300),(\"I004\",400);</span>\n\nThis gives us the following table:\n\n<span style=\"font-family: courier new; color: blue;\">SELECT * FROM A;</span>\n{| class=\"wikitable\"\n|+<span style=\"font-family: courier new; color: blue;\">A</span>\n!<span style=\"font-family: courier new; color: blue;\">ID</span>\n<span style=\"font-family: courier new; color: blue;\">String</span>\n!<span style=\"font-family: courier new; color: blue;\">Price</span>\n<span style=\"font-family: courier new; color: blue;\">Double</span>\n|-\n|<span style=\"font-family: courier new; color: blue;\">I001</span>\n|<span style=\"font-family: courier new; color: blue;\">100</span>\n|-\n|<span style=\"font-family: courier new; color: blue;\">I002</span>\n|<span style=\"font-family: courier new; color: blue;\">200</span>\n|-\n|<span style=\"font-family: courier new; color: blue;\">I003</span>\n|<span style=\"font-family: courier new; color: blue;\">300</span>\n|-\n|<span style=\"font-family: courier new; color: blue;\">I004</span>\n|<span style=\"font-family: courier new; color: blue;\">400</span>\n|}\n\nNow let's create a string and assign A to it:\n\n<span style=\"font-family: courier new; color: blue;\">String T = \"A\";</span>\n\nTry running the following script:\n\n<span style=\"font-family: courier new; color: blue;\">SELECT * FROM ${T};</span>\n\n[[File:StringTemplateOff.png|500px|frameless|center]]\n\nThis produces an error as '''string_template=off'''. \n\nLet's set '''string_template=on'''. Note we find string_template under the '''setlocal''' command:\n\n[[File:Setlocal.png|500px|frameless|center]]\n\nTo set this to 'on' run the following:\n\n<span style=\"font-family: courier new; color: blue;\">setlocal string_template=on;</span>\n\nTry running the following script again:\n\n<span style=\"font-family: courier new; color: blue;\">SELECT * FROM ${T};</span>\n\n[[File:StringTemplateOn.png|500px|frameless|center]]\n\nThis will now output table A.\n\n\n'''Using PROCEDURES'''\n\nLet's roll back and set '''string_template=off'''. This time we will create the following PROCEDURE:\n\n<span style=\"font-family: courier new; color: blue;\">CREATE PROCEDURE testProc OFTYPE AMISCRIPT USE arguments=\"String T\" script=\"Int n = 2; Table t = SELECT * FROM ${T} LIMIT n; SELECT * FROM t;\";</span>\n\nand then call the procedure as such:\n\n<span style=\"font-family: courier new; color: blue;\">CALL testProc(T);</span>\n[[File:TestProc.png|500px|frameless|center]]\n\n\n== Copying Style from one Dashboard to Another ==\n\nTo copy a dashboard style from one dashboard to another we can use the Import/Export Style function in Style Manager. Let's take a pre-styled dashboard as such:\n\n[[File:PreStyledDashboard.png|1000px|frameless|center]]\n\nTo copy this style, enter Development mode and under ''Dashboard'' select ''Style Manager''. Select the style to copy, right click to ''Export Style'' and copy:\n\n[[File:ExportStyle.png|1000px|frameless|center]]\n\nNext, open the dashboard in which you would like to import this style. Under ''Style Manager'' select ''Import Style''. Paste the copied text - you can rename the ''id'' and ''lb'' to your preferred names (here we have named both to '''CopiedStyle'''):\n\n[[File:ImportStyle.png|1000px|frameless|center]]\n\nFinally, under '''Layout Default''', select CopiedStyle to inherit from. This will update the style of the entire dashboard to this style.\n\n[[File:InheritedStyle.png|1000px|frameless|center]]\n\n= Charts =\n== Plotting Bar Chart Side-by-Side ==\nIn this example, we will be plotting a grouped bar chart to show the GDP per capita for UK, USA, JPN and AUS between 2017 and 2020. The result looks as such:\n\n[[File:Plot Bars Side-by-Side.png|500px|frameless|center]]\n\nTo start off with, create a Datamodel with the code snippet below. Once we have a table with our data, in this case <span style=\"font-family: courier new; font-weight: bold;\">GDPPerCapita</span> table we need to prepare the dataset to plot the data. The following code snippet prepares the data by creating a table called <span style=\"font-family: courier new; font-weight: bold;\">GDPPerCapitaPlot</span>.\n\n\n<syntaxhighlight lang=\"amiscript\">\n{\n create table GDPPerCapita (Year string, UK double, USA double, JPN double, AUS double);\n insert into GDPPerCapita values (\"2017\", 45000, 58000, 42000, 50000);\n insert into GDPPerCapita values (\"2018\", 50000, 55500, 42500, 46000);\n insert into GDPPerCapita values (\"2019\", 47500, 59500, 45500, 42700);\n insert into GDPPerCapita values (\"2020\", 42500, 54700, 43500, 45700);\n \n Table t = select * from GDPPerCapita;\n List yearList = select Year from GDPPerCapita;\n List valTypesList = t.getColumnNames();\n valTypesList.remove(t.getColumnLocation(\"Year\"));\n\n create table GDPPerCapitaPlot (Year string, valType string, x double, val double);\n \n int n = 1;\n for (int i = 0; i < yearList.size(); i++) {\n    Row r = t.getRow(i);\n    string Year = r.get(\"Year\");\n    double ld = 0.0;\n    int cnt = 0;\n    for (string valType : valTypesList) {\n      double val = r.get(valType);\n      insert into GDPPerCapitaPlot values (Year, valType, n, val);\n      ld += n;\n      n += 1;\n      cnt += 1;\n    }\n    ld = ld / cnt;\n    insert into GDPPerCapitaPlot values (Year, \"label\", ld, 1.0);\n    n+=1;\n  }\n}\n</syntaxhighlight>\n\nNext, we will add a visualisation to the Datamodel on the <span style=\"font-family: courier new; font-weight: bold;\">GDPPerCapitaPlot</span> table. Choose the \u20182D Chart\u2019 \u2013 \u2018Advanced\u2019, and fill in the options as below (For \u2018Color\u2019 in \u2018MARKERS\u2019 section choose Custom_Series and add series of colours in hex code for example \u2018#E4572E,#29335C,#F3A712,#A8C686,#669BBC\u2019):\n\n[[File:EditPlot.png|500px|frameless|center]]\n\nNB. Make sure the options under ''Marker Position Override'' are filled in as shown.\n\nThis will give us the following bar plot:\n\n[[File:Midresult.png|500px|frameless|center]]\n\nTo add axis and get the final result, add a panel below the bar chart, then add a visualisation based on <span style=\"font-family: courier new; font-weight: bold;\">GDPPerCapitaPlot</span> table with the following information:\n\n[[File:AddAxis.png|500px|frameless|center]]\n\nEditing and styling the axis and the divider gives us the desired chart.\n\n== Plotting Multiple Line Graphs on Same Panel ==\nIn this example we will show how to plot multiple line graphs on the same chart panel. We will use a sample dataset that shows the GDP Growth of several countries:\n[[File:GDPGrowthData.png|1000px|frameless|center]]\n\nOutput will look as such:\n[[File:GDPGrowthGraph.png|1000px|frameless|center]]\n\nThe method we will follow is similar to the one before. Firstly, we want to be able to choose the countries we plot, so we will create a HTML panel with a 'Multiple Checkbox' field called 'variables'. Once we have this field set up, insert the following snippet into a Datamodel.\n<syntaxhighlight lang=\"amiscript\">\n{ \n  List vars = variables.getSelected();\n  String varsStr = strJoin(\",\", vars);\n  \n  CREATE TABLE PlotSeries (Year long, yVals double, var string);\n  for (string v : vars) {\n    INSERT INTO PlotSeries (Year, yVals, var) FROM SELECT Year, ${v}, \"${v}\" FROM GDPGrowth;\n  }\n}\n</syntaxhighlight>\n\nThis creates a table PlotSeries which will be used to plot the graph:\n[[File:GDPDatamodel.png|500px|frameless|center]]\n\nNext create a visualisation on the Datamodel from above using the PlotSeries table and choose '2D Chart' - 'Advanced'. We will add 'Year' on the X-axis and 'yVals' on the Y-axis. We want to group the data by the Countries so add 'vars' in the 'Group By' option. Finally, we want to have the lines for each country represented by a different colour - in the 'Line Colour' option choose 'Series' (to get predetermined colours) or 'Custom Series' (to choose custom colours) and __series_num:\n[[File:GDPWindow.png|1000px|frameless|center]]\n\n= Dynamic Aggregation on Visible Columns  =\nIn this example we go through the steps showing how a user can use visible columns to drive aggregate columns. This is done by getting the visible columns and using the column names to drive a new query whenever the columns are hidden or shown.\n\nWe start by creating a datamodel named '''Orders'''  that produces the '''Orders''' table with the following data:\n\n<syntaxhighlight lang=\"amiscript\">\n{\n  CREATE TABLE Orders(ID int, Country string, Size long, Price double, OrdType string, Side string);\n  INSERT INTO Orders Values(1,\"HK\",5000,3.00,\"1\",\"B\"), (2,\"HK\",3000,5.00,\"2\",\"B\"), (3,\"CH\",2000,6.00,\"1\",\"B\"), (4,\"CH\",1500,8.00,\"2\",\"S\");\n}\n</syntaxhighlight>\n\nNext, create a blender named '''OrdersGroupBy''' on the above datamodel. We need to add and access an argument called ''groupByClause'' which will be passed into this blender and will store the column names that drive the aggregation. This argument can be accessed from the '''wheres''' map passed into onProcess(WHERE,wheres,rtevents).\n\nOur argument '''groupByClause''' can be accessed like so:\n\n<syntaxhighlight lang=\"amiscript\">\nString groupByClause = wheres.get(\"groupByClause\");\n</syntaxhighlight>\n\n\nUse the following code to produce a new table that will have some aggregate columns, in this example we have a column that sums all of the sizes and a column that gives the average price.\n\nWe want to add some conditions using an if statement, this is where we make use of the \u2018groupByClause\u2019 we defined previously. Since this argument will contain the column names that drive the aggregation we want to make sure that a table is still created even if the argument is empty. We do this by adding a condition to create a table from our existing columns if no new column is provided.\n\nIf we are given a new column name we add this to a select statement. We add the column name both in the select portion and then at the end to group the results of the query.\n\n<syntaxhighlight lang=\"amiscript\">\n{\n  CREATE TABLE Orders AS SELECT * FROM Orders WHERE ${WHERE};\n  \n  String groupByClause = wheres.get(\"groupByClause\");\n  \n   if (groupByClause == null || groupByClause == \"\") {\n    groupByClause = \"Country, OrdType, Side\";\n    create table OrdersGroupBy as select ${groupByClause}, sum(Size), avg(Price) from Orders group by ${groupByClause};\n    // create table OrdersGroupBy as select sum(Size), avg(Price) from Orders;\n  }\n  else {\n    create table OrdersGroupBy as select ${groupByClause}, sum(Size), avg(Price) from Orders group by ${groupByClause}; \n  }\n}\n</syntaxhighlight>\n\nNext, create a visualization for OrdersGroupBy table.\n\n[[File:OrdersGroupByVisualisation.png|1000px|frameless|center]]\n\nNow, go to the ''AmiScript Callbacks'' for the OrdersGroupBy table just created. Within the ''onColumnsArranged()'' paste the following code. This code first gets a list of all of the visible columns. Then we remove our aggregate columns, in this case we want to remove the sum(Size) and average(Price) columns, these are removed as they are not used in the aggregation but instead just show the results of the aggregation. We add each visible column name to a map m. We process our blender ''OrdersGroupBy'' and provide our map as the parameter, this map will be parsed as the ''wheres'' map that we use in the blender.\n\n[[File:DynamicAggVisibleColumnCallback.png|1000px|frameless|center]]\n\n<syntaxhighlight lang=\"amiscript\">\n{\n  list visCol = this.getVisibleColumns();\n  list visColProcessed = new List();\n  for (string c : visCol) {\n    if (!strStartsWith(c, \"avg\", true) && !strStartsWith(c, \"sum\", true)) {\n      c = strReplace(c, \" \", \"\");\n      visColProcessed.add(c);\n    }\n  }\n  \n  string groupByClauseStr = strJoin(\", \", visColProcessed);\n  // session.alert(groupByClauseStr);\n  \n  Map m = new map();\n  m.put(\"groupByClause\", groupByClauseStr); \n  OrdersGroupBy.process(m);\n}\n</syntaxhighlight>\n\nBefore we perform any aggregates, we need to edit the '''OrdersGroupBy''' blender so that when the ''groupByClause'' is null or empty we only see the sum(Size) and avg(Price) columns. Our blender should now have the following code snippet:\n\n<syntaxhighlight lang=\"amiscript\">\n{\n  CREATE TABLE Orders AS SELECT * FROM Orders WHERE ${WHERE};\n  \n  String groupByClause = wheres.get(\"groupByClause\");\n  \n   if (groupByClause == null || groupByClause == \"\") {\n    // groupByClause = \"Country, OrdType, Side\";\n    // create table OrdersGroupBy as select ${groupByClause}, sum(Size), avg(Price) from Orders group by ${groupByClause};\n    create table OrdersGroupBy as select sum(Size), avg(Price) from Orders;\n  }\n  else {\n    create table OrdersGroupBy as select ${groupByClause}, sum(Size), avg(Price) from Orders group by ${groupByClause}; \n  }\n}\n</syntaxhighlight>\n\n\nWe are now ready to hide/show columns and watch the aggregation happen. You can use the ''Hide This Column...'' option or ''Arrange Columns...'' option to hide/show columns.\n\n= Autofill Text Fields Using a Lookup =\n\nIn this example we will show how to use the '''Country''' field to populate the '''Country Code''', '''Continent''' and '''Population''' field. We will use the Country table for this example.\n[[File:Lookup.png|1000px|frameless|center]]\n\nFirst create a HTML Panel and add a 'Text' field attached to the Country Datamodel and choose '''Name''' (here the Name column contains Country names) as the display value:\n\n[[File:EditTextField.png|500px|frameless|center]]\n\nUse the same method to add the other fields of interest (ie. '''Country Code''', '''Continent''', '''Population'''). Next we will add a button called 'Lookup' with the following script - when this button is clicked the remaining fields will populate based on the chosen '''Country'''. \n\n<syntaxhighlight lang=\"amiscript\">\nTable dbTable = layout.getDatamodel(\"Country\").getData().get(\"Country\");\nstring countryName = countryname.getValue();\nTable dbTableInfo = dbTable.query(\"select Continent, Code, Population from this where Name==\\\"${countryName}\\\" limit 1\");\nstring Continent = dbTableInfo.getRow(0).get(\"Continent\");\nstring Code = dbTableInfo.getRow(0).get(\"Code\");\nstring Population = dbTableInfo.getRow(0).get(\"Population\");\n\nif (strIs(Continent)) {\n  continentname.setValue(Continent);\n}\nelse {\n  continentname.setValue(\"\");\n}\n\nif (strIs(Code)) {\n  countrycode.setValue(Code);\n}\nelse {\n  countrycode.setValue(\"\");\n}\n\nif (strIs(Population)) {\n  population.setValue(Population);\n}\nelse {\n  population.setValue(\"\");\n}\n</syntaxhighlight>\n\nThis scripts uses a function called '''query''' to get the values corresponding to the chosen '''Country'''. When searching for a '''Country''' we also get a dropdown list of the Countries:\n[[File:Dropdown.png|250px|frameless|center]]\n\nPerforming a lookup on Belgium, we can see the other fields are populated:\n[[File:Belguim.png|1000px|frameless|center]]\n\n= Filtering in Select Fields =\nIn this example we will use the Country table and create 3 select fields. The output of the first select field will be used in the WHERE clause to give the filtered selection of data which acts as the input of the second select field.\n\n[[File:SelectField.png|1000px|frameless|center]]\n\nFirst we will create 3 select fields and call them '''Continent''', '''Region''' and '''Country Name'''. Then create a blender on the Country datamodel called CountryFiltered and insert the following snippet:\n\n<syntaxhighlight lang=\"amiscript\">\n{\n  CREATE TABLE Region AS SELECT Region FROM Country WHERE Continent==continent.getValue();\n  CREATE TABLE CountryInRegion AS SELECT Name FROM Country WHERE Region==region.getValue() and Continent==continent.getValue();\n}\n</syntaxhighlight>\n\n[[File:CountryFilteredDM.png|500px|frameless|center]]\n\nHere the continent.getValue() and region.getValue() gets the value in the select field named '''Continent''' and '''Region''' respectively.\n\nNext we will set up the three fields as shown below. We get the relevant data from the '''CountryFiltered''' datamodel.\n[[File:ContinentSelectField.png|500px|frameless|center]] \n\n\n[[File:RegionSelectField.png|500px|frameless|center]] \n\n\n[[File:CountryNameSelectField.png|500px|frameless|center]]\n\nFinally, in order for the select field options to update each time we select a different Continent or Region, we will need to reprocess the datamodel. Thus for the '''Continent''' and '''Region''' select field, add the following AMI Script:\n<syntaxhighlight lang=\"amiscript\">\nlayout.getDatamodel(\"CountryFiltered\").reprocess();\n</syntaxhighlight>\n\n= Transpose Tables =\n\nIn this example we will show how to transpose the data in a table from rows to columns in the front end and the backend. \n\n=== Frontend Datamodel ===\nFirst let's create a dummy table from the frontend datamodel:\n\n<syntaxhighlight lang=\"amiscript\">\n{\n  USE ds=\"AMI\" EXECUTE CREATE PUBLIC TABLE HouseBills (House string, BillType string, Amount float);\n  USE ds=\"AMI\" INSERT INTO HouseBills VALUES (\"House 1\", \"Gas\", 5.2), (\"House 1\", \"Electricity\", 15.9), (\"House 1\", \"Water\", 10.3), \n                                             (\"House 2\", \"Gas\", 4.5), (\"House 2\", \"Electricity\", 12.4), (\"House 2\", \"CouncilTax\", 20), (\"House 2\", \"Water\", 11), \n                                             (\"House 3\", \"Gas\", 8.7), (\"House 3\", \"Electricity\", 22.5), (\"House 3\", \"CouncilTax\", 30), \n                                             (\"House 4\", \"Gas\", 6.9), (\"House 4\", \"Electricity\", 20.1), (\"House 4\", \"CouncilTax\", 25.5), (\"House 4\", \"Water\", 11.8);\n}\n</syntaxhighlight>\n\nThis gives us the following table:\n[[File:HouseBillTable.png|500px|frameless|center]]\n\nNow we want to transpose this table so that the column headers are the different bill types and the rows contain the amount for each house. Thus since there are 4 houses, we will get a dataset with 4 rows. By using the following script we can generate our desired output:\n\n<syntaxhighlight lang=\"amiscript\">\n{\n  CREATE TABLE HouseBills AS USE  EXECUTE SELECT House, BillType, Amount FROM HouseBills WHERE ${WHERE};\n  list HouseName = select House from HouseBills group by House;\n  list billType = select BillType from HouseBills group by BillType;\n  \n  string columnHeaders = \"House, \" + strJoin(\", \", billType);\n  string columnNameSchema = \"House string, \" + strJoin(\" float, \", billType) + \" float\";\n  \n  create table TransposeTable (${columnNameSchema});\n  \n  for (int i = 0; i < HouseName.size(); i++) {\n    string insertVals = \"\";\n    string house = HouseName.get(i);\n    insertVals += \"\\\"${house}\\\"\";\n    \n    for (int j = 0; j < billType.size(); j++) {\n      string bill = billType.get(j);\n      float amt = select Amount from HouseBills where House == \"${HouseName.get(i)}\" and BillType == \"${billType.get(j)}\";\n      insertVals += \", \" + (amt != null ? amt : \"null\");\n    }\n    \n    insert into TransposeTable (${columnHeaders}) values (${insertVals});\n  }\n}\n</syntaxhighlight>\n\nWe first create a datamodel on the HouseBills table that is stored in AMI. Next we extract the distinct houses names and bill types, and store it in a list named '''HouseName''' and '''billType''' respectively. \n\nWe will need to create a schema for the new transposed table - to do this we use unique list containing the bill type and perform a strJoin by adding the data type of those columns (in this case we assign their column type as a float). So the '''columnNameSchema''' string is:\n\n<syntaxhighlight lang=\"amiscript\">House string, Gas float, Electricity float, Water float, CouncilTax float</syntaxhighlight>\n\nThis can now be used to create the '''TransposeTable'''.\n\nFinally we want to get the amount corresponding to each house and bill type so we perform a for-loop that extracts the required amount and appends it to the string '''insertVals'''. Note if the amount for a particular bill type doesn't exist then we will assign a null value. Once we have the string of values to insert in the correct format we can insert it into the transpose table. We get the final result as such:\n[[File:TransposeTable.png|500px|frameless|center]]\n\n\n=== Backend Using Procedures ===\nWe can use the same script as above with a few minor changes to transpose data in the backend. The main difference will come from escaping characters:\n\n <span style=\"font-family: courier new; color: blue;\">CREATE PROCEDURE TransposeProcedure OFTYPE AMISCRIPT USE </span>\n  \n   <span style=\"font-family: courier new; color: blue;\">arguments=\"string T\" </span>\n  \n   <span style=\"font-family: courier new; color: blue;\">script=\"list HouseName = select House from ${T} group by House; \n           <span style=\"font-family: courier new; color: blue;\">list billType = select BillType from ${T} group by BillType; \n           \n           <span style=\"font-family: courier new; color: blue;\">string columnHeaders = \\\"House, \\\" + strJoin(\\\", \\\", billType); \n           <span style=\"font-family: courier new; color: blue;\">string columnNameSchema = \\\"House string, \\\" + strJoin(\\\" float, \\\", billType) + \\\" float\\\"; \n           \n           <span style=\"font-family: courier new; color: blue;\">create table TransposeTable (${columnNameSchema}); \n           \n           <span style=\"font-family: courier new; color: blue;\">for (int i = 0; i < HouseName.size(); i++) {\n               <span style=\"font-family: courier new; color: blue;\">string insertVals = \\\"\\\"; \n               <span style=\"font-family: courier new; color: blue;\">string house = HouseName.get(i); \n               <span style=\"font-family: courier new; color: blue;\">insertVals += \\\" \\\\\\\"${house}\\\\\\\" \\\"; \n               \n               <span style=\"font-family: courier new; color: blue;\">for (int j = 0; j < billType.size(); j++) {\n                   <span style=\"font-family: courier new; color: blue;\">string bill = billType.get(j); \n                   <span style=\"font-family: courier new; color: blue;\">float amt = select Amount from ${T} where House == \\\"${HouseName.get(i)}\\\" and BillType == \\\"${billType.get(j)}\\\"; \n                   <span style=\"font-family: courier new; color: blue;\">insertVals += \\\", \\\" + (amt != null ? amt : \\\"null\\\"); \n               <span style=\"font-family: courier new; color: blue;\">}\n               <span style=\"font-family: courier new; color: blue;\">insert into TransposeTable (${columnHeaders}) values (${insertVals});\n           <span style=\"font-family: courier new; color: blue;\">}\n           <span style=\"font-family: courier new; color: blue;\">select * from TransposeTable;\"</span>\n\nNB: for readability purposes the above has new lines and tabs - when writing the procedure ensure that it flows and there are no line breaks:\n\n CREATE PROCEDURE TransposeProcedure OFTYPE AMISCRIPT USE arguments=\"string T\" script=\"list HouseName = select House from ${T} group by House; list billType = select BillType from ${T} group by BillType; string columnHeaders = \\\"House, \\\" + strJoin(\\\", \\\", billType); string columnNameSchema = \\\"House string, \\\" + strJoin(\\\" float, \\\", billType) + \\\" float\\\"; create table TransposeTable (${columnNameSchema}); for (int i = 0; i < HouseName.size(); i++) {string insertVals = \\\"\\\"; string house = HouseName.get(i); insertVals += \\\" \\\\\\\"${house}\\\\\\\" \\\"; for (int j = 0; j < billType.size(); j++) {string bill = billType.get(j); float amt = select Amount from ${T} where House == \\\"${HouseName.get(i)}\\\" and BillType == \\\"${billType.get(j)}\\\"; insertVals += \\\", \\\" + (amt != null ? amt : \\\"null\\\"); } insert into TransposeTable (${columnHeaders}) values (${insertVals});} select * from TransposeTable;\"\n\n\n[[File:BackendTransposeTable.png|1000px|frameless|center]]\n\n= Snapping/Unsnapping Dividers =\n\nIn this example we discuss how to get different charts popping up depending on the data filtering. We will again use the Country table to showcase the example.\n\nFirst lets create a window with 4 panels as such:\n[[File:SnappingDividers.png|1000px|frameless|center]]\n\nThe left panel contains two '''Text''' field - one for '''Continent''' and other for '''Region'''. The '''Continent''' text field will display the values in the Continent column in the Country table from the Country datamodel:\n[[File:ContinentTextField.png|500px|frameless|center]]\n\nNext, let's create a blender on the Country Datamodel called '''CountryFiltered''' which will contain the following script:\n<syntaxhighlight lang=\"amiscript\">\n{\n  CREATE TABLE RegionInContinent AS SELECT Region, sum(Population) as TotalPopulation FROM Country WHERE Continent==continent.getValue() group by Region;\n  CREATE TABLE CountryInContinentRegion AS SELECT Name, Population FROM Country WHERE Continent==continent.getValue() and Region==region.getValue();\n}\n</syntaxhighlight>\n\nwhere <span style=\"font-family: courier new; font-weight: bold;\">continent.getValue()</span> and <span style=\"font-family: courier new; font-weight: bold;\">region.getValue()</span> get the values from the Continent and Region text fields respectively.\n\nThen the '''Region''' text field will display the values in the Region column in the RegionInContinent table in the CountryFiltered datamodel:\n[[File:RegionTextField.png|500px|frameless|center]]\n\nNow that the left panel is done, we will split the right panel into three and create three '''V Bar''' charts. The top most bar chart will plot '''Continent vs. Population''' from the Country table:\n[[File:SummaryBarChart.png|500px|frameless|center]]\n\nThe middle bar chart will plot '''Region vs. TotalPopulation''' from the RegionInContinent table:\n[[File:RegionBarChart.png|500px|frameless|center]]\n\nSimilarly, the bottom bar chart will plot '''Name vs. Population''' from the CountryInContinentRegion table.\n\nNow that we have the panels completed, we will show how to maximise the different bar charts depending on the filtering of the text fields. We want the logic as follows:\n* if the '''Continent''' text field is empty we should have the '''Continent vs. Population''' plot\n* if the '''Continent''' text field has a value (and '''Region''' text field is empty) we should have the '''Region vs. TotalPopulation''' plot where the Regions are in the chosen Continent\n* if the '''Region''' text field has a value we should have the '''Country Name vs. Population''' plot\n\nTo get this we need to add the following script to the '''Continent''' text field:\n<syntaxhighlight lang=\"amiscript\">\nif (continent.getValue() == \"\") {\n  SummaryDivider.unsnap();\n  SummaryDivider.setSnapDirection(\"Bottom\");\n  SummaryDivider.snap();\n}\nelse {\n  SummaryDivider.unsnap();\n  SummaryDivider.setSnapDirection(\"Top\");\n  SummaryDivider.snap();\n  RegionDivider.setSnapDirection(\"Bottom\");\n  RegionDivider.snap();\n}\n\nlayout.getDatamodel(\"CountryFiltered\").reprocess();\n</syntaxhighlight>\n\nand the following script to the '''Region''' text field:\n\n<syntaxhighlight lang=\"amiscript\">\nif (region.getValue() == \"\") {\n  RegionDivider.unsnap();\n  RegionDivider.setSnapDirection(\"Bottom\");\n  RegionDivider.snap();\n}\nelse {\n  RegionDivider.unsnap();\n  RegionDivider.setSnapDirection(\"Top\");\n  RegionDivider.snap();\n}\n\nlayout.getDatamodel(\"CountryFiltered\").reprocess();\n</syntaxhighlight>\n\nwhere the '''SummaryDivider''' is the divider between '''Continent vs. Population''' plot and '''Region vs. TotalPopulation''' plot, and '''RegionDivider''' is the divider between '''Region vs. TotalPopulation''' plot and '''Name vs. Population''' plot.\n\n\n[[File:Snap Continent.png|500px|frameless]]\n[[File:Snap Region.png|500px|frameless]]\n[[File:Snap Country Name.png|500px|frameless]]\n\n= Transferring Historical Data from one Center to a Historical Center =\n\nIn this example we will write a simple procedure to transfer historical data from one center to another. \n\nIn our working center we will use a table with the following schema;\n\n<syntaxhighlight lang=\"amiscript\">\nCREATE PUBLIC TABLE DataTable (AccountID String, Price double, Quantity Double, Date Long, D long);\n</syntaxhighlight>\n\nSimilarly, in the center with historical data we have a table with the same name but a different schema:\n\n<syntaxhighlight lang=\"amiscript\">\nCREATE PUBLIC TABLE DataTable (AccountID String, Price double, Quantity Double, Date Long, SourceD Long, HistDate Long);\n</syntaxhighlight>\n\nNB: D is an auto-generated incrementing unique id for the row which unique across all tables (see Reserved Columns on Public Tables - https://docs.3forge.com/mediawiki/AMI_Realtime_Database#Reserved_columns_on_public_Tables).\n\nNext we will use the below procedure to transfer the historical data. The arguments for the procedure are as follows:\n* tableName - this is the table you will be transferring data from and to (ie. DataTable)\n* histCenter - this is the name of the datasource where the historical data will sent to\n* whereClause - argument that can be used to get the data you want to transfer (ie. you may want to send across data ''where Date == 20220101'')\n* batchSize - argument to specify the number of data rows to send across in each go\n\nNote that this procedure uses column D to decide which data to send.\n\n <span style=\"font-family: courier new; color: blue;\"> CREATE PROCEDURE MoveRowsToHist OFTYPE AMISCRIPT USE </span>\n  \n   <span style=\"font-family: courier new; color: blue;\"> arguments=\"string tableName, string histCenter, string whereClause, int batchSize\" </span>\n  \n   <span style=\"font-family: courier new; color: blue;\"> script=\"long histDate = formatDate(timestamp(), \\\"yyyyMMdd\\\", \\\"UTC\\\");\n           <span style=\"font-family: courier new; color: blue;\"> long srcTblDmax = select max(D) from ${tableName} where ${whereClause};\n           <span style=\"font-family: courier new; color: blue;\"> srcTblDmax = srcTblDmax != null ? srcTblDmax : 0L;\n           <span style=\"font-family: courier new; color: blue;\"> int destTblDmax = use ds=${histCenter} execute select max(SourceD) from ${tableName} where ${whereClause} AND HistDate == ${histDate};\n           <span style=\"font-family: courier new; color: blue;\"> destTblDmax = destTblDmax != null ? destTblDmax : 0;\n           \n           <span style=\"font-family: courier new; color: blue;\"> while (srcTblDmax > destTblDmax) {\n               <span style=\"font-family: courier new; color: blue;\"> use ds=${histCenter} insert into ${tableName} from select * except(D), D as SourceD, ${histDate} as HistDate from ${tableName} \n               <span style=\"font-family: courier new; color: blue;\">     where ${whereClause} AND D > destTblDmax limit batchSize order by SourceD;\n               <span style=\"font-family: courier new; color: blue;\"> destTblDmax = use ds=${histCenter} execute select max(SourceD) from ${tableName} where ${whereClause} AND HistDate == ${histDate};\n           <span style=\"font-family: courier new; color: blue;\">}\"\n\n\n CREATE PROCEDURE MoveRowsToHist OFTYPE AMISCRIPT USE arguments=\"string tableName, string histCenter, string whereClause, int batchSize\" script=\"long histDate = formatDate(timestamp(), \\\"yyyyMMdd\\\", \\\"UTC\\\"); long srcTblDmax = select max(D) from ${tableName} where ${whereClause}; srcTblDmax = srcTblDmax != null ? srcTblDmax : 0L; int destTblDmax = use ds=${histCenter} execute select max(SourceD) from ${tableName} where ${whereClause} AND HistDate == ${histDate}; destTblDmax = destTblDmax != null ? destTblDmax : 0; while (srcTblDmax > destTblDmax) {use ds=${histCenter} insert into ${tableName} from select * except(D), D as SourceD, ${histDate} as HistDate from ${tableName} where ${whereClause} AND D > destTblDmax limit batchSize order by SourceD; destTblDmax = use ds=${histCenter} execute select max(SourceD) from ${tableName} where ${whereClause} AND HistDate == ${histDate};}\"\n\n\n= Create Table to Display Values from Particular Row =\n\nIn this example we will show how to display the row values from a table in another table. This will be the end result:\n\n[[File:DisplayTable.png|1000px|frameless|center]]\n\nFirstly, we will create a table visualisation on the Country table (or table or your choice). \n\nNext create a datamodel with the display table:\n\n<syntaxhighlight lang=\"amiscript\">\n{\n  create table RowTable (ColumnName String, Value String);\n}\n</syntaxhighlight>\n\nNext we want to create a callback such that every time we click on a values in a row it will generate the display table (table in the right panel above). To do this we will use the ''onCellClicked(column,val,rowval)'' callback under amiscript callbacks for the table panel. The following script will get the row values and the corresponding column names and save it in the table.\n\n<syntaxhighlight lang=\"amiscript\">\nTable RowValTable = new Table(\"RowValTable\",\"ColumnName String, Value String\");\nfor (string c : rowvals.getKeys()) {  \n  string elem = rowvals.get(c);\n  if (!strStartsWith(c, \"!\", true)) {\n      RowValTable.addRow(c, elem);\n    }\n}\n\nmap m = new Map();\nm.put(\"RowValTable\", RowValTable);\ndisplayTable.processSync(m);\n</syntaxhighlight>\n\nEnable the ''displayTable'' datamodel from the Variable Tree on the right so that the script knows what datamodel to process:\n[[File:VariableTreeEnable.png|1000px|frameless|center]]\n\nNow we need to make the columns clickable so choose one column (or any number) and under edit column turn on clickable. \n\nTest this and then create a visualisation on the display table in the right panel. Now to have the datamodel containing the display table to update with the row values from the main table, we will update the displayTable code snippet to:\n\n<syntaxhighlight lang=\"amiscript\">\n{\n  // create table RowTable (ColumnName String, Value String);\n  Table RowValTable = wheres.get(\"RowValTable\");\n  create table RowTable as select * from RowValTable;\n}\n</syntaxhighlight>\n\nNB. when you test this you may get a runtime error of unknown table; you can ignore this error.\n\nFinally, clicking on an clickable cell in the Country table will automatically populate the display table.\n\n= Duplicate Panels Programmatically =\n\nIn this example we will show how to duplicate any panel programmatically instead of importing/exporting the panel manually.\n\nLet's first create panel that we want to duplicate; here we have a window with the City table on the left and Country table on the right. We've named the left panel with the City table as '''CityWorld''' and the right panel with the Country table as '''CountryWorld'''. There is also a divider between the two tables which we have named '''CityCountryDiv''' (to rename this go to green cog of the Divider -> Settings -> PanelID). \n\n[[File:CityCountryPanel.png|1000px|center]]\n\nNote that this divider is how we would access both the left and right panel.\n\n[[File:CityCountryDiv.png|1000px|frameless|center]]\n\nLet's create a HTML Panel with a button that duplicates this window when clicked on. We'll call this button '''Duplicate Panel''': \n\n[[File:DuplicatePanelButton.png|1000px|thumb|center]]\n\nAdd the following snippet of code in the Ami Script tab of the button - this code exports the config of the named panel and re-imports it as a new window:\n\n<syntaxhighlight lang=\"amiscript\">\n\nMap config = layout.getPanel(\"CityCountryDiv\").exportConfig();\nsession.importWindow(\"New Window\",config);\n\n</syntaxhighlight>\n\nExit the development mode and click on the ''Duplicate Panel'' button - this now generates a new window called '''New Window''' containing the duplicate of the panel.\n\nNB: If you only wish to duplicate the CityWorld panel, replace <span style=\"font-family: courier new; font-weight: bold;\">getPanel(\"CityCountryDiv\")</span> with <span style=\"font-family: courier new; font-weight: bold;\">getPanel(\"CityWorld\")</span>.\n\n= Flashing Cells =\nTo make table cell flash in response to cell value changing, right click on the table -> style<br>\n[[File:Cell flash1.png]]<br>\n\nIn the CELL FLASH section, you can configure the ''flash up/down color'' and ''flash Duration''.<br>\n[[File:Cellflash2.png]]<br>\n\nNote:<br>\n*The flash Duration is in the unit of milliseconds.\n*Flash up color is the color the cell changes to when the cell value goes up.\n*Flash down color is the color the cell changes to when the cell value goes down.\n\n= Apply row-wise filters based on a specific column = \noftentimes, the user might want to only show specific rows and hide the other rows based on a specific column. The following example demonstrates how we can apply row-wise filter based on a specific column in a real time table, as well as how to show summary of a given table and how to align two tables in the panel programmatically.\n\n1.Creating the demo table<br>\nFor this demonstration, we need to create a real time table with the following schema and data.\n\n<syntaxhighlight lang=\"sql\">\ncreate public table RT_table(System string, Symbol string, Quantity integer);\n\ninsert into RT_table values(\"sys1\",\"TSLA\",20),(\"sys2\",\"AAPL\",30),(\"sys3\",\"AAPL\",30),(\"sys1\",\"TEST\",60),\n\t\t\t   (\"sys2\",\"AAPL\",10),(\"sys3\",\"TEST\",80),(\"sys3\",\"TSLA\",80),(\"sys2\",\"TSLA\",45);\n</syntaxhighlight>\n[[File:Table.png]]\n\n== Set visible columns ==\nSuppose we only want to visualize the columns \"System\" and \"Quantity\", hiding column \"Symbol\". We can do this via ami script, using the ami script function setVisibleColumns() against your real time table panel. To achieve this, one solution is to create a button such that when the user clicks the button, the setVisibleColumns() function will be triggered. If we double click the button, go to the tab Ami Script, and enter the following command:\n\n<syntaxhighlight lang=\"sql\">\nlist VisibleCols = new list(\"Quantity\",\"System\");\naggPnl.setVisibleColumns(VisibleCols);\n</syntaxhighlight>\n\n[[File:Button.png]]\n<br>\nInside the function setVisibleColumns(), we can specify the names of the columns we want to make visible and pass in this as a list. Once we click the button, now the real time table only has columns \"System\" and \"Quantity\".\n\n[[File:Hiding column.png]]\n\n2. Apply row-wise filters to rows where '''Symbol==\"TEST\"'''<br> \nSuppose we want to aggregate on the table excluding the rows where Symbol==\"TEST\", here is how we can configure this using ami script to programmatically set up the filter. If we go to our panel, and right click on the central button, select AmiScript Callbacks\n[[File:OnColumnArrangedCallback.png]]\n\nGo to the tab onColumnsArranged(), and enter the following command:\n\n<syntaxhighlight lang=\"java\">\nif (visiblePnl.getVisibleColumns().contains(\"System\") && !visiblePnl.getVisibleColumns().contains(\"Symbol\")) //if visible cols contain system but not symbol\n{\n  visiblePnl.setCurrentWhere(\"\\\"Symbol\\\"!=\\\"TEST\\\"\")//anything whose symbol is not equal to TEST will be displayed, i.e. set filter to filter out rows where symbol==\"TEST\"\n}\nelse\n{\n  visiblePnl.setCurrentWhere(\"true\"); //display everything\n}\n</syntaxhighlight>\n\nNow if I hit the submit button, everytime there is a change in the column arrangement, the '''onColumnsArranged()''' callback gets fired. In this case, all the rows whose symbol==\"TEST\" will not be displayed.\n\n[[File:AfterSettingUpCallback.png]]\n\n3. Aggregate on the visible columns and rows <br>\nOnce the filters are properly set up, we might also want to do a real-time aggregate table on this. Let's open up a new panel and right click on the central button, select '''create realtime table/visualization''' and choose '''Aggregate Table''' widget against ''' aggPnl''' (which is the panel after we apply row & column-wise filters)\n[[File:Setup1.jpg]]\n[[File:Step2.jpg]]\n\nNext fill in the fields to specify on what columns you want to do aggregation on. Suppose we want to group by only on '''System''' and return the '''sum(Quantity)''', then we can configure the table like this: <br>\n[[File:Config1.jpg]]\n\nThe final window that consists both panels may look something like this:<br>\n[[File:Result.jpg]]\n\n4.Show summary of table <br> \nWe may also want to show a summary of the table. The way to do this is to right click the central button on the table you want to show summary of, and go to '''settings'''. Then scroll all the way down and enable '''summary of rows'''<br>\n[[File:Show summary1.jpg]]\n[[File:Enable summary.jpg]] <br>\n\nOnce enabled, now we can select the rows we want to show summary of and right click -> '''Summarize selected''' <br>\n[[File:Summarize selected.jpg]]\n[[File:Summary result.jpg]]\n\n== Align the tables in the panels ==\nSuppose we want to align the following two tables that are situated in two separate panels.<br>\n[[File:AlignTableCase.jpg]]<br>\nFirst, we need to make sure that the two tables must have identical column headers. Let's modify the second column header name '''Total Quantity''' to '''Quantity''' to match the corresponding column header name in table 1.\nTo do this, let's right click on the second column in table 2 and select '''edit column'''\n[[File:Edit column.jpg]] \n<br>\nGo to the '''Column Header''', and modify '''Title''' field<br>\n[[File:ChangeName1.jpg]]\n[[File:ChangeName2.jpg]]\n\n\nRight click on table1, go to '''AmiScript Callbacks''' and then go to '''OnColumnSized''', enter the following amiscripts and hit '''submit'''<br>\n<syntaxhighlight lang=\"sql\">\nlist sourceCols = aggPnl.getVisibleColumns();\nfor (String col: sourceCols)\n{\n  summaryPnl.getColumn(col).setWidth(aggPnl.getColumn(col).getWidth());\n}\n</syntaxhighlight>\n[[File:S1.jpg]]\n[[File:Amiscropt.jpg]]\n\nNow the two tables are fully aligned. You could drag either one of the tables however you want, you will find both tables will always stay aligned and in sync.\n\n[[File:AlignRes.jpg]]\n\n= Include and Apply New Style To Your Dashboard = \nAMI has several layout styles you could choose from. These layout styles are encoded in JSON format files and are placed in amione/data/styles directory. In this example, we have a stylesheet called '''DARKMATTER.amistyle.json''' and show how to include it in AMI.<br>\n1.Navigate to the AMI installation path and place the attached JSON file inside the styles directory (amione/data/styles) <br>\n[[File:Dir1.jpg]]\n<br>\n2.Navigate to /amione/config and add the following inside local.properties file\n<pre>\nami.style.files=data/styles/*amistyle.json\n</pre>\n[[File:L2.jpg]]\n[[File:L3.png]]\n\n3.Restart AMI \nshut down the current AMI instance and restart\n<br>\n\n4.Log in and open Dashboard > Style Manager<br>\n[[File:L31.jpg]]\n<br>\n\n5.Select Layout Default from tree and Dark Matter from the dropdown list \nYour dashboard should now pick up the styles from the Dark Matter stylesheet.<br>\n[[File:Layout.png]]\n[[File:Like.jpg]]\n\n= Encrypting passwords in access.txt = \n3forge recommends using the access.txt Authentication (AmiAuthenticatorFileBacked\nAuthentication Plugin) only for demo purposes, for production environments we recommend\nimplementing a AmiAuthenticator Plugin.\nBy default, AMI does not encrypt passwords in the access.txt. This document details the steps\nfor encrypting passwords in your access.txt.\n\n1. Ensure your 3forge AMI Application is running. Your access.txt by default would be located in\nyour data directory.\n\n[[File:Files in AMI data folder.png]]\n\n2. This document will contain a list of all users and their passwords as well as their\npermissions.\n\n[[File:Access file.png]]\n\n3. To start encrypting the passwords you will need to generate the encrypted string for\neach password. To do so you will need to telnet to your ami.db.console.port whose\ndefault value is 3290 and login to a user with DB permissions.\n\n[[File:Putty Configuration.png]]\n\n[[File:PuTTY Login.png]]\n\n4.To encrypt each password, run the AMIScript method on the console\nstrEncrypt(\"your-password\"). This command will return your encrypted password.\n\n[[File:Encryption Procedure.png]]\n\n5. For each password in the access.txt update the password with the encrypted value.\n\n[[File:Passwords Updating.png]]\n\n6. Add a new property in a local.properties or create one in your config directory. In the file\nadd the following property:\nusers.access.file.encrypt.mode=password\n\n[[File:Local.properties.png]]\n\n7. The final step is to restart your 3forge AMI Application\n\n1st Note: Store the amikey.aes securely so that you have a recovery mechanism setup for\nthis in case of data loss. This key by default is set by the property and configured with:\nami.aes.key.file=persist/amikey.aes. If lost, users will not be able to login to their\naccounts.\n\n2nd Note: To change the the access.txt file set the property:\nusers.access.file=pathToYourAccess.txt\n\n= Window Visibility Permission Control Over Different User Groups  =\nAMI has its own way of doing permission control over different groups of users so that certain windows are only visible to users with entitlements. Suppose we have two windows named '''Restricted''' and '''Everyone'''. The Restricted window should '''NOT''' be visible to the users whose role is '''NOT''' Admin. The following screenshot shows how to set up such a scenario. <br>\n1.Go to Dashboard -> Custom Callbacks <br>\n[[File:P1.jpg]]\n<br>\n2.Go to '''onStartup''' tab and enter the following AMI script commands:<br>\n[[File:P3.png]]\n<br>\n<syntaxhighlight lang=\"java\">\nstring role = session.getProperty(\"ROLE\");\nif (role != \"Admin\"){\n  for (window w: session.getWindows()){\n    if (w.getName()==\"Restricted\"){\n      w.setType(\"HIDDEN\");\n      w.minimize();\n    }\n  }\n}\n</syntaxhighlight>\n<br>\n3. Hit '''Submit'''<br>\nNow the users should be able to see different windows based on their entitlements.<br>\n[[File:P5.jpg]]\n\n= Subscribe to Realtime Tables/feeds From a Datamodel and Listen for Changes to Update Form Fields  =\nSuppose we have two real time tables: '''ReconFailure''' and '''ReconSuccess''' that keeps track of successful and failed transactions across different systems in real time, initially set to zero<br>\n[[File:ReconFailureSchema.jpg]]\n[[File:ReconSuccessSchema.jpg]]<br>\nYou can use the following code to follow along this tutorial<br>\n<syntaxhighlight lang=\"sql\">\ndrop table if exists ReconFailure;\ncreate public table ReconFailure(FailureCount int, System string);\ninsert into ReconFailure values(0,\"s1\"),(0,\"s2\"),(0,\"s3\");\n\ndrop table if exists ReconSuccess;\ncreate public table ReconSuccess(SuccessCount int, System string);\ninsert into ReconSuccess values(0,\"s1\"),(0,\"s2\"),(0,\"s3\");\n</syntaxhighlight>\n\n\n1. Build a datamodel against two real time tables '''ReconFailure''' and '''ReconSuccess'''<br>\n[[File:BuildDatamodel.jpg]]<br>\n2. Configure the form fields (using text field in this case)<br>\nIn a new panel, hit the central button, go to '''Create HTML Panel'''<br>\n[[File:Create field.jpg]]<br>\nHit the central button in the middle, go to '''Add Field'''-> '''Text Field'''<br>\n[[File:Field2.jpg]]<br>\nMake 6 Text fields as below:<br>\n[[File:Field3.jpg]]\n\n3. Edit the datamodel and set up form field variables that match the two real time table schemas<br>\n[[File:ConfigureVariable.png]]<br>\n\n4.Subscribe to two real time feeds: '''ReconFailure''' and '''ReconSuccess'''<br>\n[[File:ConfigureAMIScript.jpg]]<br>\n5. Go to '''OnProcess''' and enter the following code script\n<syntaxhighlight lang=\"java\">\nboolean isSnapshotProcessing = !session.isFeedSnapshotProcessed(\"ReconSuccess\") && !session.isFeedSnapshotProcessed(\"ReconFailure\");\n  if (isSnapshotProcessing)\n    return;\n  \n  RealtimeEvent rte = rtevents;\n  while(rte != null) {\n    if (rte.getFeed() == \"ReconSuccess\") {\n      String system = rte.getValues().get(\"System\");\n      int count = rte.getValues().get(\"SuccessCount\");\n      if (system == \"s1\")\n        s1SuccessField.setValue(count);\n      else if (system == \"s2\")\n        s2SuccessField.setValue(count);\n      else if (system == \"s3\")\n        s3SuccessField.setValue(count);\n    } else if (rte.getFeed() == \"ReconFailure\") {\n      String system = rte.getValues().get(\"System\");\n      int count = rte.getValues().get(\"FailureCount\");\n      if (system == \"s1\")\n        s1FailField.setValue(count);\n      else if (system == \"s2\")\n        s2FailField.setValue(count);\n      else if (system == \"s3\")\n        s3FailField.setValue(count);\n    }\n    rte = rte.getNext();\n  }\n</syntaxhighlight>\n\n6. Set up timers to simulate real time feeds <br>\nFor the sake of demonstration, I am using two timers here to update two real time tables '''ReconFailure''' and '''ReconSuccess'''. In real life scenarios, you may connect to some external real time feeds.\n\n<code>\n//timer1 to update ReconSuccess\n</code>\n<syntaxhighlight lang=\"sql\">\ncreate timer SuccessTransaction oftype AMISCRIPT on \"500\" USE script= \"list Systems = new list(\\\"s1\\\",\\\"s2\\\",\\\"s3\\\"); int SysID = rand(3); String sys = Systems.get(SysID); Update ReconSuccess set SuccessCount=SuccessCount+1 where System==sys;\"\n</syntaxhighlight>\n\n<code>\n//timer2 to update ReconFailure\n</code>\n\n<syntaxhighlight lang=\"sql\">\ncreate timer FailureTransaction oftype AMISCRIPT on \"500\" USE script= \"list Systems = new list(\\\"s1\\\",\\\"s2\\\",\\\"s3\\\"); int SysID = rand(3); String sys = Systems.get(SysID); Update ReconFailure set FailureCount=FailureCount+1 where System==sys;\"\n</syntaxhighlight>\n\nOnce enabling the timer, the figures in the text fields will be in sync with the counts in two real time tables.<br>\n[[File:SyncResult.jpg]]\n\n= Real time Data model  =\nWe can have our data model subscribe to a real time feed to make it a real time data model. <br>\nLet's say we have a real time feed '''transaction(TransactionID Long,sym String,price Double)''' <br>\n1. create a data model called '''realtimeDM''' and subscribe to real time feed '''transaction'''<br>\n[[File:RealtimeDM0.jpg]]<br>\n\n2. You could also configure queries to construct derived tables<br>\nIn this example, we created another table showing the top3 symbol with the most total price.<br>\n[[File:RealtimeDMDerived.jpg]]\n<br>\nAdditionally, you could also configure the '''conflate time''' parameter to control how frequently you want your data model to rerun and refresh.<br>\nIn this example, it is set to 10 seconds, which means the data model will rerun 10 seconds per time.<br>\n[[File:RealtimeDM conflate.jpg]]<br>\n3. Create real time visualization panel off of the real time data model that we just created<br>\nIn this example, we created a real time heatmap off of the '''top3sym''' table.<br>\n[[File:RealtimeDM vp.jpg]]<br>\n[[File:RealtimeDMChooseTable.jpg]]<br>\n4. Final Heatmap<br>\n[[File:RealtimeDMHeatmap.jpg]]\n\n'''Note that you can press space on the heatmap to zoom in/out.'''\n\n= Import data model and panel configuration to AMI = \n1. Create a '''/myConfigs''' (can be any name you want) directory local to your AMI installation and put the configuration files (usually in the format of JSON) in it <br>\nThe example below has the configuration file named CountryTemplate.json. You can have multiple configuration files<br>\n[[File:ImportDM1.jpg]] <br>\n2. Read, Parse the config files and import the data model and panel to the AMI session<br>\nAlso additionally, you can configure data model's '''WHERE''' variable.<br>\nThe amiScript below show how to achieve these where each visualization panel has exactly one underlying data model: <br>\n\n<syntaxhighlight lang=\"java\">\nfilesystem fs=session.getFileSystem();\nString Panelconfig = fs.readFile(\"myConfigs/testPanelConfig.json\");\nString DMconfig = fs.readFile(\"myConfigs/testDMConfig.json\");\nmap Panelparsed = parseJson(Panelconfig);\nmap DMparsed = parseJson(DMconfig);\ndatamodel dm=session.importDatamodel(DMparsed);\nmap m = Panelparsed.jsonPath(\"portletConfigs.0.portletConfig.dm.0\"); //get the id from the data model that we imported\nm.put(\"dmadn\", dm.getId()); //put the id above into the panel's configuration\ndm.process(new map(\"WHERE\", \"Code=\\\"ABW\\\"\")); // configure WHERE variable\nsession.importWindow(\"newWindow1\", Panelparsed); //import the window\n</syntaxhighlight>\n\n= Progress Bar in Column Formatting = \nOn the frontend table GUI, we can add and configure additional columns to make it look like a progress bar. <br>\nSuppose we have a GUI table '''progress''' like the following:<br>\n[[File:ProgressBar.jpg]]<br>\nWe can configure an additional column that displays the ratio of the number of Completed and the total number and shows the current progress<br>\n[[File:ProgressBar2.jpg]]<br>\nAdditionally, we can also configure the style of the progress bar. For example:<br>\n[[File:ProgressBar3.jpg]]\n\n= Breadcrumb Filter Example = \nThis is a basic example of a breadcrumb filter through the use of HTML and Javascript in a Form Div field. <br><br>\n[[File:Breadcrumb1.png|500px|500px]] <br><br>\nThe layout follows the breadcrumb trail of tiers (T1 - T4) as filter levels. Most of the script can be found in the custom methods of the layout, and some in the onAmiJsCallback callback of the breadcrumb panel (for handling clicks to reset filters to indicated level/tier). <br><br>\nLayout: <br>\n<blockquote><pre>\n{\"layouts\":[{\"data\":{\"includeFiles\":[],\"metadata\":{\"amiScriptMethods\":[\"{\\n\",\"  string getBreadCrumbStyle() {\\n\",\"    return \\\"<style>\\n\",\"      ul.breadcrumb {padding: 10px 10px;\\n\",\"      list-style: none;\\n\",\"      background-color: #eee;\\n\",\"      }\\n\",\"      ul.breadcrumb li {\\n\",\"      display: inline;\\n\",\"      font-size: 18px;\\n\",\"      }\\n\",\"      ul.breadcrumb li+li:before {\\n\",\"        padding: 8px;\\n\",\"        color: black;\\n\",\"        content: \\\\\\\"/\\\\\\\";\\n\",\"      }\\n\",\"      ul.breadcrumb li a {\\n\",\"        color: #0275d8;\\n\",\"        text-decoration: none;\\n\",\"      }\\n\",\"      ul.breadcrumb li a:hover {\\n\",\"        color: #01447e;\\n\",\"        text-decoration: underline;\\n\",\"      }\\n\",\"      </style>\\\";\\n\",\"  };\\n\",\"  \\n\",\"  boolean setFilter(){\\n\",\"    //get panels\\n\",\"    TablePanel tier1 = layout.getPanel(\\\"tier1\\\");\\n\",\"    TablePanel tier2 = layout.getPanel(\\\"tier2\\\");\\n\",\"    TablePanel tier3 = layout.getPanel(\\\"tier3\\\");\\n\",\"    TablePanel tier4 = layout.getPanel(\\\"tier4\\\");\\n\",\"    \\n\",\"    //build where clause for filtering & values for breadcrumbs\\n\",\"    string whereClause = \\\"\\\";\\n\",\"    string t1Vals = \\\"\\\";\\n\",\"    string t2Vals = \\\"\\\";\\n\",\"    string t3Vals = \\\"\\\";\\n\",\"    string t4Vals = \\\"\\\";\\n\",\"    if (tier1.getSelectedRows().size()!=0){\\n\",\"      row firstRow = tier1.getSelectedRows().get(0);\\n\",\"      string firstRowVal = firstRow.getValue(\\\"T1\\\");\\n\",\"      whereClause += \\\"T1 IN (\\\\\\\"${firstRowVal}\\\\\\\"\\\";\\n\",\"      t1Vals += firstRowVal;\\n\",\"      for (int i = 1; i < tier1.getSelectedRows().size(); ++i){\\n\",\"        row r = tier1.getSelectedRows().get(i);\\n\",\"        string rVal = r.getValue(\\\"T1\\\");\\n\",\"        whereClause += \\\",\\\\\\\"${rVal}\\\\\\\"\\\";\\n\",\"        t1Vals += \\\", ${rVal}\\\";\\n\",\"      }\\n\",\"      whereClause += \\\")\\\";\\n\",\"    }\\n\",\"    if (tier2.getSelectedRows().size()!=0){\\n\",\"      if (whereClause != \\\"\\\")\\n\",\"        whereClause += \\\" && \\\";\\n\",\"      row firstRow = tier2.getSelectedRows().get(0);\\n\",\"      string firstRowVal = firstRow.getValue(\\\"T2\\\");\\n\",\"      whereClause += \\\"T2 IN (\\\\\\\"${firstRowVal}\\\\\\\"\\\";\\n\",\"      t2Vals += firstRowVal;\\n\",\"      for (int i = 1; i < tier2.getSelectedRows().size(); ++i){\\n\",\"        row r = tier2.getSelectedRows().get(i);\\n\",\"        string rVal = r.getValue(\\\"T2\\\");\\n\",\"        whereClause += \\\",\\\\\\\"${rVal}\\\\\\\"\\\";\\n\",\"        t2Vals += \\\", ${rVal}\\\";\\n\",\"      }\\n\",\"      whereClause += \\\")\\\";\\n\",\"    }\\n\",\"    if (tier3.getSelectedRows().size()!=0){\\n\",\"      if (whereClause != \\\"\\\")\\n\",\"        whereClause += \\\" && \\\";\\n\",\"      row firstRow = tier3.getSelectedRows().get(0);\\n\",\"      string firstRowVal = firstRow.getValue(\\\"T3\\\");\\n\",\"      whereClause += \\\"T3 IN (\\\\\\\"${firstRowVal}\\\\\\\"\\\";\\n\",\"      t3Vals += firstRowVal;\\n\",\"      for (int i = 1; i < tier3.getSelectedRows().size(); ++i){\\n\",\"        row r = tier3.getSelectedRows().get(i);\\n\",\"        string rVal = r.getValue(\\\"T3\\\");\\n\",\"        whereClause += \\\",\\\\\\\"${rVal}\\\\\\\"\\\";\\n\",\"        t3Vals += \\\", ${rVal}\\\";\\n\",\"      }\\n\",\"      whereClause += \\\")\\\";\\n\",\"    }\\n\",\"    if (tier4.getSelectedRows().size()!=0){\\n\",\"      if (whereClause != \\\"\\\")\\n\",\"        whereClause += \\\" && \\\";\\n\",\"      row firstRow = tier4.getSelectedRows().get(0);\\n\",\"      string firstRowVal = firstRow.getValue(\\\"T4\\\");\\n\",\"      whereClause += \\\"T4 IN (\\\\\\\"${firstRowVal}\\\\\\\"\\\";\\n\",\"      t4Vals += firstRowVal;\\n\",\"      for (int i = 1; i < tier4.getSelectedRows().size(); ++i){\\n\",\"        row r = tier4.getSelectedRows().get(i);\\n\",\"        string rVal = r.getValue(\\\"T4\\\");\\n\",\"        whereClause += \\\",\\\\\\\"${rVal}\\\\\\\"\\\";\\n\",\"        t4Vals += \\\", ${rVal}\\\";\\n\",\"      }\\n\",\"      whereClause += \\\")\\\";\\n\",\"    }\\n\",\"    \\n\",\"    //apply where clause\\n\",\"    TablePanel mainTable = layout.getPanel(\\\"mainTable\\\");\\n\",\"    mainTable.setCurrentWhere(whereClause);\\n\",\"    \\n\",\"    //call breadcrumb builder\\n\",\"    t1Vals = t1Vals == \\\"\\\" ? \\\"All\\\" : t1Vals;\\n\",\"    t2Vals = t2Vals == \\\"\\\" ? \\\"All\\\" : t2Vals;\\n\",\"    t3Vals = t3Vals == \\\"\\\" ? \\\"All\\\" : t3Vals;\\n\",\"    t4Vals = t4Vals == \\\"\\\" ? \\\"All\\\" : t4Vals;\\n\",\"    map crumbValues = new map();\\n\",\"    crumbValues.put(\\\"t1\\\", t1Vals);\\n\",\"    crumbValues.put(\\\"t2\\\", t2Vals);\\n\",\"    crumbValues.put(\\\"t3\\\", t3Vals);\\n\",\"    crumbValues.put(\\\"t4\\\", t4Vals);\\n\",\"    buildBreadCrumb(crumbValues);\\n\",\"    \\n\",\"    return true;\\n\",\"  };\\n\",\"  \\n\",\"  boolean buildBreadCrumb(map crumbValues){\\n\",\"    String breadCrumb = getBreadCrumbStyle();\\n\",\"    breadCrumb += \\\"<body>\\n\",\"        <ul class=\\\\\\\"breadcrumb\\\\\\\">\\\";\\n\",\"    for (int i = 1; i < crumbValues.size() + 1; ++i){\\n\",\"      string crumbVal = crumbValues.get(\\\"t${i}\\\");\\n\",\"      if (crumbVal == \\\"All\\\"){\\n\",\"        boolean allBreak = true;\\n\",\"        for (int j = i; j < crumbValues.size() + 1; ++j){\\n\",\"          string allCheck = crumbValues.get(\\\"t${j}\\\");\\n\",\"          if (allCheck != \\\"All\\\")\\n\",\"            allBreak = false;\\n\",\"        }\\n\",\"        if (allBreak) \\n\",\"          break;\\n\",\"      }\\n\",\"      breadCrumb += \\\"<li><a onclick='amiJsCallback(this,${i}, 0);'>${crumbVal}</a></li>\\\";\\n\",\"    }\\n\",\"    breadCrumb += \\\"</ul>\\n\",\"                  </body>\\\";\\n\",\"                  \\n\",\"    FormPanel breadCrumbPnl = layout.getPanel(\\\"breadcrumbPnl\\\");\\n\",\"    FormDivField breadcrumbDiv = breadCrumbPnl.getField(\\\"breadcrumbDiv\\\");\\n\",\"    breadcrumbDiv.setValue(breadCrumb);\\n\",\"  };\\n\",\"}\"],\"customPrefsImportMode\":\"reject\",\"dm\":{\"dms\":[{\"callbacks\":{\"entries\":[{\"amiscript\":[\"{\\n\",\"  CREATE TABLE Sample(T1 String, T2 String, T3 String, T4 String, value string);\\n\",\"  \\n\",\"  for (int i1=0; i1<2; ++i1){\\n\",\"    string t1 = \\\"1.${i1+1}\\\";\\n\",\"    for (int i2=0; i2<4; ++i2){\\n\",\"      string t2 = \\\"2.${i2+1}\\\";\\n\",\"      for (int i3=0; i3<8; ++i3){\\n\",\"        string t3 = \\\"3.${i3+1}\\\";\\n\",\"        for (int i4=0; i4<8; ++i4){\\n\",\"          string t4 = \\\"4.${i4+1}\\\";\\n\",\"          insert into Sample values (t1, t2, t3, t4, \\\"Sample\\\");\\n\",\"        }\\n\",\"      }\\n\",\"    }\\n\",\"  }\\n\",\"}\\n\"],\"hasDatamodel\":true,\"linkedVariables\":[],\"name\":\"onProcess\",\"schema\":{\"tbl\":[{\"cols\":[{\"nm\":\"T1\",\"tp\":\"String\"},{\"nm\":\"T2\",\"tp\":\"String\"},{\"nm\":\"T3\",\"tp\":\"String\"},{\"nm\":\"T4\",\"tp\":\"String\"},{\"nm\":\"value\",\"tp\":\"String\"}],\"nm\":\"Sample\",\"oc\":\"ask\"}]}}]},\"lbl\":\"mainTable\",\"lm\":0,\"lower\":[],\"queryMode\":\"startup\",\"test_input_type\":\"OPEN\",\"test_input_vars\":\"String WHERE=\\\"true\\\";\",\"to\":0},{\"callbacks\":{\"entries\":[{\"amiscript\":[\"{\\n\",\"  CREATE TABLE Tier1 AS SELECT T1 FROM Sample WHERE ${WHERE} group by T1;\\n\",\"}\"],\"hasDatamodel\":true,\"inputDm\":[\"mainTable\"],\"linkedVariables\":[],\"name\":\"onProcess\",\"schema\":{\"tbl\":[{\"cols\":[{\"nm\":\"T1\",\"tp\":\"String\"},{\"nm\":\"T2\",\"tp\":\"String\"},{\"nm\":\"T3\",\"tp\":\"String\"},{\"nm\":\"T4\",\"tp\":\"String\"},{\"nm\":\"value\",\"tp\":\"String\"}],\"nm\":\"Sample\",\"oc\":\"ask\"},{\"cols\":[{\"nm\":\"T1\",\"tp\":\"String\"}],\"nm\":\"Tier1\",\"oc\":\"ask\"}]}}]},\"lbl\":\"tier1\",\"lm\":0,\"lower\":[\"mainTable\"],\"test_input_type\":\"OPEN\",\"test_input_vars\":\"String WHERE=\\\"true\\\";\",\"to\":0},{\"callbacks\":{\"entries\":[{\"amiscript\":[\"{\\n\",\"  CREATE TABLE Tier2 AS SELECT T2 FROM Sample WHERE ${WHERE} group by T2;\\n\",\"}\\n\"],\"hasDatamodel\":true,\"inputDm\":[\"mainTable\"],\"linkedVariables\":[],\"name\":\"onProcess\",\"schema\":{\"tbl\":[{\"cols\":[{\"nm\":\"T1\",\"tp\":\"String\"},{\"nm\":\"T2\",\"tp\":\"String\"},{\"nm\":\"T3\",\"tp\":\"String\"},{\"nm\":\"T4\",\"tp\":\"String\"},{\"nm\":\"value\",\"tp\":\"String\"}],\"nm\":\"Sample\",\"oc\":\"ask\"},{\"cols\":[{\"nm\":\"T2\",\"tp\":\"String\"}],\"nm\":\"Tier2\",\"oc\":\"ask\"}]}}]},\"lbl\":\"tier2\",\"lm\":0,\"lower\":[\"mainTable\"],\"test_input_type\":\"OPEN\",\"test_input_vars\":\"String WHERE=\\\"true\\\";\",\"to\":0},{\"callbacks\":{\"entries\":[{\"amiscript\":[\"{\\n\",\"  CREATE TABLE Tier3 AS SELECT T3 FROM Sample WHERE ${WHERE} group by T3;\\n\",\"}\\n\"],\"hasDatamodel\":true,\"inputDm\":[\"mainTable\"],\"linkedVariables\":[],\"name\":\"onProcess\",\"schema\":{\"tbl\":[{\"cols\":[{\"nm\":\"T1\",\"tp\":\"String\"},{\"nm\":\"T2\",\"tp\":\"String\"},{\"nm\":\"T3\",\"tp\":\"String\"},{\"nm\":\"T4\",\"tp\":\"String\"},{\"nm\":\"value\",\"tp\":\"String\"}],\"nm\":\"Sample\",\"oc\":\"ask\"},{\"cols\":[{\"nm\":\"T3\",\"tp\":\"String\"}],\"nm\":\"Tier3\",\"oc\":\"ask\"}]}}]},\"lbl\":\"tier3\",\"lm\":0,\"lower\":[\"mainTable\"],\"test_input_type\":\"OPEN\",\"test_input_vars\":\"String WHERE=\\\"true\\\";\",\"to\":0},{\"callbacks\":{\"entries\":[{\"amiscript\":[\"{\\n\",\"  CREATE TABLE Tier4 AS SELECT T4 FROM Sample WHERE ${WHERE} group by T4;\\n\",\"}\\n\"],\"hasDatamodel\":true,\"inputDm\":[\"mainTable\"],\"linkedVariables\":[],\"name\":\"onProcess\",\"schema\":{\"tbl\":[{\"cols\":[{\"nm\":\"T1\",\"tp\":\"String\"},{\"nm\":\"T2\",\"tp\":\"String\"},{\"nm\":\"T3\",\"tp\":\"String\"},{\"nm\":\"T4\",\"tp\":\"String\"},{\"nm\":\"value\",\"tp\":\"String\"}],\"nm\":\"Sample\",\"oc\":\"ask\"},{\"cols\":[{\"nm\":\"T4\",\"tp\":\"String\"}],\"nm\":\"Tier4\",\"oc\":\"ask\"}]}}]},\"lbl\":\"tier4\",\"lm\":0,\"lower\":[\"mainTable\"],\"test_input_type\":\"OPEN\",\"test_input_vars\":\"String WHERE=\\\"true\\\";\",\"to\":0}]},\"fileVersion\":3,\"rt\":{\"processors\":[]},\"stm\":{\"styles\":[{\"id\":\"LAYOUT_DEFAULT\",\"lb\":\"Layout Default\",\"pt\":\"DEFAULT\"}]}},\"portletConfigs\":[{\"portletBuilderId\":\"amidesktop\",\"portletConfig\":{\"active\":\"Div1\",\"amiPanelId\":\"@DESKTOP\",\"amiStyle\":{\"pt\":\"LAYOUT_DEFAULT\"},\"windows\":[{\"header\":true,\"height\":697,\"hidden\":false,\"left\":582,\"portlet\":\"Div1\",\"pos\":0,\"state\":\"flt\",\"title\":\"Window\",\"top\":442,\"width\":1235,\"zindex\":2},{\"header\":true,\"height\":137,\"hidden\":false,\"left\":558,\"portlet\":\"breadcrumbPnl\",\"pos\":1,\"state\":\"flt\",\"title\":\"Window - 2\",\"top\":277,\"width\":1280,\"zindex\":1}]}},{\"portletBuilderId\":\"div\",\"portletConfig\":{\"amiPanelId\":\"Div1\",\"amiStyle\":{\"pt\":\"LAYOUT_DEFAULT\",\"vl\":{\"div\":{\"divAlign\":\"start\"}}},\"child1\":\"Div2\",\"child2\":\"mainTable\",\"dir\":\"v\",\"isMin\":false,\"offset\":0.14736842105263157,\"upid\":\"Div1\"}},{\"portletBuilderId\":\"div\",\"portletConfig\":{\"amiPanelId\":\"Div2\",\"amiStyle\":{\"pt\":\"LAYOUT_DEFAULT\"},\"child1\":\"tier1\",\"child2\":\"Div3\",\"dir\":\"h\",\"isMin\":false,\"offset\":0.15460992907801419,\"upid\":\"Div2\"}},{\"portletBuilderId\":\"div\",\"portletConfig\":{\"amiPanelId\":\"Div3\",\"amiStyle\":{\"pt\":\"LAYOUT_DEFAULT\"},\"child1\":\"tier2\",\"child2\":\"Div4\",\"dir\":\"h\",\"isMin\":false,\"offset\":0.24283305227655985,\"upid\":\"Div3\"}},{\"portletBuilderId\":\"div\",\"portletConfig\":{\"amiPanelId\":\"Div4\",\"amiStyle\":{\"pt\":\"LAYOUT_DEFAULT\"},\"child1\":\"tier3\",\"child2\":\"tier4\",\"dir\":\"h\",\"isMin\":false,\"offset\":0.48654708520179374,\"upid\":\"Div4\"}},{\"portletBuilderId\":\"amiform\",\"portletConfig\":{\"amiPanelId\":\"breadcrumbPnl\",\"amiStyle\":{\"pt\":\"LAYOUT_DEFAULT\"},\"amiTitle\":\"\",\"buttons\":[],\"callbacks\":{\"entries\":[{\"amiscript\":[\"int selected = action;\\n\",\"int totalTiers = 4;\\n\",\"\\n\",\"for (int i = selected + 1; i < totalTiers + 1; ++i){\\n\",\"  TablePanel tierPanel = layout.getPanel(\\\"tier${i}\\\");\\n\",\"  tierPanel.selectRows(false);\\n\",\"}\"],\"linkedVariables\":[],\"name\":\"onAmiJsCallback\"}]},\"dm\":[],\"fields\":[{\"callbacks\":{},\"disabled\":false,\"dme\":\"\",\"heightPx\":59,\"help\":\"\",\"hidden\":true,\"l\":\"breadcrumbDiv\",\"labelHidden\":true,\"leftPosPx\":160,\"n\":\"breadcrumbDiv\",\"style\":\"\",\"t\":\"divField\",\"template\":\"\",\"topPosPx\":40,\"widthPx\":840,\"zidx\":1}],\"guides\":[],\"htmlTemplate2\":null,\"snap\":20,\"titlePnl\":{\"title\":\"\"},\"upid\":\"breadcrumbPnl\"}},{\"portletBuilderId\":\"amistatictable\",\"portletConfig\":{\"amiCols\":[{\"fm\":\"T1\",\"id\":\"T1\",\"location\":0,\"tl\":\"T1\",\"tp\":\"text\",\"width\":188},{\"fm\":\"T2\",\"id\":\"T2\",\"location\":1,\"tl\":\"T2\",\"tp\":\"text\",\"width\":214},{\"fm\":\"T3\",\"id\":\"T3\",\"location\":2,\"tl\":\"T3\",\"tp\":\"text\",\"width\":215},{\"fm\":\"T4\",\"id\":\"T4\",\"location\":3,\"tl\":\"T4\",\"tp\":\"text\",\"width\":268},{\"fm\":\"value\",\"id\":\"value\",\"location\":4,\"tl\":\"Value\",\"tp\":\"text\",\"width\":130},{\"id\":\"D\",\"width\":100}],\"amiPanelId\":\"mainTable\",\"amiStyle\":{\"pt\":\"LAYOUT_DEFAULT\"},\"amiTitle\":\"Sample\",\"curtimeUpdateFrequency\":1000,\"dm\":[{\"dmadn\":\"mainTable\",\"dmtbid\":[\"Sample\"]}],\"dynamicColumns\":\"false\",\"editDblClk\":true,\"editInplace\":false,\"editMenuTitle\":\"Edit Row(s)\",\"editMode\":0,\"editRerunDM\":true,\"filters\":{},\"pinCnt\":0,\"rollupEnabled\":false,\"scrollToBottomOnAppend\":false,\"showCommandMenu\":true,\"showLastRuntime\":true,\"titlePnl\":{\"title\":\"Sample\"},\"upid\":\"mainTable\",\"varTypes\":{\"T1\":\"String\",\"T2\":\"String\",\"T3\":\"String\",\"T4\":\"String\",\"value\":\"String\"}}},{\"portletBuilderId\":\"amistatictable\",\"portletConfig\":{\"amiCols\":[{\"ei\":\"\",\"eof\":\"\",\"fm\":\"T1\",\"id\":\"T1\",\"location\":0,\"sy\":\"\\\"center\\\"\",\"tl\":\"T1\",\"tp\":\"text\",\"width\":174},{\"id\":\"D\",\"width\":100}],\"amiPanelId\":\"tier1\",\"amiStyle\":{\"pt\":\"LAYOUT_DEFAULT\"},\"amiTitle\":\"Tier1\",\"callbacks\":{\"entries\":[{\"amiscript\":\"setFilter();\",\"linkedVariables\":[],\"name\":\"onSelected\"}]},\"curtimeUpdateFrequency\":1000,\"dm\":[{\"dmadn\":\"tier1\",\"dmtbid\":[\"Tier1\"]}],\"dynamicColumns\":\"false\",\"editDblClk\":true,\"editInplace\":false,\"editMenuTitle\":\"Edit Row(s)\",\"editMode\":0,\"editRerunDM\":true,\"filters\":{},\"pinCnt\":0,\"rollupEnabled\":false,\"scrollToBottomOnAppend\":false,\"showCommandMenu\":true,\"showLastRuntime\":true,\"titlePnl\":{\"title\":\"Tier1\"},\"upid\":\"tier1\",\"varTypes\":{\"T1\":\"String\"}}},{\"portletBuilderId\":\"amistatictable\",\"portletConfig\":{\"amiCols\":[{\"ei\":\"\",\"eof\":\"\",\"fm\":\"T2\",\"id\":\"T2\",\"location\":0,\"sy\":\"\\\"center\\\"\",\"tl\":\"T2\",\"tp\":\"text\",\"width\":169},{\"id\":\"D\",\"width\":100}],\"amiPanelId\":\"tier2\",\"amiStyle\":{\"pt\":\"LAYOUT_DEFAULT\"},\"amiTitle\":\"Tier2\",\"callbacks\":{\"entries\":[{\"amiscript\":\"setFilter();\",\"linkedVariables\":[],\"name\":\"onSelected\"}]},\"curtimeUpdateFrequency\":1000,\"dm\":[{\"dmadn\":\"tier2\",\"dmtbid\":[\"Tier2\"]}],\"dynamicColumns\":\"false\",\"editDblClk\":true,\"editInplace\":false,\"editMenuTitle\":\"Edit Row(s)\",\"editMode\":0,\"editRerunDM\":true,\"filters\":{},\"pinCnt\":0,\"rollupEnabled\":false,\"scrollToBottomOnAppend\":false,\"showCommandMenu\":true,\"showLastRuntime\":true,\"titlePnl\":{\"title\":\"Tier2\"},\"upid\":\"tier2\",\"varTypes\":{\"T2\":\"String\"}}},{\"portletBuilderId\":\"amistatictable\",\"portletConfig\":{\"amiCols\":[{\"ei\":\"\",\"eof\":\"\",\"fm\":\"T3\",\"id\":\"T3\",\"location\":0,\"sy\":\"\\\"center\\\"\",\"tl\":\"T3\",\"tp\":\"text\",\"width\":168},{\"id\":\"D\",\"width\":100}],\"amiPanelId\":\"tier3\",\"amiStyle\":{\"pt\":\"LAYOUT_DEFAULT\"},\"amiTitle\":\"Tier3\",\"callbacks\":{\"entries\":[{\"amiscript\":\"setFilter();\",\"linkedVariables\":[],\"name\":\"onSelected\"}]},\"curtimeUpdateFrequency\":1000,\"dm\":[{\"dmadn\":\"tier3\",\"dmtbid\":[\"Tier3\"]}],\"dynamicColumns\":\"false\",\"editDblClk\":true,\"editInplace\":false,\"editMenuTitle\":\"Edit Row(s)\",\"editMode\":0,\"editRerunDM\":true,\"filters\":{},\"pinCnt\":0,\"rollupEnabled\":false,\"scrollToBottomOnAppend\":false,\"showCommandMenu\":true,\"showLastRuntime\":true,\"titlePnl\":{\"title\":\"Tier3\"},\"upid\":\"tier3\",\"varTypes\":{\"T3\":\"String\"}}},{\"portletBuilderId\":\"amistatictable\",\"portletConfig\":{\"amiCols\":[{\"ei\":\"\",\"eof\":\"\",\"fm\":\"T4\",\"id\":\"T4\",\"location\":0,\"sy\":\"\\\"center\\\"\",\"tl\":\"T4\",\"tp\":\"text\",\"width\":167},{\"id\":\"D\",\"width\":100}],\"amiPanelId\":\"tier4\",\"amiStyle\":{\"pt\":\"LAYOUT_DEFAULT\"},\"amiTitle\":\"Tier4\",\"callbacks\":{\"entries\":[{\"amiscript\":\"setFilter();\",\"linkedVariables\":[],\"name\":\"onSelected\"}]},\"curtimeUpdateFrequency\":1000,\"dm\":[{\"dmadn\":\"tier4\",\"dmtbid\":[\"Tier4\"]}],\"dynamicColumns\":\"false\",\"editDblClk\":true,\"editInplace\":false,\"editMenuTitle\":\"Edit Row(s)\",\"editMode\":0,\"editRerunDM\":true,\"filters\":{},\"pinCnt\":0,\"rollupEnabled\":false,\"scrollToBottomOnAppend\":false,\"showCommandMenu\":true,\"showLastRuntime\":true,\"titlePnl\":{\"title\":\"Tier4\"},\"upid\":\"tier4\",\"varTypes\":{\"T4\":\"String\"}}}]},\"location\":\"breadcrumbs.ami\",\"type\":\"ABSOLUTE\"}],\"rootLayout\":{\"location\":\"breadcrumbs.ami\",\"type\":\"ABSOLUTE\"}}\n</pre></blockquote>\n\n= Sending emails from AMI =\nBelow is an minimal example setup for setting up email in the properties file (''local.properties''): <br><br>\n<code>\nemail.client.host=smtp.office365.com <br>\nemail.client.port=587 <br>\nemail.client.username=demo@outlook.com <br>\nemail.client.password=samplePassword\n</code><br><br>\nChange the host and port properties accordingly (gmail, outlook, etc). <br>\nMore email configuration properties can be found here: [[AMI_Configuration_Guide#Email_Configuration_Properties|3forge_Email_Configuration_Properties]]<br>\n\n== Layout Setup ==\nThere are two methods in amiscript for sending emails: <br>\n<code>\nsendEmail (sends an email and returns immediately with the email-send-UID)<br>\nsendEmailSync (sends an email and returns with the email result)\n</code><br><br>\n\nMethod description:<br>\n[[File:EmailExample1.png|700px|700px]] <br><br>\n\n== List of supported attachment file formats ==\n* Application:\natom\n, json\n, jar\n, js\n, ogg, ogv\n, pdf\n, ps\n, woff\n, xhtml, xht, xml\n, dtd\n, zip\n, gz\n, xlsx\n*Audio:\nau, snd\n,mid, rmi\n,mp3\n,aif, aifc, aiff\n,m3u\n,ra, ram\n,wav\n*Image:\n,bmp\n,cod\n,gif\n,ief\n,jpe, jpeg, jpg\n,jfif\n,png\n,svg\n,tif, tiff\n,ras\n,cmx\n,ico\n,pnm\n,pbm\n,pgm\n,ppm\n,rgb\n,xbm\n,xpm\n,xwd\n*Message:\nmht, mhtml, nws\n*Text:\ncss\n,323\n,htm, html, stm\n,uls\n,bas, c, h, txt, ami\n,rtx\n,sct\n,tsv\n,htt\n,htc\n,etc\n,vcf\n*Video:\nmp2, mpa, mpe, mpeg, mpg, mpv2\n,mov, qt\n,lsf, lsx\n,asf, asr, asx\n,avi\n,movie\n*X-world:\nflr, vrml, wrl, wrz, xaf, xof\n\n= Troubleshooting SSL-related issues =\n\nHere's some guidance on how to troubleshoot the following SSL-related issues, such as '''''No available authentication scheme''''','''''Unsupported or unrecognized SSL message''''',or '''''SSL Protocol Error'''''\n\nIn our experience these issues are not caused by 3forge AMI but are generic error messages indicating that there was a problem in how the certificate  was generated. <br>\n\nHere is our recommended procedure for the generation:<br>\n\n1) Download the root keystore for your environment (Should not matter if using jks or pem)\n<code>keytool -importkeystore -srckeystore cacerts.jks -destkeystore web.keystore </code>\nOr<br>\n<code>keytool -import -file cacert.pem -keystore web.keystore </code>\n\n2) Generate Certificate Signing Request (CSR) - Modifies keystore * keypass and storepass should match '''''https.keystore.password''''', source password is the cacerts password<br>\n\n<code>keytool  -genkeypair -keystore web.keystore -alias server -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -dname \"CN=...,OU=...,O=C=...\"</code>\n\n3) Through your Certificate Authority (CA) create a Certificate Request -> Certificate Type, MS CA WebServer to generate a Certificate<br>\n\n4) Import the certificate into the keystore<br>\n<code>keytool -import -v -trustcacerts -alias root -keystore web.keystore -file cert.cer -keypass [pass] -storepass [pass]</code>\n\n\n4b) In our experience the following command didn't work and was the cause of the above SSL-related errors<br>\n<code>keytool -importcert -keystore web.keystore -alias server -file cert.cer</code>\n\n= Memory Overload =\n\n== Overview ==\n<big>There are several places in AMI from where memory consumption info can be collected. Then a few steps can be taken to optimize the consumption.</big>\n\n\n\n==  How do we know if AMI is facing performance issues? ==\n\n<big>1.1 The AMI Data Statistics bar will turn red indicating AMI slowdown</big>\n[[File:Screenshot 2023-07-11 140228.png|thumb|left]]<br>\n\n\n<br>\n\n\n<big>1.2 Chart irregularity in the AMI Data Statistics Window</big>\n[[File:Screenshot 2023-06-20 160206.png|820px|thumb|left]]\n\n'''Keep in mind that the memory usage is shown as troughs (dips) in the chart. E.g. Tip of the chart = AMI memory usage + unused references (garbage). In other words, for a more accurate assessment of memory usage, look at the memory usage after the garbage collection finishes.'''\n\n\n\n<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>\n\n\n1. Upper Graph - memory footprint of the webserver\n<br>\n2. Bottom Graph - memory footprint of the center\n<br>\n3. This graph is designed so that the top of the graph is the maximum memory capacity. '''If the line gets near the top, AMI is closer to memory overload.''' <br>\n\n\n<big>1.3 AMI Log Viewer</big>\n\nSetup of Log Viewer can be found in 3forge documentation -> \n[[Guides#Log_Viewer_Layout]]\n\n* Upload the file AMIOne.amilog in the filename bar and open the ''Memory Details'' tab\n[[File:Screenshot 2023-07-11 115533.png|850px|thumb|left]]\n\n<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>\n\n1. The red line is the maximum memory capacity of AMI. If the Used Memory and Allocated Memory reaches to the max, it suggests AMI will face performance issues.\n\n2. The table in the panel below gives insightful details of the AMI like\n* Percentage of free memory\n* Values of maximum, allocated, and used memory.\n[[File:Screenshot 2023-07-11 115748.png|700px|thumb|left]]\n\n\n\n<br><br><br><br><br><br><br><br><br><br><br><br>\n\n\n \n\n3. We can use these values to better understand how much memory AMI used up and would need to let go.\n\n==Troubleshooting==\n<big>Data lives both in the webserver and the center. The most important step is to identify where the memory issue lies.\n</big>\n\n===  <big>Web Server </big>===\n<big>1. Open the ''Data Model Tables'' tab from the ''AMI Data Statistics'' Window in the upper right corner of AMI</big>\n[[File:Datamodel tables.png|700px|thumb|left]]\n<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>\n\nThis tab contains information of the tables created in the current webserver the user is in.\n* The Cells column should be looked at as it shows which table has the most data.\n* You can also view the count of rows and columns.\nLooking at the count of cells, we can recognize the most populated table from the web server to modify.\n\n====<big>SOLUTION</big> - Dropping a Table from the Web Server====\nData models often use intermediary tables which aren't used in any visualizations. These tables can be dropped at the end of the data model to reduce memory footprint.\n[[File:Droptablept2.png|900px|thumb|left]]\n\n\n\n\n\n\n<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>\n\n* In this example, the tables that are joined (price,quantity) are no longer needed in the data model as the joined table output will be used for visualizations.\n* Hence, tables price and quantity can be dropped.\n\n=== <big>Center</big> ===\n<big>1. Open the AMI DB Shell Tool from Dashboard</big><br>\nWe can view all the tables in the center through the command line by writing\n<syntaxhighlight lang=\"amiscript\">\nSHOW TABLES;\n</syntaxhighlight>\n[[File:Screenshot 2023-06-20 105849.png|850px|thumb|left]]\n\n<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>\n\n\n\n\nWe can view the row count of every table and find the most populated table in order to reduce memory footprint.\n\n\n<big>2. Diagnose a Table</big><br>\nFor deeper analysis, we can diagnose a table through the AMI shell tool by writing\n<syntaxhighlight lang=\"amiscript\">\nDIAGNOSE TABLE table_name;\n</syntaxhighlight>\n\n[[File:Screenshot 2023-06-20 111748.png|850px|thumb|left]]\n\n\n\n\n<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>\n\n\nThe DIAGNOSE table will show all the necessary information about each column related to memory consumption  (ignore TABLE_OVERHEAD)\n* EST_MEMORY shows the memory for each column.\n* COMMENT shows the datatypes and the byte size of each column.\n\n====<big>SOLUTION</big> -  Datatype Conversion====\nIf a string column exists in the table, it is recommended for the column to be stored as a '''STRING BITMAP''' or '''STRING ONDISK''' to reduce the high memory usage of a regular string column.\n\nMore info on efficient column types for a string can be found in our 3forge documentation ->   [[AMI_Realtime_Database#Choosing_the_Best_Column_Type_for_String]]\n\nIf data type conversion cannot be done then dropping a table from the center would be the last option.\n\n= Timeout expired while waiting for read lock =\n\n== Overview ==\n\nThis issue typically happens when there is a read request while AMIDB is processing a write request. AmiDB is designed to allow multiple reads but only single write at any given time. So while the write request is being processed, the incoming read request will be queued and when the timeout value is reached, it outputs the timeout exception.\n\n=== Recommendation ===\n\nThe most common cause is a slow running Timer or Trigger. A short term solution is to increase the timeout value so the read threads can wait longer. However, the long term solution would be to carefully inspect and optimize the code that is doing the write so it is performant enough to avoid the timeout issue in the first place. There are a few approaches we can use to identify the problematic Timer/Trigger:\n\n# Use AMIDB diagnostic queries ('''[[AMI_Realtime_Database#DIAGNOSE|diagnose tableName]]''', '''[[AMI_Realtime_Database#Realtime%20Tools|show timers]]''', show triggers) to find Timers/Triggers with higher average or max run time\n# Review the AmiOne.log by hand to see what Timer/Trigger was running when the timeout expired\n# Use the [[Guides#Log_Viewer_Layout|'''log viewer''']] dashboard to see the runtime for each Timer/Trigger. After setting up the dashboard and uploading the AmiOne.log file, navigate to the Timers and Triggers tabs and look for Timers/Triggers with significantly higher Total Run Time or significantly higher Run Time when the timeout expired\n\nOnce you have identified the slow running Timer or Trigger review the code to see what could be causing the slowness. Some common causes of inefficient Timers/Triggers include:\n\n* Calling large Update/Insert queries\n* Calling lots of Select/Insert/Update/Delete queries without an index on the columns in the WHERE clause\n* Calling expensive stored procedures\n\n= Table with CRUD Operations =\n\nCRUD stands for Create, Read, Update, Delete and describes the four key operations for editing the contents of a table. The guide below will show how to create a Realtime Table in AMI with CRUD operations that change the underlying table in the AMIDB.\n\n[[File:CRUD final.png]]\n\n== Read ==\n\nFirst, create a table by running the AmiScript below in '''Dashboard > AMIDB Shell Tool''':\n\n<syntaxhighlight lang=\"amiscript\">\nCREATE PUBLIC TABLE crudDemo(id long, v1 string, v2 string);\nINSERT INTO crudDemo VALUES (1,\"A\",\"O123\"),(2,\"A\",\"O124\"),(3,\"B\",\"O123\"),(4,\"A\",\"O121\");\n</syntaxhighlight>\n\nNext, create a new Window/Panel, then '''Create Realtime Table / Visualization'''. Select the Realtime Feed '''crudDemo''', click Next, then Finish. There should now be a Realtime Table with 3 columns and 4 rows.\n\n== Update ==\n\nClick on the table, then go to '''Settings''' and change \"In Table Editing\" to \"Single Row\" or \"Multiple Rows\":\n\n[[File:CRUD table settings.png|500px]]\n\nClick on the column header of \"Id\" and select '''Edit Column'''. Then scroll down and change the '''Edit''' setting to '''Readonly'''. Do the same for columns \"V 1\" and \"V 2\" setting them to '''Text Field'''.\n\n[[File:CRUD column settings.png|500px]]\n\nClick on the table, then go to '''AmiScript Callbacks...''' and navigate to the '''onEdit(vals, oldVals)''' tab and add in the following AmiScript:\n\n<syntaxhighlight lang=\"amiscript\">\nfor (row r: vals.getRows()) {\n  String updateString = \"\";\n  for (String key: r.getKeys()) {\n    updateString += key + \"=\\\"\" + r.get(key) + \"\\\", \";\n  }\n  updateString = updateString.beforeLast(\",\",true);\n  use ds=\"AMI\" execute update crudDemo set ${updateString} where id==${r.get(\"id\")};\n}\n</syntaxhighlight>\n\n== Create ==\n\nClick on the table, then go to '''Custom Menus > Add Menu Item''', set Display to \"Add Row\", then go to the '''Callbacks''' tab and add in the following AmiScript:\n\n<syntaxhighlight lang=\"amiscript\">\nuse ds=\"AMI\" execute insert into crudDemo from select max(id)+1,\"\",\"\" from t;\n</syntaxhighlight>\n\n== Delete ==\n\nGo to '''Custom Menus > Add Menu Item''', set Display to \"Delete Row(s)\", then go to the '''Callbacks''' tab and add in the following AmiScript:\n\n<syntaxhighlight lang=\"amiscript\">\nTablePanel  t = this.getOwner();\nfor (Row r: t.getSelectedRows()) {\n  use ds=\"AMI\" execute delete from crudDemo where id==\"${r.get(\"id\")}\";\n}\n</syntaxhighlight>\n\n= Logging =\n== Turn off logging for a particular class ==\nAny time you wish to mute certain messages from showing up in your AmiOne.log, you can use the following template, replace '''CLASSNAME''' with the full class name, and put that inside your local.properties file:\n\n<syntaxhighlight>\nspeedlogger.stream.CLASSNAME=BASIC_APPENDER;FILE_SINK;OFF\n</syntaxhighlight>\n\n''Note that each line turns off logging for a single class only.''\n\n<br>\nHere is an example of extracting the class name from AmiOne.log:\n\nINF 20231108-13:32:25.009 EST5EDT [F1POOL-04] <span style='color:blue;font-weight:bold;'>com.f1.ami.amicommon.centerclient.AmiCenterClientState</span>::onUserSnapshotRequest: localhost:3341 (seqnum=721) User demo requested: [team] of which [team] will be requested from the Ami Center\n\n\n== Adjusting log levels in AMI ==\n\n{| class=\"wikitable\"\n|-\n! Code !! Full label\n|-\n| ALL || All\n|-\n| FNE || Fine\n|-\n| INF || Info\n|-\n| WRN || Warning\n|-\n| SVR || Severe\n|-\n| OFF || OFF\n|}\n\n\nYou may adjust the log level for a certain class of logs by replacing the part after the last semicolon with the code in the above table:\n\nspeedlogger.stream.CLASSNAME=BASIC_APPENDER;FILE_SINK;<span style='color:red'>WRN</span>\n\n== Logging Configuration ==\nThe following set of configuration allows you to redirect logging to Stdout:\n===Basic===\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.appender.BASIC_APPENDER.pattern</span>=%P %d{YMD-h:m:s.S z} [%t] %c::%M: %m %D%n\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.appender.BASIC_APPENDER.timezone</span>=EST5EDT\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.appender.BASIC_APPENDER.type</span>=BasicAppender\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.sink.FILE_SINK.type</span>=file\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.sink.FILE_SINK.fileName</span>=${f1.logs.dir}/${f1.logfilename}.log\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.sink.FILE_SINK.maxFiles</span>=10\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.sink.FILE_SINK.maxFileSizeMb</span>=1000\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.stream.</span>=BASIC_APPENDER;FILE_SINK;INFO\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.stream.AMI</span>=BASIC_APPENDER;FILE_SINK;OFF\n\n===AmiScript===\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.appender.SIMPLE_APPENDER.pattern</span>=%P %d{YMD-h:m:s.S z} [%t] %c: %m %D%n\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.appender.SIMPLE_APPENDER.timezone</span>=EST5EDT\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.appender.SIMPLE_APPENDER.type</span>=BasicAppender\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.stream.AMISCRIPT.LOGINFO</span>=SIMPLE_APPENDER;FILE_SINK;INF\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.stream.AMISCRIPT.LOGWARN</span>=SIMPLE_APPENDER;FILE_SINK;WRN\n\n===Backend Messages===\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.appender.AMIMESSAGES_APPENDER.pattern</span>=%d{YMD-h:m:s.S z} %m%n\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.appender.AMIMESSAGES_APPENDER.timezone</span>=EST5EDT\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.appender.AMIMESSAGES_APPENDER.type</span>=BasicAppender\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.sink.AMIMESSAGES_SINK.type</span>=file\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.sink.AMIMESSAGES_SINK.fileName</span>=${f1.logs.dir}/AmiMessages.log\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.sink.AMIMESSAGES_SINK.maxFiles</span>=10\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.sink.AMIMESSAGES_SINK.maxFileSizeMb</span>=1000\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.stream.AMI_MESSAGES</span>=AMIMESSAGES_APPENDER;AMIMESSAGES_SINK;INFO\n\n===AMI Statistics===\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.appender.AMISTATS_APPENDER.pattern</span>=%m%n\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.appender.AMISTATS_APPENDER.type</span>=BasicAppender\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.sink.AMISTATS_SINK.type</span>=file\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.sink.AMISTATS_SINK.fileName</span>=${f1.logs.dir}/${f1.logfilename}.amilog\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.sink.AMISTATS_SINK.maxFiles</span>=10\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.sink.AMISTATS_SINK.maxFileSizeMb</span>=1000\n* <span style=\"font-family: courier new; color: blue;\">speedlogger.stream.AMI_STATS</span>=AMISTATS_APPENDER;AMISTATS_SINK;ALL\n\n==Redirecting log to another location==\nTo redirect all logging('''AmiOne.log'''/'''AmiMessages.log'''/'''AmiOne.amilog''') to another location, you will need to override the below property in local.properties to point to the new folder:\n<span style=\"font-family: courier new; color: blue;\">f1.logs.dir=absolute/or/relative/path/to/new/folder\n\n= Controlling layouts with URL parameters =\n\nThe <code>onUrlParams(Map params)</code> callback can be used to control a dashboard based on the URL parameters. For this example we'll be assuming the base URL for accessing AMI is <code>localhost:33332</code>. First, create a new window, click on the green icon, then select '''Place Highlighted In Tab''':\n\n[[File:Url place highlighted.png]]\n\nSelect the green icon for the Tabs and select \"Add Tab\":\n\n[[File:Url new tab.png]]\n\nClick on the dropdown arrow for the first tab to access the tab's settings then Change both the title and id for the first tab. Repeat this for the second tab:\n\n[[File:Url tab settings.png]]\n\n[[File:Url rename tab options.png]]\n\nNow that we have two tabs, navigate to '''Dashboard > Custom Callbacks''' then navigate to the <code>onUrlParams(Map params)</code> callback. Note that you may have to use the arrows on the right hand side to scroll to the `onUrlParams(params)` callback.\n\nFinally, we can import the tabs using the right hand menu and add the following AMIScript to select tabs based on the URL parameters:\n\n<syntaxhighlight lang=\"amiscript\">\nString selectedTab = params.get(\"tab\");\n\nif (selectedTab == \"first\") {\n  Tabs1.setSelectedTab(first_tab);\n} else if (selectedTab == \"second\") {\n  Tabs1.setSelectedTab(second_tab);\n}\n</syntaxhighlight >\n\nThe final callback should appear as shown below:\n\n[[File:Url callback.png]]\n\nFinally, we can test the URL by accessing <code>http://localhost:33332/3forge?tab=first</code> and <code>http://localhost:33332/3forge?tab=second</code>."
                    }
                ]
            }
        }
    }
}