Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
arijit_das
Active Contributor

In my previous post, I described how to create a blinking label add-on for SAP BusinessObjects Dashboards 4.0. In this post, I shall describe how to create an On-Off toggle button add-on.

You must be thinking that as we already have a toggle button component in the tool, why to invest time on creating the same component again :???: . Actually recently I was watching some videos in Youtube on how to create custom skins for flex components. Being inspired by those, I tried to create a very simple add-on for SAP BusinessObjects Dashboards using custom skin in Flash Builder. Now, how does this differ from the existing toggle button component ? The component will actually look like the on-off button we see in an iPhone or iPad. The on/off state can be toggled by sliding the thumb :smile: .

To keep it simple, I have kept only following features :

1. State Insertion : We can bind a cell in the excel model to insert the current state of the toggle button.

2. Default State : We can select a default state for the component.

3. Dynamic Visibility : We can specify a condition to show/hide the component.

The steps for creating this component are described below:

  • Create the skin using Adobe Fireworks CS6 and Adobe Flash Catalyst CS5.5.
  • Create the component in Flash Builder.
  • Create the property sheet in Flash Builder.
  • Create the add-on file using Dashboards Add-On Packager.

Creating the Skin

Actually this component is a Horizontal Slider component in Flex with two states - 0 and 1 and having a step-size of 1. Our first task is to create the skin for the flex HSlider component.

1. Launch Adobe Fireworks and create a new document.

2. Use a Rectangle Tool to draw a rectangle in the canvas.

3. Select the rectangle and apply following properties:

     Width: 100

     Height: 35

     Fill Color: #0000FF

     Border Color: #00008C

     Tip size: 2

     Stroke category: Soft Rounded

     Roundness: 10px

4. Create a new Inner Shadow Filter for the rectangle as below:

5. Now draw a label 'ON' with the Text Tool. Make the width 50 and place it on top of the rectangle.

6. Similarly, create another label 'OFF' as below:

7. Group the 3 layers and give a name Track.

8. Till now, we have created the track of the slider. Now it is time to create the thumb which will slide.

     Create another rectangle with following properties:

     Width: 50

     Height: 35

     Fill Color: #EEEEEE

     Border Color: #757584

     Tip size: 1

     Stroke category: Soft Rounded

     Roundness: 10px

9. Add one Inner Glow Filter for the thumb like below:

10. Rename the new rectangle layer to Slider.

11. Place the thumb on top of track and check if it fits properly.

12. Click on File > Export and save in FXG format. Here I have saved the file as slider.fxg.

13. Now launch Adobe Flash Catalyst and create a new project.

14. Click File > Import > Adobe FXG File (.fxg)...

15. Import the slider.fxg file created by Adobe Fireworks.

16. Select two layers together and choose Horizontal Slider from the drop-down as shown:

17. Give a name as CustomSliderSkin.

18. Click Edit Parts.

19. Select Normal state.

20. Select Track layer and click Track (recommended) as shown:

21. Similarly, select Slider layer and click on Thumb (recommended).

22. Go to Project Library tab and click on Export Library Package button.

23. Save with name CustomSliderSkin.fxpl.

24. We have successfully created the skin for the component. Now in the next step we shall use this skin in Flash Builder to build the component.

Creating the component

1. Launch Flash Builder and click File > Import > Flash Builder Project...

2. Import the CustomSliderSkin.fxpl file we just created.

3. Create a new Flex Project with name OnOffButton.

4. In the properties of the project go to Flex Build Path section.

5. Choose Framework linkage as Merged into code. Click on Add project... button and select CustomSliderSkin. Click OK.

6. Under Flex compiler section, provide following command in Additional compiler arguments:

-locale en_US -isolate-styles=false -static-link-runtime-shared-libraries=true


7. Use Flex SDK 4.0 to compile the project.

8. Under src folder create a package with name com.yahoo.ari007_cse. Inside that, create an actionscript file with name OnOffButton.as.

9. Open the actionscript file and paste the code below:

OnOffButton.as

/*

------------------------------------------------------------------

ActionScript file: OnOffButton.as

Author: Arijit Das

Date Created: Jan 20, 2013

------------------------------------------------------------------

*/

package com.yahoo.ari007_cse

