Menu

Adding a custom tab to a class's details view with custom UI through an extension

2024-03-07
2024-03-12
  • Jacques Dancause

    Good day,

    Our team uses iTop, and we have a need to create a new tab for a certain class (it is called RefreshAndReplacement).

    We have been looking at Molkobain's datacenter view extension as an example. On racks and enclosures, this extension provides a new tab that allows you to see a visual representation of servers in racks and such. We figured this would be a good way to learn how to do this.

    We start by creating a class that implements iApplicationUIExtension, and OnDisplayRelations we add an AjaxTab. This ajaxtab points to a ajax.render.php which handles setting the page output.
    We are not sure about this pattern, might need to change it, but we are just trying to copy what Molkobain has in order to make a minimum viable product before refactoring.

    This new tab shows up successfully, but upon clicking on it, we get a weird response from the server. It is not an error message - just a regular message in the view. It says :

    "Unknown attribute nb_u from class RefreshAndReplacement"

    The thing is, nowhere in our code do we reference nb_u. I know it has something to do with Molkobain's code, but I'm not too sure where to find where this message is coming from.

    Here's some of the code:

    in applicationuiextension.class.php, OnDisplayRelations:

        public function OnDisplayRelations($oObject, WebPage $oPage, $bEditMode = false)
        {
            if (!($oPage instanceof iTopWebPage))
            {return; }
    
            // Don't display graphical view is element still being created
            if ($oObject->IsNew()) {
                return;
            }
    
            $oDeploymentCreatorView = DeploymentCreatorFactory::BuildFromObject($oObject);
            $oDeploymentCreatorView->SetObjectInEditMode($bEditMode);
    
            // Add content in an async tab
            $sPreviousTab = $oPage->GetCurrentTab();
            $oPage->AddAjaxTab(
                "Hello world", //Dict::S('Molkobain:DatacenterView:Tabs:View:Title'),
                $oDeploymentCreatorView->GetEndpoint(array(
                    'operation' => $oDeploymentCreatorView::ENUM_ENDPOINT_OPERATION_RENDERTAB,
                    'edit_mode' => $bEditMode,
                ))
            );
            // Put tab cursor back to previous to make sure nothing breaks our tab (other extension for example)
            $oPage->SetCurrentTab($sPreviousTab);
    
            return;
        }
    

    ajax.render.php:

    <?php
    use ContainerCrashers\iTop\Extension\DeploymentCreator\DeploymentCreatorFactory;
    
    /** @noinspection UsingInclusionOnceReturnValueInspection */
    @include_once '../approot.inc.php';
    @include_once '../../approot.inc.php';
    @include_once '../../../approot.inc.php';
    
    
    try
    {
        require_once APPROOT.'application/application.inc.php';
        require_once APPROOT.'/application/startup.inc.php';
    
        $oPage = new AjaxPage('');
    
        require_once APPROOT.'/application/user.preferences.class.inc.php';
    
    
        LoginWebPage::DoLoginEx('backoffice', false);
    
        $oPage->no_cache();
    
        $sOperation = utils::ReadParam('operation', '');
        $sClass = utils::ReadParam('class', '', false, 'class');
        $iId = (int) utils::ReadParam('id', 0);
    
        $oObject = MetaModel::GetObject($sClass, $iId);
        $oDeploymentCreatorView = DeploymentCreatorFactory::BuildFromObject($oObject);
    
    
        // Render tab
                $oPage->SetContentType('text/html');
                $oOutput = $oDeploymentCreatorView->Render();
    
                // HTML
                $oPage->add("We're inside the try.");
                $oPage->add($oOutput->GetHtml());
    
    
                // JS inline
                if(!empty($oOutput->GetJs()))
                {
                    $oPage->add_ready_script($oOutput->GetJs());
                }
                // CSS inline
                if(!empty($oOutput->GetCss()))
                {
                    $oPage->add_style($oOutput->GetCss());
                }
    
                $oPage->output();
    } catch (Exception $e)
    {
        // Note: Transform to cope with XSS attacks
        echo htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8');
        IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString());
    }
    

    Deploymentcreatorfactory.class.php:

    <?php
    
    namespace ContainerCrashers\iTop\Extension\DeploymentCreator;
    // ContainerCrashers\iTop\Extension\DeploymentCreator\DeploymentCreatorFactory'
    
    use Exception;
    use DBObject;
    
    /**
     * Class DeploymentCreatorFactory
     *
     * @package ContainerCrashers\iTop\Extension\DeploymentCreator
     * @since 1.1.0
     */
    class DeploymentCreatorFactory
    {
        const DEFAULT_DEPLOYMENT_CREATOR_VIEW_CLASS = 'ContainerCrashers\\iTop\\Extension\\DeploymentCreator\\DeploymentCreatorView';
    
        /** @var string $sDeploymentCreatorClass */
        protected static $sDeploymentCreatorClass;
        /** @var array $aCachedDeploymentCreatorViews */
        protected static $aCachedDeploymentCreatorViews = array();
    
        /**
         * Returns a DeploymentCreatorView of $oObject based on the current registered class (static::$sDeploymentCreatorView)
         *
         * Note: DeploymentCreatorView objects are cached in static::$aCachedDeploymentCreatorViews
         *
         * @param \DBObject $oObject
         *
         * @return \ContainerCrashers\iTop\Extension\DeploymentCreator\DeploymentCreatorView
         *
         * @throws \Exception
         */
        public static function BuildFromObject(DBObject $oObject)
        {
            // Set default class if none
            if(empty(static::$sDeploymentCreatorClass))
            {
                static::$sDeploymentCreatorClass = static::DEFAULT_DEPLOYMENT_CREATOR_VIEW_CLASS;
            }
    
            // Check if class exists
            if(!class_exists(static::$sDeploymentCreatorClass))
            {
                throw new Exception('Could not make DeploymentCreatorView as "'.static::$sDeploymentCreatorClass.'" class does not exists.');
            }
    
            // Cache view
            $sCacheKey = get_class($oObject) . '::' . $oObject->GetKey();
            if(!array_key_exists($sCacheKey, static::$aCachedDeploymentCreatorViews))
            {
                static::$aCachedDeploymentCreatorViews[$sCacheKey] = new static::$sDeploymentCreatorClass($oObject);
            }
    
            return static::$aCachedDeploymentCreatorViews[$sCacheKey];
        }
    
        /**
         * Registers the PHP class to build. Must be an instance of static::DEFAULT_DEPLOYMENT_CREATOR_VIEW_CLASS.
         *
         * @param string $sClass The FQCN of the class
         *
         * @throws \Exception
         */
        public static function RegisterClass($sClass)
        {
            // Check if class instance of DeploymentCreatorView
            if(false === is_a($sClass, static::DEFAULT_DEPLOYMENT_CREATOR_VIEW_CLASS, true)) {
                throw new Exception('Could not register "'.$sClass.'" as DeploymentCreatorView class as it does not extends it.');
            }
    
            static::$sDeploymentCreatorClass = $sClass;
        }
    }
    

    deploymentview.class.php

    <?php
     namespace ContainerCrashers\iTop\Extension\DeploymentCreator;
    
    use DBObject;
    use utils;
    use Combodo\iTop\Renderer\RenderingOutput;
    use Molkobain\iTop\Extension\DatacenterView\Common\Helper\ConfigHelper;
    
    /**
     * Class DeploymentCreatorView
     *
     * @package ContainerCrashers\iTop\Extension\DeploymentCreator
     */
    class DeploymentCreatorView
    {
        const ENUM_ENDPOINT_OPERATION_RENDERTAB = 'render_tab';
        const DEFAULT_OBJECT_IN_EDIT_MODE = false;
    
        /** @var string $sStaticConfigHelperClass */
        protected $sStaticConfigHelperClass;
        /** @var \DBObject $oObject */
        protected $oObject;
        /** @var string $sType */
        protected $sType;
        /** @var bool $bObjectInEditMode Is object in edition mode */
        protected $bObjectInEditMode;
        /** @var array $aOptions Current value of the options */
        protected $aOptions;
    
        public function __construct(DBObject $oObject)
        {
            $this->sStaticConfigHelperClass = static::GetStaticConfigHelperClass();
            $this->oObject = $oObject;
            $this->sType = 'refreshandreplacement';
            $this->bObjectInEditMode = static::DEFAULT_OBJECT_IN_EDIT_MODE;
            // Note: There is note static default value as array is not allowed before PHP 5.6
            $this->aOptions = array();
        }
    
    
        //--------------------
        // Getters / Setters
        //--------------------
    
        /**
         * @return \DBObject
         */
        public function GetObject()
        {
            return $this->oObject;
        }
    
        /**
         * Returns the object's type (see ENUM_ELEMENT_TYPE_XXX constants)
         *
         * @return string
         */
        public function GetType()
        {
            return $this->sType;
        }
    
        /**
         * Returns true if the object is in edition mode
         *
         * @return bool
         */
        public function IsObjectInEditMode()
        {
            return (bool) $this->bObjectInEditMode;
        }
    
        /**
         * Sets if the object is in edition mode
         *
         * @param bool $bObjectInEditMode If not passed, set to true automatically
         *
         * @return $this
         */
        public function SetObjectInEditMode($bObjectInEditMode = true)
        {
            $this->bObjectInEditMode = (bool) $bObjectInEditMode;
            return $this;
        }
    
    
        //----------
        // Helpers
        //----------
    
        /**
         * Returns the endpoint url with optional $aParams.
         * Note: that object's class & id are always passed as parameters.
         *
         * @param array $aParams Array of key => value to pass in the endpoint as parameters
         *
         * @return string
         */
        public function GetEndpoint($aParams = array())
        {
            $aQueryStringParams = array(
                'class=' . $this->GetObjectClass(),
                'id=' . $this->GetObjectId(),
            );
    
            foreach($aParams as $sKey => $sValue)
            {
                $aQueryStringParams[] = $sKey . '=' . $sValue;
            }
    
            return utils::GetAbsoluteUrlModulesRoot() . ConfigHelper::GetModuleCode() . '/console/ajax.render.php?' . implode('&', $aQueryStringParams);
        }
    
        /**
         * @return string
         */
        public function GetObjectClass()
        {
            return get_class($this->oObject);
        }
    
        /**
         * @return int
         */
        public function GetObjectId()
        {
            return $this->oObject->GetKey();
        }
    
        /**
         * Returns the whole view (legend, elements, ...) as fragments (HTML, JS files, JS inline, CSS files, CSS inline)
         *
         * @return \Combodo\iTop\Renderer\RenderingOutput
         * @throws \DictExceptionMissingString
         */
        public function Render()
        {
            $oOutput = new RenderingOutput();
            $oOutput->AddHtml(<<<HTML
            <div>HEllo world!</div>
            HTML);
            return $oOutput;
        }
    
        /**
         * Returns the FQCN of the ConfigHelper used by the current (static) class
         *
         * @return string
         */
        public static function GetStaticConfigHelperClass()
        {
            return '\\Molkobain\\iTop\\Extension\\DatacenterView\\Common\\Helper\\ConfigHelper';
        }
    }
    

    If someone more experienced than me has any idea as to why this is happening, I would be very grateful if you could point me in the right direction :)

     
  • Molkobain

    Molkobain - 2024-03-08

    Hello Jacques,

    It depends on what you want to display in that tab. If it's something relatively simple, you can do something far more simple that this. The example you took had a lot of code and factorization, but it might be "too much" for displaying simple things.

    Let us know what you want to display and we'll help you go through it.

    Edit: I pasted you below an example of the minimum you need to make a ajax tab. Note that if you want to make a non-ajax tab, it's even simpler. But you should consider the computing time of the tab to decide if it should be ajax or not (meaning, does it have a lot to display?)

    Guillaume

     

    Last edit: Molkobain 2024-03-08
    • Jacques Dancause

      Hey Guillaume,

      First of all, thanks so much for the quick reply. Your example provided makes a lot of sense and we are going to test it today.

      Secondly, I had not considered the performance improvements of using a different type of tab. We are just trying to display a form on this page, so I am curious about what other types of tab we could use?


      Edit: So after some trial and error today we managed to create a regular tab with the following code in our applicationuiextension:

          $oPage->AddToTab(OBJECT_PROPERTIES_TAB, "Hello World", "<p>Bonjour et Bienvenue!</p>);
      

      This worked well. I do have one more question - but not sure if I should create a new thread for it or not. It has to do with rendering a field from the class FieldUIBlockFactory. If I create a new field with this class, it returns a UIBlock. But, AddToTab wants us to pass in an HTML string.

      I was wondering, do we have to somehow convert the UIBlock to HTML, or do we have to use a different method to pass in a UIBlock instead of HTML?

      Thanks a lot,
      Jacques

       

      Last edit: Jacques Dancause 2024-03-12
  • Molkobain

    Molkobain - 2024-03-08

    Here is a simplification

    in applicationuiextension.class.php, OnDisplayRelations:

        public function OnDisplayRelations($oObject, WebPage $oPage, $bEditMode = false)
        {
            if (!($oPage instanceof iTopWebPage))
            {return; }
    
            // Remove this if you want the tab to display in the object creation form
            if ($oObject->IsNew()) {
                return;
            }
    
            // Add content in an async tab
            $sPreviousTab = $oPage->GetCurrentTab();
            $oPage->AddAjaxTab(
                "Hello world", //Dict::S('Molkobain:DatacenterView:Tabs:View:Title'),
                utils::GetAbsoluteModulePath(<YOUR_MODULE_CODE_HERE>)."ajax.render.php?class=" . get_class($oObject) . "&id="&$oObject->GetKey()";
            );
            // Put tab cursor back to previous to make sure nothing breaks our tab (other extension for example)
            $oPage->SetCurrentTab($sPreviousTab);
    
            return;
        }
    

    <your_module_code_here>/ajax.render.php</your_module_code_here>

    <?php
    /** @noinspection UsingInclusionOnceReturnValueInspection */
    @include_once '../approot.inc.php';
    @include_once '../../approot.inc.php';
    @include_once '../../../approot.inc.php';
    
    
    try
    {
        require_once APPROOT.'application/application.inc.php';
        require_once APPROOT.'/application/startup.inc.php';
    
        $oPage = new AjaxPage('');
    
        LoginWebPage::DoLoginEx('backoffice', false);
    
        $oPage->no_cache();
    
        // Operation is not passed yet in the URL, use it if you need it
        $sOperation = utils::ReadParam('operation', '');
        $sClass = utils::ReadParam('class', '', false, 'class');
        $iId = (int) utils::ReadParam('id', 0);
    
        $oObject = MetaModel::GetObject($sClass, $iId);
    
        // Do your business logic there
        // And build the HTML to return
    
                // Render tab
                $oPage->SetContentType('text/html');
    
                // HTML
                $oPage->add("ADD HTML HERE");
                $oPage->add(<<<HTML
    YOU CAN ALSO ADD HTML
    LIKE THAT ON SEVRAL LINES
     HTML
                 );
    
                // JS inline
                if(!empty($oOutput->GetJs()))
                {
                    $oPage->add_ready_script("alert('that is a ready script');");
                }
                // CSS inline
                if(!empty($oOutput->GetCss()))
                {
                    $oPage->add_style(".some_css_rule {}");
                }
    
                $oPage->output();
    } catch (Exception $e)
    {
        // Note: Transform to cope with XSS attacks
        echo htmlentities($e->GetMessage(), ENT_QUOTES, 'utf-8');
        IssueLog::Error($e->getMessage()."\nDebug trace:\n".$e->getTraceAsString());
    }
    
     
    👍
    1
    ❤️
    1

Log in to post a comment.

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.