How to Use the VisualScript Beta

VisualScript consists of 3 elements:

  1. The VisualScript app
  2. The VisualScript Gadget (Jira)
  3. The VisualScript Macro (Confluence)

VisualScript for Jira

Install VisualScript for Jira

Log-in as a Jira admin and visit the Jira admin dashboard

  • Click Jira Settings, then Add-ons, and finally Manage Add-ons.
  • If you do not already, allow non-marketplace apps to be installed.
  • Click Upload Add-on at the top right side of the page.
  • The Upload Add-on dialog will display
  • Click Browse and locate the VisualScript jar file. Click Upload.
  • A confirmation message appears when VisualScript is successfully installed.

Adding the Gadget

Once the VisualScript add-on is installed, a new dashboard gadget will be made available.

  • From a dashboard, click Add Gadget.
  • The Add Gadget dialog will appear, search for VisualScript.
  • In the VisualScript item, click Add gadget.

Using the Gadget

For the beta the gadget has only one input, a URL field. In the field, enter the URL for a REST endpoint and click set URL.

If your endpoint is functioning properly, your report will display.

VisualScript for Confluence

Install VisualScript for Confluence

  • Log-in as a Confluence admin and visit the Confluence admin console
  • Click the Manage Add-ons.
  • If you do not already, allow non-marketplace apps to be installed.
  • Click Upload Add-on at the top right side of the page.
  • The Upload Add-on dialog will display.
  • Click Browse and locate the VisualScript jar file. Click Upload.
  • A confirmation message appears when VisualScript is successfully installed.

Adding the Macro

Once the VisualScript add-on is installed, a new macro will be available as well as a new page template. To get started, we will use the page template.

  • Click Add Page.
  • From the Add Page dialog, choose VisualScript from the list of templates.
  • A new page will be created with a VisualScript macro already inserted.

Using the Macro

For the beta the macro has only one property, a URL field.

Double-click the macro title to display the URL field. In the field, enter the URL for a REST endpoint and click ok.

Publish your page to set the macro.

If your endpoint is functioning properly, your visual will display.

Configure a REST endpoint

You may use any REST endpoint that is configured to return SDON. Read the SDON Cookbook to learn more about SDON.