{

    import components.MySlider;

   

    import flash.events.Event;

   

    import mx.containers.HBox;

    import mx.events.FlexEvent;

    import mx.styles.CSSStyleDeclaration;

    import mx.styles.StyleManager;

   

    import spark.components.HSlider;

   

    public class OnOffButton extends HBox{

        /*

        ------------------------------------------------------------------

        Properties

        ------------------------------------------------------------------

        */

        private var _slider:HSlider=new HSlider();

       

        private var _defaultState:String="ON";

        private var _defaultStateChanged:Boolean=true;       

        private var _dynamicVisibility:Boolean=false;

        private var _visibleStatus:String=null;

        private var _visibleKey:String=null;

        private var _visibleKeyChanged:Boolean=false;

        private var _visibleStatusChanged:Boolean=true;               

       

        /*

        ------------------------------------------------------------------

        Constructor   

        ------------------------------------------------------------------

        */       

        public function OnOffButton():void{

            super();

        }

       

        /*

        ------------------------------------------------------------------

        Static Function to determine if the component is being

        used at design time       

        ------------------------------------------------------------------

        */       

        public static function isInCanvas():Boolean{           

            var globalStyle:CSSStyleDeclaration=StyleManager.getStyleManager(null).getStyleDeclaration("global");

            if (Boolean(globalStyle.getStyle("inCanvas"))==true){

                return true;

            }

            return false;

        }

       

        /*

        ------------------------------------------------------------------

        When component is created, set visibility depending on

        property values

        ------------------------------------------------------------------

        */

        public function cretionCompleteHandler(event:Event):void{

            if(!(_dynamicVisibility) || _visibleStatus==visibleKey || (_visibleStatus==null && visibleKey=="")){

                this._slider.enabled=true;

                this.visible=true;

            }

            else{

                this._slider.enabled=false;

                this.visible=false;

            }

        }

       

        /*

        ------------------------------------------------------------------

        defaultState Property

        ------------------------------------------------------------------

        */       

        public function set defaultState(st:String):void{

            if(_defaultState != st){

                _defaultState=st;

                _defaultStateChanged=true;

                invalidateProperties();

            }

        }

        public function get defaultState():String{

            return _defaultState;

        }       

        /*

        ------------------------------------------------------------------

        visibleKey Property

        ------------------------------------------------------------------

        */       

        public function set visibleKey(key:String):void{

            _visibleKey=key;

            _visibleKeyChanged=true;

            invalidateProperties();

        }

        public function get visibleKey():String{

            return _visibleKey;

        }

        /*

        ------------------------------------------------------------------

        visibleStatus Property

        ------------------------------------------------------------------

        */           

        public function set visibleStatus(status:String):void{

            _visibleStatus=status;

            _visibleStatusChanged=true;

            invalidateProperties();

        }

        public function get visibleStatus():String{

            return _visibleStatus;

        }

        /*

        ------------------------------------------------------------------

        dynamicVisibility Property

        ------------------------------------------------------------------

        */           

        public function set dynamicVisibility(flag:Boolean):void{

            _dynamicVisibility=flag;

        }

        public function get dynamicVisibility():Boolean{

            return _dynamicVisibility;

        }

        /*

        ------------------------------------------------------------------

        state Property

        ------------------------------------------------------------------

        */

        [Bindable (event="valueChanged")]

        public function get state():String{

            return (this._slider.value==0?"OFF":"ON");

        }

       

        /*

        ------------------------------------------------------------------

        Dispatch event when state changes

        ------------------------------------------------------------------

        */

        public function stateChangedHandler(event:Event):void{           

            this.dispatchEvent(new Event("valueChanged"));

        }

       

        /*

        ------------------------------------------------------------------

        Build composite component

        ------------------------------------------------------------------

        */       

        override protected function createChildren():void{

            super.createChildren();           

           

            this._slider.value=(this._defaultState == "ON" ? 1 : 0);

            this._slider.setStyle("skinClass",components.CustomSliderSkin);

            this._slider.maximum=1;

            this._slider.minimum=0;

            this._slider.stepSize=1;

            this._slider.addEventListener(Event.CHANGE,stateChangedHandler);

            this.addChild(_slider);

           

            this.minWidth=100;

            this.minHeight=35;

           

            this.addEventListener(FlexEvent.CREATION_COMPLETE,cretionCompleteHandler);

            if(!(_dynamicVisibility) || _visibleStatus==_visibleKey || (_visibleStatus==null && _visibleKey=="")){

                dispatchEvent(new Event("valueChanged"));

            }

        }

       

        /*

        ------------------------------------------------------------------

        Action taken after change in any property

        ------------------------------------------------------------------

        */       

        override protected function commitProperties():void{

            super.commitProperties();

           

            /*

            -------------------------------------------------------------------

            Check whether to update the default state

            -------------------------------------------------------------------

            */

            if (this._defaultStateChanged){

                this._slider.value=(this._defaultState == "ON" ? 1 : 0);

                invalidateDisplayList();

                _defaultStateChanged=false;

            }

           

            /*

            -------------------------------------------------------------------

            Check whether to show/hide the component

            -------------------------------------------------------------------

            */           

            if(this._visibleKeyChanged || this._visibleStatusChanged){

                if(!isInCanvas()){

                    if(!(_dynamicVisibility) || _visibleStatus==_visibleKey || (_visibleStatus==null && _visibleKey=="")){

                        this._slider.enabled=true;

                        this.visible=true;

                        this.dispatchEvent(new Event("valueChanged"));

                    }

                    else {

                        this._slider.enabled=false;

                        this.visible=false;

                    }

                    invalidateDisplayList();

                }

                _visibleKeyChanged=false;

                _visibleStatusChanged=false;

            }           

        }       

    }

}