In this tutorial we will use ScriptRunner for Jira to setup the REST endpoint.

  • From the Admin dashboard, click Add-ons.
  • Locate ScriptRunner in the left navigation panel, and click REST endpoints.
  • Click Add New Item, then Custom Endpoint.
  • Give it a descriptive name, we suggest "VisualScript Hello World".
  • Copy and paste the following sample script into the inline script panel:
  • /*
    This SR4J script is to visualise the relationships between a base issue and the linked epic and the linked different issues
    and render the results as SDON.
     
    This is developed to work with the headless plugin for SmartDraw available from xxxx for Jira and yyyy for Confluence.
     
    Date: 29/May/2018
    Developers:  Johnson Howard and Phill Fox
     
    */
     
    import com.atlassian.jira.bc.issue.link.IssueLinkService
    import com.atlassian.jira.component.ComponentAccessor
    import com.atlassian.jira.config.properties.APKeys
    import com.atlassian.jira.issue.CustomFieldManager
    import com.atlassian.jira.issue.Issue
    import com.atlassian.jira.issue.IssueManager
    import com.atlassian.jira.issue.link.IssueLinkManager
    import com.atlassian.jira.security.JiraAuthenticationContext
    import com.onresolve.scriptrunner.runner.rest.common.CustomEndpointDelegate
    import groovy.json.JsonBuilder
    import groovy.transform.BaseScript
    import groovy.transform.Field
     
    import javax.ws.rs.core.MultivaluedMap
    import javax.ws.rs.core.Response
     
    @Field String inwardName = "Inward"  //Change this to the name of the inward issueLink you wish to map
    @Field String outwardName = "Outward"   //Change this to the name of the outward issueLink you wish to map
    @Field String linkName = "Outward"   //Change this to the name of the issueLink you wish to map
    @Field String epicLinkName = "Epic Link"
    @Field String normalCellFillHex = "#B2D4FF"
    @Field String labelFillHex = "#000000"
    @Field String returnFillHex = "#2684FF"
    @Field String cellLabelFillHex = "#0065FF"
    @Field String epicCellLabelFillHex = "#FFFFFF"
    @Field int centralIssueColumn = 3
    @Field int centralIssueRow = 3
    @Field int tableRows = 4
    @Field int tableColumns = 3
    @Field int tableHeight = 95
    @Field int tableWidth = 150
    @Field String baseURL = ComponentAccessor.getApplicationProperties().getString(APKeys.JIRA_BASEURL)
     
    import org.apache.log4j.Level
    import org.apache.log4j.Logger
    // Setup the log and leave a message to show what we're doing
    Logger logger = log
    logger.setLevel( Level.ALL )
     
    @BaseScript CustomEndpointDelegate delegate
     
    // end point, collects the parameters from the request and begins the script
    doSdon(httpMethod: "GET") { MultivaluedMap queryParams, String body ->
        def issuekey = queryParams.getFirst("issueKey")
        linkName = queryParams.getFirst("linkName")
        setLinkNames(linkName)
        return Response.ok(new JsonBuilder(doScript(issuekey)).toString()).build()
    }
     
    // calls all the necessary methods to complete task
    def doScript(def issueKey){
     
        // get the necessry services and managers
        IssueLinkManager issueLinkManager = ComponentAccessor.getIssueLinkManager()
        IssueManager issueManager = ComponentAccessor.getIssueManager()
        CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager()
     
        //get the issue and the epic, add to the respective arrays in order to pass to the card creating methods
        Issue issue = issueManager.getIssueByCurrentKey(issueKey)
        def mainIssue = [issue]
        def epic = issueManager.getIssueByCurrentKey(getEpic(issue, customFieldManager))
        def epicIssue = [epic]
        // get the outward and inward links for the main issue
        def inwardIssues = getInwardLinkedIssues(issue,inwardName, issueLinkManager)
        def outwardIssues = getOutwardLinkedIssues(issue,outwardName, issueLinkManager)
     log.debug("outwardissues: "+outwardIssues)
        // build the meta data, root shape and table maps
        def rootShape = []
        def sdonMap = getSdonMap('1.1.20','SmartDrawSDON' , 'SQL')
        def rootShapeMap = getRootShapeMap('None','Horizontal' )
        def tableProperties = getTableProperties(tableRows,tableColumns,tableWidth,tableHeight)
     
        // add data to the row and column header cells
        def cells = []
        cells.add(getCell(2,1, 'Epic', labelFillHex, 12))
        cells.add(getCell(1,1, 'SDON DEMO',labelFillHex , 15))
        cells.add(getCell(3,1, 'Tasks', labelFillHex , 12))
        cells.add(getCell(1,2, inwardName, labelFillHex , 12))
        cells.add(getCell(1,3, 'Epic and Central Issue', labelFillHex , 12))
        cells.add(getCell(1,4, outwardName, labelFillHex , 12))
     
        // add the cell shapes for the inward, outward, main and epic issues
        inwardIssues ? cells.add(getCardCell(inwardIssues,centralIssueRow - 1, centralIssueColumn)) : null
        mainIssue ? cells.add(getCardCell(mainIssue,centralIssueRow,centralIssueColumn)) : null
        outwardIssues ? cells.add(getCardCell(outwardIssues,centralIssueRow + 1,centralIssueColumn)) : null
        epic ? cells.add(getCardCell(epicIssue,centralIssueRow ,centralIssueColumn - 1)) : null
     
        // add the meta data, root shape, table properties and cells to the map
        tableProperties.put('Cell',cells)
        rootShapeMap.put('Table',tableProperties)
        rootShape.add(rootShapeMap)
        sdonMap.put('RootShape',rootShape)
     
        // get the return maps for the outward, inward and epic cells
        def outwardReturns = outwardIssues ? getReturns(outwardIssues,mainIssue.first(), centralIssueColumn, centralIssueRow + 1,'outward') : null
        def inwardReturns = inwardIssues ? getReturns(inwardIssues,mainIssue.first(), centralIssueColumn, centralIssueRow -1,'inward') : null
        def epicReturn =  epic ?getReturns(mainIssue, epicIssue.first(), centralIssueColumn - 1, centralIssueRow -1,'') : null
     
        // collect the non-empty maps together and add to the map
        def returns = collectReturns(inwardReturns,outwardReturns,epicReturn)
        sdonMap.put('Returns',returns)
     
        return sdonMap
    }
     
    // build the meta data map
    def getSdonMap(def version, def signature, def diagramType){
        def sdonMap = ['Version': version,'Signature':signature,'DiagramType':diagramType]
        return sdonMap
    }
     
    // join all non-empty return maps together
    def collectReturns(def inwardReturns, def outwardReturns, def epicReturn){
        def returns = []
        returns = inwardReturns ? inwardReturns : returns
        returns = outwardReturns ? outwardReturns + returns : returns
        returns = epicReturn ? epicReturn + returns : returns
        return returns
    }
     
    //build the root shape map
    def getRootShapeMap(def fillColour, def textGrow){
        def rootShapeMap = ['FillColor':fillColour,'TextGrow':textGrow]
        return rootShapeMap
    }
     
    // build the table properties map and add the joins to it
    def getTableProperties(def rows, def columns , def width, def height){
        def tableProperties = ['Rows':rows,'Columns':columns, 'ColumnWidth':width, 'RowHeight':height, 'Join' : getJoin()]
        return tableProperties
    }
     
    // build the column and row joins map
    def getJoin(){
        def joins = []
        def join = ['Row': centralIssueRow - 1, 'Column': centralIssueColumn -1,'N':tableRows - 1,'Down':'1']
        joins.add(join)
        return joins
    }
    // get a single cell
    def getCell(def columnNumber, def rowNumber, def label, def textColour, def textSize){
        def cardCell = ['Column':columnNumber, 'Row':rowNumber, 'Label':label, 'TextColor':textColour, 'TextSize':textSize]
        return cardCell
    }
     
    // for each shape (issue) in the list, create a cell shape map and add it to the card cell shape map
    def getCardShapeList(def shapes){
        def cardCellShapesList = []
        shapes.each{ shape ->
            def label = shape?.key ? shape.key +'\n'+ shape?.summary : "No Issue found"
            def id = shape?.id ? shape.id : "No ID found"
     
            def cardCellShape = ['Label':label, 'ID':id, 'ShapeType':'RRect', 'FillColor':getCardColour(shape), 'TextColor':getCardLabelColour(shape)]
            def url = [:]
            url.put("url","${baseURL}/browse/${shape?.key}")
            cardCellShape.put('Hyperlink', url)
            cardCellShapesList.add(cardCellShape)
        }
        return cardCellShapesList
    }
     
    // get the card cell layout map
    def getCardCellLayout(def cardCellShapesList){
        def cardCellLayout = ['Arrangement':'Grid', 'ArrayAllignH':'center', 'ArrayAlignV':'center', 'Shapes':cardCellShapesList]
        return cardCellLayout
    }
     
    // get the card cell shape map
    def getCardCellShape(def cardCellLayout){
        def cardCellShape = ['Hide':'true', 'ShapeArray':cardCellLayout]
        return cardCellShape
    }
     
    // get a single card cell
    def getSingleCardCell(def cardCellShape, def column, def row){
        def cardCell = ['Column':column, 'Row':row, 'Shape':cardCellShape]
        return cardCell
    }
     
    // get card cell with all the built shapes added to it
    def getCardCell(def issues, def row, def column){
        def cardShapeList = getCardShapeList(issues)
        def cardCellLayout = getCardCellLayout(cardShapeList)
        def cardCellShape =  getCardCellShape(cardCellLayout)
        def cardCell =  getSingleCardCell(cardCellShape, column, row)
        return cardCell
    }
     
    // build the returns map
    def getReturns(def destinationIssues, def centralIssue, def column, def row, def label){
        def returns = []
        destinationIssues.eachWithIndex{ destinationIssue, idx ->
            returns.add(getReturnLink(centralIssue.id, destinationIssue.id, returnMapping(idx,column,row).get('startDirection'),
                                      returnMapping(idx,column,row).get('endDirection'),label))
     
        }
        return returns
    }
     
    // build a single return with the mapped start and end direction depending on position in table
    def getReturnLink(def sourceId, def destinationId, def startDirection, def endDirection, def label){
        def returnLink = ['StartID':sourceId, 'EndID':destinationId, 'LineColor': returnFillHex]
        def returnLinkMaps = ['StartDirection': startDirection, 'EndDirection': endDirection, 'Curved':'True', 'LineThick' : '2', Label:label ]
        returnLink.putAll(returnLinkMaps)
        return returnLink
    }
     
    // mapping for where the return should start and end on the two ajoining cells
    def returnMapping(def index, def column, def row){
     
        def startAndEndDirection = ['startDirection': '', 'endDirection' : '']
     
        if( index == 0 && column == centralIssueColumn && row == centralIssueRow - 1 ) {
            startAndEndDirection.put('startDirection', 'Top')
            startAndEndDirection.put('endDirection', 'Bottom')
        }else if( index == 0 && column == centralIssueColumn && row == centralIssueRow + 1) {
            startAndEndDirection.put('startDirection', 'Bottom')
            startAndEndDirection.put('endDirection', 'Top')
        }else{
            startAndEndDirection.put('startDirection', 'Right')
            startAndEndDirection.put('endDirection', 'Left')
        }
     
        return startAndEndDirection
    }
     
    // set the link name global variables
    def setLinkNames(def linkName){
        def issueLink = ComponentAccessor.getComponent(IssueLinkService.class).getIssueLinkTypes().find{it.name == linkName}
        inwardName = issueLink?.inward
        outwardName = issueLink?.outward
    }
     
    // get inward links of an issue
    def getInwardLinkedIssues(Issue issue, def linkName, IssueLinkManager issueLinkManager){
        def issueLinks = issueLinkManager.getInwardLinks(issue?.id).findAll{ it.issueLinkType.inward == linkName}
        return issueLinks.collect{it.sourceObject}
    }
     
    //get outward links of an issue
    def getOutwardLinkedIssues(Issue issue, def linkName, IssueLinkManager issueLinkManager){
        def issueLinks = issueLinkManager.getOutwardLinks(issue?.id).findAll{ it.issueLinkType.outward == linkName}
        issueLinks.collect{it.destinationObject}
    }
     
    // get the epic key of an issue
    def getEpic(Issue issue, CustomFieldManager customFieldManager){
        def epicField = issue?.getCustomFieldValue(customFieldManager.getCustomFieldObjectsByName(epicLinkName)?.first())
        return epicField ? epicField.getKey() : null
    }
     
    // get the colour of the text labels for a card, varies depending on if it's the epic card or not
    def getCardLabelColour(def issue){
        def colourMap = getEpicColourHexMap()
        return  issue.issueType.name == "Epic" ? epicCellLabelFillHex : cellLabelFillHex
    }
     
    // get the colour of the card, varies depending on if it's the epic card or not
    def getCardColour(def issue){
        def colourMap = getEpicColourHexMap()
        return  issue.issueType.name == "Epic" ? colourMap.get(getEpicColourHex(issue)) : normalCellFillHex
    }
     
    // mapping for epic colour to hex value
    def getEpicColourHexMap(){
        return [
            'ghx-label-1':'#815b3a',
            'ghx-label-2':'#f79132',
            'ghx-label-3':'#d39d3f',
            'ghx-label-4':'#3b80c4',
            'ghx-label-5':'#4a6785',
            'ghx-label-6':'#8fb021',
            'ghx-label-7':'#ac707a',
            'ghx-label-8':'#644982',
            'ghx-label-9':'#f15c75']
     
    }
     
    // get the epic colour fied value of an issue
    def getEpicColourHex(def issue){
        CustomFieldManager customFieldManager = ComponentAccessor.getCustomFieldManager()
        def epicColour = issue?.getCustomFieldValue(customFieldManager.getCustomFieldObjectsByName("Epic Color")?.first())
        return epicColour
    }
    
  • Click add
  • Right click on the new endpoint and select copy link