10. Open OnOffButton.mxml file under default package and paste the code below:

OnOffButton.mxml

<?xml version="1.0" encoding="utf-8"?>

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"

               xmlns:s="library://ns.adobe.com/flex/spark"

               xmlns:mx="library://ns.adobe.com/flex/mx" xmlns:ns="com.yahoo.ari007_cse.*">

    <ns:OnOffButton height="100%" width="100%" />

</s:Application>

11. Save all files and click File > Export Release Build. The component is created successfully.

Creating Property Sheet

1. Create a new Flex Project as OnOffButtonPropertySheet.

2. Go to project properties. Under Flex Build path, click on Add SWC... and browse to the file xcelsiusframework.swc located in <Dashboards_Installation_Dir>\Xcelsius 4.0\SDK\bin.

3. Use Flex SDK 4.0 to compile the project and use additional compiler argument :

     -locale en_US -isolate-styles=false

4. Open OnOffButtonPropertySheet.mxml file under default package and paste the code below:

OnOffButtonPropertySheet.mxml

<?xml version="1.0" encoding="utf-8"?>

<!--

File Name:        OnOffButtonPropertySheet.mxml

Author:            Arijit Das

Date Created:    Jan 20, 2013

-->

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"

                applicationComplete="init();"  backgroundColor="#F7F9FC" backgroundAlpha="1.0" preloaderChromeColor="#FEFEFF">

   

    <mx:Script>

        <![CDATA[

            import mx.containers.*;

            import mx.controls.*;

            import mx.core.Container;

            import mx.events.FlexEvent;

           

            import xcelsius.binding.BindingDirection;

            import xcelsius.binding.tableMaps.input.InputBindings;

            import xcelsius.binding.tableMaps.output.OutputBindings;

            import xcelsius.propertySheets.impl.PropertySheetExternalProxy;

            import xcelsius.propertySheets.interfaces.IPropertySheetProxy;

            import xcelsius.propertySheets.interfaces.PropertySheetFunctionNamesSDK;

           

            /*

            -----------------------------------------------------------------------------

            The Xcelsius proxy between this Property Sheet and its Xcelsius

            custom component.

            -----------------------------------------------------------------------------

            */           

            protected var proxy:IPropertySheetProxy = new PropertySheetExternalProxy();       

            /*

            -----------------------------------------------------------------------------

            The name of the property that the user is (re-)binding.

            -----------------------------------------------------------------------------

            */

            protected var propertyToBind:String;

            /*

            -----------------------------------------------------------------------------

            The current binding id (may be null if there is no binding) for

            the    property the user is (re)-binding.

            -----------------------------------------------------------------------------

            */

            protected var currentBindingID:String;

           

            /*

            -----------------------------------------------------------------------------

            Default State Choice

            -----------------------------------------------------------------------------

            */

            [Bindable]

            protected var _state:Array = new Array("ON","OFF");

       

           

            /*

            -----------------------------------------------------------------------------

            Initialises this Property Sheet on load.

            -----------------------------------------------------------------------------

            */

            protected function init():void

            {

                proxy.addCallback(PropertySheetFunctionNamesSDK.GET_PROPERTIES_FUNCTION, getProperties);

                proxy.addCallback(PropertySheetFunctionNamesSDK.RESPONSE_BINDING_ID, this.continueBind);

                proxy.callContainer(PropertySheetFunctionNamesSDK.INIT_COMPLETE_FUNCTION);

               

       

                loadProperties();               

                initValues();

            }   

           

            protected function getProperties():Array

            {

                var persist:Array = new Array();

                var persistObject:Object = new Object();

                persistObject.name = "selectedTab";

                persistObject.value = viewstack1.selectedIndex;

                persist.push(persistObject);

               

                return persist;

            }

           

            /*

            -----------------------------------------------------------------------------

            Load the selected tab

            -----------------------------------------------------------------------------

            */

            protected function loadProperties():void

            {               

                var persistProperties:Array = proxy.getPersist(["selectedTab"]);

                if (persistProperties != null && persistProperties.length != 0)

                {

                    var propertyObject:Object = persistProperties[0];                                       

                    var propertyValue:* = propertyObject.value;

                    viewstack1.selectedIndex = propertyValue as int;

                }

            }

           

            /*

            -----------------------------------------------------------------------------

            Initialises this Property Sheet on load to show the current

            Xcelsius custom component property/style value

            -----------------------------------------------------------------------------

            */           

            protected function initValues():void

            {       

                var propertyValues:Array = proxy.getProperties([

                    "state",

                    "defaultState",

                    "visibleKey",

                    "visibleStatus"                   

                ]);

               

                var propertyValuesLength:int = (propertyValues != null ? propertyValues.length : 0);

                for (var i:int=0; i < propertyValuesLength; i++)

                {

                    var propertyObject:Object = propertyValues[i];                   

                    var propertyName:String = propertyObject.name;                   

                    var propertyValue:* = propertyObject.value;

                    var bindingText:String = "";               

                    switch (propertyName)

                    {

                        case "visibleStatus":

                            bindingText = getPropertyBindDisplayName(propertyName);

                            if (bindingText != null){                                                           

                                statustext.text = bindingText;

                                visiblelabel.enabled=true;

                                keylabel.enabled=true;

                                keytext.enabled=true;

                                keybind.enabled=true;

                            }

                            else {

                                visiblelabel.enabled=false;

                                keylabel.enabled=false;

                                keytext.enabled=false;

                                keybind.enabled=false;

                            }   

                            break;

                        case "visibleKey":

                            bindingText = getPropertyBindDisplayName(propertyName);

                            if (bindingText != null){                                                           

                                keytext.text = bindingText;

                            }

                            else keytext.text = propertyValue;

                            break;

                        case "state":                           

                            bindingText = getPropertyBindDisplayName(propertyName);

                            if (bindingText != null){                                                           

                                stateInsertion.text = bindingText;

                            }                                               

                            break;

                        case "defaultState":                           

                            defaultstate.selectedIndex=_state.indexOf(propertyValue);

                            break;

                        default:

                            break;

                    }

                }               

            }

           

            /*

            -----------------------------------------------------------------------------

            Returns the bind display name or null if not bound

            -----------------------------------------------------------------------------

            */           

            protected function getPropertyBindDisplayName(propertyName:String):String

            {

                // Get the array of bindings for this property.

                var propertyBindings:Array = proxy.getBindings([propertyName]);

                if ((propertyBindings != null) && (propertyBindings.length > 0) && (propertyBindings[0].length > 0))

                {

                    // We have at least one binding for this property so pick the 1st one.

                    // Note: [0][0] is 1st property in the array, then 1st binding for that property.

                    var bindingID:String = propertyBindings[0][0];

                    return proxy.getBindingDisplayName(bindingID);

                }               

                return null;

            }

           

            /*

            -----------------------------------------------------------------------------

            Allows the user to select the Excel spreadsheet cell to bind to

            an Xcelsius custom component property

            -----------------------------------------------------------------------------

            */           

            protected function initiateBind(propertyName:String):void

            {

                // If there is an existing binding for this property show

                // that in the Excel binding selection window.

                // Store the currentBindingID (null if there is no current

                // binding), we need this when we "continueBinding".

                currentBindingID = null;

                var propertyBindings:Array = proxy.getBindings([propertyName]);

                if ((propertyBindings != null) && (propertyBindings.length > 0))

                {

                    // Use the 1st binding address for the property.

                    currentBindingID = propertyBindings[0];

                }

               

                // Store the name of the property that we are binding,

                // we need this when we "continueBinding".               

                propertyToBind = propertyName;

               

                // Let the user choose where to bind to in the Excel spreadsheet.

                proxy.requestUserSelection(currentBindingID);

            }

           

            /*

            -----------------------------------------------------------------------------

            Completes the binding when the user has finished selecting the

            cell to bind to or cleared the binding

            -----------------------------------------------------------------------------

            */

            protected function continueBind(bindingID:String):void

            {

                var propertyName:String = propertyToBind;

                var propertyValues:Array;

                var propertyObject:Object;

                var bindingAddresses:Array;

               

                // Clear any existing bindings - so we can re-bind.

                if (currentBindingID != null)

                {

                    proxy.unbind(currentBindingID);

                    currentBindingID = null;

                }

               

                // Process the property binding.

                switch (propertyName)

                {   

                    case "visibleStatus":

                        if ((bindingID == null) || (bindingID == "")){

                            statustext.text = null;                           

                            proxy.setProperty(propertyName, null);

                            proxy.setProperty("dynamicVisibility", false);

                           

                            visiblelabel.enabled=false;

                            keylabel.enabled=false;

                            keytext.enabled=false;

                            keybind.enabled=false;

                           

                            return;

                        }

                        statustext.text = proxy.getBindingDisplayName(bindingID);                       

                        if(statustext.text!=null){

                            visiblelabel.enabled=true;

                            keylabel.enabled=true;

                            keytext.enabled=true;

                            keybind.enabled=true;

                        }

                        else{

                            visiblelabel.enabled=false;

                            keylabel.enabled=false;

                            keytext.enabled=false;

                            keybind.enabled=false;

                        }                       

                        proxy.bind("visibleStatus", null, bindingID, BindingDirection.OUTPUT, "", OutputBindings.SINGLETON);

                        proxy.setProperty("dynamicVisibility", true);

                        break;

                    case "visibleKey":

                        if ((bindingID == null) || (bindingID == "")){

                            keytext.text = null;                           

                            proxy.setProperty(propertyName, null);

                            return;

                        }                       

                        keytext.text = proxy.getBindingDisplayName(bindingID);                       

                        proxy.bind("visibleKey", null, bindingID, BindingDirection.OUTPUT, "", OutputBindings.SINGLETON);

                        break;

                    case "state":

                        if ((bindingID == null) || (bindingID == "")){                                                   

                            stateInsertion.text =null;

                            return;

                        }                       

                        stateInsertion.text = proxy.getBindingDisplayName(bindingID);                       

                        proxy.bind("state", null, bindingID, BindingDirection.INPUT, InputBindings.SINGLETON, "");

                        break;

                    default:

                        break;                

                }               

            }

           

        ]]>

    </mx:Script>

   

   

   

   

    <mx:ViewStack id="viewstack1" creationPolicy="all" left="0" right="0" y="51" height="100%" minWidth="268" minHeight="350">

       

        <mx:Canvas id="general" label="General" minWidth="268"  minHeight="350" width="100%" height="100%">

           

            <!--

            ~~~~~~~~~~~~Data Insertion section~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

            -->           

            <mx:Label y="20" text="Data Insertion" width="145" left="23" fontWeight="bold"/>

            <mx:HRule y="29" right="56" left="110"/>

            <mx:Label text="Insert State (ON/OFF)" left="25"  y="51"/>

            <mx:TextInput id="stateInsertion" y="50" right="83" left="150" enabled="false" />

            <mx:Button y="51" right="56"  width="24"

                       click="initiateBind('state');"

                       icon="@Embed('resources/bind to cell.png')"/>

        </mx:Canvas>

       

        <mx:Canvas id="behavior" label="Behavior" minWidth="268"  minHeight="350" width="100%" height="100%">

           

            <!--

            ~~~~~~~~~~~~Default State section~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

            -->   

            <mx:Label y="9" text="State" width="145" left="23" fontWeight="bold"/>

            <mx:HRule y="18" right="56" left="60"/>

            <mx:Label x="50" y="38" text="Default State"/>

            <mx:ComboBox dataProvider="{_state}" x="126" y="39" editable="false" width="73" id="defaultstate" change="{

                         proxy.setProperty('defaultState',defaultstate.selectedLabel);

                         }">

            </mx:ComboBox>           

           

            <!--

            ~~~~~~~~~~~~Dynamic Visibility section~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

            -->           

            <mx:Label y="88" text="Dynamic Visibility" width="145" left="23" fontWeight="bold"/>

            <mx:HRule y="97" right="56" left="128"/>

            <mx:Label y="115" text="Show component only if status matches key:"

                      width="290" id="visiblelabel" enabled="false" left="50"/>

            <mx:Label x="69" y="144" text="Status:"/>

            <mx:TextInput id="statustext" x="126" y="144" width="77" enabled="false" change="{

                          proxy.setProperty('visibleStatus',statustext.text);

                          if(statustext.text!=null){

                          visiblelabel.enabled=true;

                          keylabel.enabled=true;

                          keytext.enabled=true;

                          keybind.enabled=true;

                          }

                          else{

                          visiblelabel.enabled=false;

                          keylabel.enabled=false;

                          keytext.enabled=false;

                          keybind.enabled=false;

                          }

                          }"/>

            <mx:Button id="statusbind" y="144" width="24"

                       click="initiateBind('visibleStatus');"

                       icon="@Embed('resources/bind to cell.png')" x="206"/>

            <mx:Label id="keylabel" enabled="false" x="69" y="174" text="Key:"/>

            <mx:TextInput id="keytext" x="126" y="174" width="77" enabled="false" change="{

                          proxy.setProperty('visibleKey',keytext.text);

                          var propertyBindings:Array = proxy.getBindings(['visibleKey']);

                          var bId:String=propertyBindings[0];

                          proxy.unbind(bId);

                          }"/>

            <mx:Button id="keybind" enabled="false" y="174" width="24"

                       click="initiateBind('visibleKey');"

                       icon="@Embed('resources/bind to cell.png')" x="206" height="23"/>           

        </mx:Canvas>

       

    </mx:ViewStack>

    <mx:TabBar x="0" y="0" dataProvider="{viewstack1}" height="51" width="209" fontWeight="bold" chromeColor="#1977CE" color="#FFFFFF">

    </mx:TabBar>

</mx:Application>

5. Under src folder, create a new folder and name it resources. Locate the image file bind to cell.png in the location <Dashboards_Installation_Dir>\Xcelsius  4.0\SDK\samples\CustomPropSheetHorizontalSlider\CustomPropSheetHorizontalSliderPropertySheet\src\resources and copy it inside newly created resources folder.

6. Save all files and click File > Export Release Build...

7. We have successfully created the component and its property sheet.

Package and Test

1. Launch Dashboards Add-On Packager.

2. Give the name as OnOffButton.

3. Under Visual Components, Click Add Component.

4. Under Class Name, provide com.yahoo.ari007_cse.OnOffButton. Give the Display Name as OnOffButton. Browse Component SWF from <OnOffButton_Project_Location>\bin-release\OnOffButton.swf. Browse Property Sheet SWF from <OnOffButtonPropertySheet_Project_Location>\bin-release\OnOffButton.swf. Select a Category. The add-on will be available under this specified category in Dashboards Designer. Optionally, you can specify icons and version of the add-on.

5. Go to Build tab and Click Build Package button. Save the add-on as OnOffButton.xlx.

6. Install the add-on in Dashboards designer and test.

Happy dashboarding ... :smile:

2 Comments
Labels in this area