Skip to content
Snippets Groups Projects
Select Git revision
  • 6b795c4a93bf2ed5b11dd269f1683cebb3b8c885
  • master default protected
  • Legacy_Php7
3 results

project_gui.class.php

Blame
  • project_gui.class.php 78.52 KiB
    <?php
    
    require_once 'project.class.php';
    require_once 'htmlguielements.class.php';
    
    
    /* ######################################################################
    
      IML DEPLOYMENT
    
      class project for all actions for single project
      Rendering of web ui
    
      ---------------------------------------------------------------------
      2013-11-08  Axel <axel.hahn@iml.unibe.ch>
      (...)
      2024-08-26  Axel   php8 only; added variable types; short array syntax
      ###################################################################### */
    
    /**
     * class for single project
     */
    // class project {
    class projectgui extends project
    {
    
        // ----------------------------------------------------------------------
        // private functions
        // ----------------------------------------------------------------------
    
        /**
         * Return html code for a div with background color based on a checksum of the given text
         * 
         * @param null|string $sText      text that is used for checksum; if false ist returns a gray
         * @param string $sContent   optional: text to show
         * @return string The HTML code
         */
        private function _getChecksumDiv(null|string $sText = '', string $sContent = '', string $sBarHeight = '3px'): string
        {
            if ($sText) {
    
                // color ranges in decimal values for RGB from ... to
                $iFgStart = 60;
                $iFgEnd = 160;
                $iBgStart = 200;
                $iBgEnd = 250;
    
                $iFgStart = 60;
                $iFgEnd = 160;
                $iBgStart = 190;
                $iBgEnd = 250;
    
                // deivider: 3 digits of md5 will be extracted
                $iFgDivider = 16 * 16 * 16 / ($iFgEnd - $iFgStart);
                $iBgDivider = 16 * 16 * 16 / ($iBgEnd - $iBgStart);
    
                $sHash = md5($sText);
                $sColor = ''
                    . 'color: rgba('
                    . ($iFgStart + round(hexdec(substr($sHash, 0, 3)) / $iFgDivider)) . ','
                    . ($iFgStart + round(hexdec(substr($sHash, 3, 3)) / $iFgDivider)) . ','
                    . ($iFgStart + round(hexdec(substr($sHash, 6, 3)) / $iFgDivider)) . ','
                    . '1'
                    . ');'
                    . 'background: rgba('
                    . ($iBgStart + round(hexdec(substr($sHash, 0, 3)) / $iBgDivider)) . ','
                    . ($iBgStart + round(hexdec(substr($sHash, 3, 3)) / $iBgDivider)) . ','
                    . ($iBgStart + round(hexdec(substr($sHash, 6, 3)) / $iBgDivider)) . ','
                    . '1'
                    . ');'
                ;
            } else {
                $sColor = "color: #888; background: #ccc;";
            }
            return '<div style="' . $sColor . ' border-top: ' . $sBarHeight . ' solid;">' . ($sContent ?: ' ') . '</div>';
        }
    
    
        /**
         * Get html code for the colored bar on top of each phase detail items.
         * It returns false of revision number was not found in the given phase + place
         * 
         * @param string $sPhase  phase of a project
         * @param string $sPlace  place in the given phase
         * @return bool|string The HTML code
         */
        private function _renderBar(string $sPhase, string $sPlace, string $sBarHeight = '3px'): bool|string
        {
            $aDataPhase = $this->getPhaseInfos($sPhase);
            $aData = $aDataPhase[$sPlace];
            if (!array_key_exists("revision", $aData)) {
                return false;
            }
            return $this->_getChecksumDiv($aData["revision"], '', $sBarHeight);
        }
    
        /**
         * Render deploy infos: show hosts and its installed revisions
         * @param array $aData  deployment metadata 
         * @return string
         */
        private function _renderHostsData(array $aData): string
        {
            $sReturn = '';
            if (isset($aData['_hosts'])) {
    
                // $sReturn.= print_r($aData['_hosts'], 1);
                $sReturn .= '<div class="hosts">'
                    . '<br><strong>' . t('hosts') . ':</strong><br>'
                ;
                foreach ($aData['_hosts'] as $sHostname => $aHostinfos) {
                    $oUpdateDate = date("U", strtotime($aHostinfos['time']));
                    $iAgeUpdate = round((date("U") - $oUpdateDate) / 60);
                    $sAge = $iAgeUpdate < 60 * 60 * 13 ? "$iAgeUpdate min" : "??";
    
                    $sReturn .= '<div class="host">'
                        . $this->_getChecksumDiv(
                            $aHostinfos['_data']['revision'],
                            $this->_oHtml->getIcon('host') . '<br>' . $sHostname
                        )
                        . "($sAge)"
                        . '</div>'
                    ;
                }
                $sReturn .= '</div><div style="clear: both;"></div>';
            }
            return $sReturn;
        }
    
        /**
         * Get html code for list of hosts in a phase
         * 
         * @param string $sPhase  phase of a project
         * @return string
         */
        private function _renderHosts(string $sPhase): string
        {
            $aDataPhase = $this->getPhaseInfos($sPhase);
            if (is_array($aDataPhase) && array_key_exists('deployed', $aDataPhase)) {
                return $this->_renderHostsData($aDataPhase['deployed']);
            }
            return '';
        }
    
        /**
         * Get html code for list of files in a phase
         * 
         * @param string $sPhase  phase of a project
         * @return string
         */
        private function _renderFiles(string $sPhase): string
        {
            $sReturn = '';
            $aFiles = $this->getBuildfilesByPlace($sPhase, 'ready2install');
            if (!$aFiles || !$aFiles['filecount']) {
                return '';
            }
            $sReturn .= '<strong>' . t("filelist") . '</strong> (' . $aFiles['filecount'] . '):<br>';
            foreach ($aFiles['files'] as $sFilename => $aData) {
                $sReturn .= '<div class="file file-' . $aData['type'] . ' fileext-' . $aData['extension'] . '" title="' . $sFilename . ' (' . $aData['type'] . ')">'
                    . $aData['icon'] . $sFilename
                    // . ' ('.$aData['type'].')'
                    . '</div>'
                ;
            }
            $sReturn .= '(' . $aFiles['totalsize-hr'] . ')';
            return $sReturn;
        }
    
        // ----------------------------------------------------------------------
        // RENDERING
        // ----------------------------------------------------------------------
    
    
        /**
         * Get html for a row with td for all places of a phase
         * 
         * @param string $sPhase   phase
         * @param bool   $bActions draw action links (deploy, accept) on/ off
         * @param bool   $bLong    use long variant to display infos? 
         * @return string
         */
        public function renderAllPhaseDetails(string $sPhase, bool $bActions = true, bool $bLong = true): string
        {
            if (!$this->isActivePhase($sPhase)) {
                return '
                            <td class="td-phase-' . $sPhase . ' td-phase-inactive ' . $this->_aConfig["id"] . '" colspan="' . count($this->_aPlaces) . '">
                                <div class="versioninfo center inactive">' . $this->_oHtml->getIcon('sign-info') . t('inactive') . '</div>
                            </td>';
            }
            $sRow2 = false;
    
            $aRows = [];
            $sLastPlace = '';
    
            foreach (array_keys($this->_aPlaces) as $sPlace) {
                $aRows[$sPlace] = $this->renderPhaseDetail($sPhase, $sPlace, $bActions, $bLong);
    
                // generate ">>" sign for lastly generated td
                if (
                    $sLastPlace && array_key_exists("version", $this->_aData["phases"][$sPhase][$sLastPlace])
                    && array_key_exists("version", $this->_aData["phases"][$sPhase][$sPlace])
                    && $this->_aData["phases"][$sPhase][$sLastPlace]["version"] == $this->_aData["phases"][$sPhase][$sPlace]["version"]
                    && !$bLong
                ) {
                    $aRows[$sLastPlace] = $this->_renderBar($sPhase, $sPlace) . "&raquo;";
                }
                $sLastPlace = $sPlace;
            }
    
            foreach (array_keys($this->_aPlaces) as $sPlace) {
                $sRow2 .= '<td class=" td-phase-' . $sPhase . ' td-place-' . $sPlace . ' td' . $this->_aConfig["id"] . '">' . $aRows[$sPlace] . '</td>';
            }
            return $sRow2;
        }
    
        /**
         * Get html code for current project errors by rendering a box per error in $this->_errors
         * @return string
         */
        public function renderErrorBoxes(): string
        {
            $sReturn = '';
            if (count($this->_errors)) {
                foreach ($this->_errors as $sError) {
                    $sReturn .= $this->_oHtml->getBox("error", $sError);
                }
            }
            return $sReturn;
        }
    
    
        /**
         * fix output of commit message as html
         * This is a compatibility function for older builds
         * 
         * @param  null|string  $sMessage  git commit message
         * @return string
         */
        public function transformCommitMessage(null|string $sMessage): string
        {
            if (strstr($sMessage, '<br>Date:')) {
                $_aReplace = [
                    '<br>Author:' => "\nAuthor:",
                    '<br>Date:' => "\nDate:",
                    '<br><br>' => "\n\n",
                ];
                $sMessage = str_replace(array_keys($_aReplace), array_values($_aReplace), $sMessage) . " *";
            }
            return htmlentities($sMessage);
        }
    
        /**
         * Get html code for info link that shows popup with metadata on mouseover
         * 
         * @param array $aInfos   metainfos of the package (from json file)
         *                   one of ok=1|error=message - status key
         *                   date      - timestamp of build
         *                   revision  - revision
         *                   branch    - branch
         *                   message   - commit message
         * @param array $aOptions options
         *                   title - tile in popover; default: empty
         *                   label - link label; default: empty (results in Infos|ERROR)
         *                   more  - additional infos in popover; default: empty
         *                   hpos  - horizontal position; one of left|right; default: right
         * @return string
         */
        public function renderInfoLink(array $aInfos, array $aOptions = []): string
        {
            $sReturn = '';
            $bIsError = false;
            $this->_oHtml = new htmlguielements();
    
            $sInfos = '';
            $sTitle = '';
            if (array_key_exists("title", $aOptions) && $aOptions["title"]) {
                $sTitle .= $aOptions["title"];
            }
            if (array_key_exists("ok", $aInfos)) {
                $sLinktitle = t('infos');
                if (array_key_exists("message", $aInfos)) {
                    $sInfos .= $this->_getChecksumDiv(
                        $aInfos["revision"],
                        $this->_oHtml->getIconByType('calendar') . t('build-from') . ' ' . date("d.m.Y H:i:s", strtotime($aInfos["date"])) . '<br>'
                        . $this->_oHtml->getIconByType('branch') . t('branch') . ': ' . $aInfos["branch"] . '<br>'
                        . $this->_oHtml->getIconByType('revision') . t('revision') . ': ' . $this->_renderRevision($aInfos["revision"]) . '<br>'
                        . $this->_oHtml->getIconByType('comment') . t('commitmessage') . ': '
                    )
                        . '<pre>' . $this->transformCommitMessage($aInfos["message"]) . '</pre>';
                    if (array_key_exists("more", $aOptions)) {
                        $sInfos .= $aOptions["more"];
                    }
                }
            } else {
                $bIsError = true;
                if (!$sTitle) {
                    $sTitle .= ' ' . t('error');
                }
                $sLinktitle = t('error');
                $sInfos = $this->_oHtml->getBox('error', '') . '<p>' . $aInfos["error"] . '</p>';
            }
            $sInfos .= $this->_renderHostsData($aInfos);
    
            if (array_key_exists("label", $aOptions) && $aOptions["label"]) {
                $sLinktitle .= $aOptions["label"];
            }
    
            // render html
            $sId = 'info' . md5($sInfos);
            $sReturn = '<a href="#" class="btn ' . ($bIsError ? 'btn-danger' : 'btn-default') . '" title="" 
                    onclick="showIdAsModalMessage(\'' . $sId . '\', \'' . $sTitle . '\'); return false;"
                    >'
                . $this->_oHtml->getIcon($bIsError ? 'sign-error' : 'sign-info') // ... '<i class="fa fa-info"></i> '
                . $sLinktitle
                . '</a><div id="' . $sId . '" style="display: none;" '
            ;
            if (array_key_exists("hpos", $aOptions)) {
                $sReturn .= ' class="' . $aOptions["hpos"] . '"';
            }
            $sReturn .= '>';
    
            if ($sTitle) {
                // $sReturn.='<span class="title">' . $sTitle . '</span><br><br>';
            }
    
            $sReturn .= $sInfos . '</div>';
    
            if ($bIsError) {
                // $sReturn = '<div class="error">' . $sReturn . '</div>';
            }
    
            return $sReturn;
        }
    
        /**
         * Get html for a colored link to any project action
         * 
         * @param string $sFunction name of the action; one of accept|build|cleanup|deploy|new|overview|phase|rollback|setup
         * @param string $sPhase    current phase where to place the link
         * @return string
         */
        public function renderLink(string $sFunction, string $sPhase = '', string $sVersion = ''): string
        {
            $sFirst = $this->getNextPhase();
            $sNext = $this->getNextPhase($sPhase);
            $aLinkdata = [
                'default' => ['class' => ''],
                'accept' => [
                    'class' => $sNext,
                    'hint' => sprintf(t("accept-hint"), $sPhase, $sNext),
                    'label' => t('accept'),
                ],
                'build' => [
                    'class' => $sFirst,
                    'hint' => sprintf(t("build-hint"), $sFirst),
                    'label' => t('build'),
                    'role' => 'buildProject'
                ],
                'cleanup' => ['class' => ''],
                'deploy' => [
                    'class' => $sPhase,
                    'hint' => sprintf(t("deploy-hint"), $sPhase),
                    'label' => t('deploy'),
                ],
                'new' => [
                    'hint' => t("new-project-hint"),
                    'label' => t('new-project'),
                ],
                'overview' => [
                    'class' => '',
                    'hint' => t('menu-project-home') . ' [' . $this->getLabel() . ']',
                    'label' => $this->getLabel()
                ],
                'phase' => [
                    'icon' => $this->_oHtml->getIcon('phase'),
                    'class' => $sPhase,
                    'hint' => sprintf(t('phase-details-hint'), $sPhase),
                    'label' => sprintf(t('phase-details'), $sPhase),
                ],
                'rollback' => [
                    'class' => $sPhase,
                    'hint' => sprintf(t('rollback-hint'), $sPhase, $sVersion),
                    'label' => t('rollback')
                ],
                'setup' => [
                    'class' => 'btn',
                    'hint' => sprintf(t('setup-hint'), $sPhase, $sVersion),
                    'label' => t('setup'),
                ],
            ];
            /*
              if (!$this->oUser->hasRole("project-action-$sFunction")){
              // $sClass .= ' disabled';
              // return '<span title="no permission [project-action-'.$sFunction.']">[ ]</span>';
              }
             * 
             */
            // fuer wen ist der Link
            $sRole = '';
            $sOnMouseover = '';
            $sOnMouseout = '';
            switch ($sFunction) {
                case 'accept';
                    $sRole = 'developer';
                    if ($sNext == "live") {
                        $sRole = 'pl';
                    }
                    $sOnMouseover = '$(\'.td-phase-' . $sNext . '.td' . $this->_aConfig["id"] . '\').addClass(\'highlight\');';
                    $sOnMouseout = '$(\'.td-phase-' . $sNext . '.td' . $this->_aConfig["id"] . '\').removeClass(\'highlight\');';
                    break;
                case 'build';
                    $sRole = 'developer';
                    $sOnMouseover = '$(\'.td-phase-' . $sNext . '.td' . $this->_aConfig["id"] . '\').addClass(\'highlight\');';
                    $sOnMouseout = '$(\'.td-phase-' . $sNext . '.td' . $this->_aConfig["id"] . '\').removeClass(\'highlight\');';
                    break;
                case 'deploy';
                    $sRole = 'developer';
                    $sOnMouseover = '$(\'.td-phase-' . $sPhase . '.td-place-ready2install.td' . $this->_aConfig["id"] . '\').addClass(\'highlight\');'
                        . '$(\'.td-phase-' . $sPhase . '.td-place-deployed.td' . $this->_aConfig["id"] . '\').addClass(\'highlight\');'
                    ;
                    $sOnMouseout = '$(\'.td-phase-' . $sPhase . '.td-place-ready2install.td' . $this->_aConfig["id"] . '\').removeClass(\'highlight\');'
                        . '$(\'.td-phase-' . $sPhase . '.td-place-deployed.td' . $this->_aConfig["id"] . '\').removeClass(\'highlight\');'
                    ;
                    break;
            }
    
            // $sClass = $sPhase;
            $sIconClass = (array_key_exists($sFunction, $aLinkdata)) ? $aLinkdata[$sFunction]['icon'] : $aLinkdata['default']['icon'];
            $sHint = isset($aLinkdata[$sFunction]['hint']) ? $aLinkdata[$sFunction]['hint'] : "";
            $sLabel = isset($aLinkdata[$sFunction]['label']) ? $aLinkdata[$sFunction]['label'] : $sFunction;
            $sClass = isset($aLinkdata[$sFunction]['class']) ? $aLinkdata[$sFunction]['class'] : '';
            if ($sRole) {
                $sClass .= " role role" . $sRole;
            }
    
            $sLink = "/deployment/" . ($this->_aConfig["id"] ? $this->_aConfig["id"] : 'all/setup') . "/";
            if ($sFunction != "overview") {
                $sLink .= "$sFunction/";
            }
            if ($sPhase) {
                $sLink .= "$sPhase/";
            }
            if ($sVersion) {
                $sLink .= "$sVersion/";
            }
            if (!$this->oUser->hasPermission("project-action-$sFunction")) {
                // $sClass .= ' disabled';
                return '<span class="btn disabled btn-default" title="no permission [project-action-' . $sFunction . '] for user [' . $this->oUser->getUsername() . ']"><i class="' . $sIconClass . '"></i> ' . $sLabel . '</span>';
            }
    
            // $sClass='btn ' . (strstr('btn-', $sClass) ? '': 'btn-default ') .$sClass;
            return $this->_oHtml->getLinkButton([
                'href' => $sLink,
                'title' => $sHint,
                'class' => $sClass,
                'type' => $sFunction,
                'onmouseover' => $sOnMouseover,
                'onmouseout' => $sOnMouseout,
                'label' => $sLabel,
            ]);
            // return '<a href="' . $sLink . '" ' . $sOnMouseover . ' title="' . $sHint . '" class="btn  btn-default ' . $sClass . '"><i class="' . $sIconClass . '"></i> ' . $sLabel . '</a>';
        }
    
        /**
         * Get html code for the setup form for a new project
         * @return string
         */
        public function renderNewProject(): string
        {
            global $aParams;
            if (!$this->oUser->hasPermission("project-action-create")) {
                return $this->oUser->showDenied();
            }
    
            require_once("formgen.class.php");
            $i = 0;
            $sID = array_key_exists("id", $aParams) ? $aParams["id"] : "";
    
            $aForms = [
                'setup' => [
                    'meta' => [
                        'method' => 'POST',
                        'action' => '?',
                    ],
                    'validate' => [],
                    'form' => [
                        'input' . $i++ => [
                            'type' => 'hidden',
                            'name' => 'setupaction',
                            'value' => 'create',
                        ],
                        'input' . $i++ => [
                            'type' => 'text',
                            'name' => 'id',
                            'label' => t("class-project-info-setup-projectId"),
                            'value' => $sID,
                            'required' => 'required',
                            'validate' => 'isastring',
                            'size' => 100,
                            'pattern' => '[a-z0-9\-_]*',
                            'placeholder' => t("class-project-info-setup-projectId-placeholder"),
                        ],
                    ],
                ],
            ];
            $aForms["setup"]["form"]['input' . $i++] = [
                'type' => 'submit',
                'name' => 'btnsave',
                'label' => t("save"),
                'value' => $this->_oHtml->getIcon('sign-ok') . t("save"),
            ];
    
            $oForm = new formgen($aForms);
            return $oForm->renderHtml("setup");
        }
    
        /**
         * Get html for a place of a phase.
         * It returns false when 
         * - phase or place are empty 
         * - phase is not active
         * - place is not valid
         * 
         * @param string  $sPhase    phase
         * @param string  $sPlace    name of the place; one of onhold|ready2install|deployed
         * @param bool    $bActions  draw action links (deploy, accept) on/ off
         * @param bool    $bLong     use long variant to display infos? 
         * @return string|boolean
         */
        public function renderPhaseDetail(string $sPhase, string $sPlace, bool $bActions = true, bool $bLong = true): bool|string
        {
    
            if (!$sPhase) {
                return false;
            }
            if (!$sPlace) {
                return false;
            }
            if (!$this->isActivePhase($sPhase)) {
                return false;
            }
            if (!array_key_exists($sPlace, $this->_aPlaces)) {
                return false;
            }
    
            $sReturn = '';
            $aDataPhase = $this->getPhaseInfos($sPhase);
            $aData = $aDataPhase[$sPlace];
            // foreach($aDataPhase[$sPlace] as $aData) {
            if (array_key_exists("ok", $aData) && array_key_exists("version", $aData)) {
                // TODO: getChecksumDiv anhand der Repo-Versionsnummer - dann kann man beim build auch die Farbe mit dem Repo HEAD vergleichen
                // time
                $sDateFormat = "d.m.Y H:i";
                $oPkgDate = date("U", strtotime($aData["date"]));
                /*
                  $iAge=date("U")-$oPkgDate;
                  $sAgeClass="";
                  if ($iAge< 60*60*24*3){
                  $sAgeClass="last1d";
                  }
                  if ($iAge< 60*60){
                  $sAgeClass="last1h";
                  }
                 */
    
                if ($bLong) {
                    // long display of the revision
                    // $sJsonUrl = $this->_getInfofile($sPhase, $sPlace);
                    $sReturn .= $this->_getChecksumDiv(
                        $aData["revision"],
                        $this->_oHtml->getIconByType('calendar') . ' ' . date($sDateFormat, $oPkgDate) . '<br>'
                        . $this->_oHtml->getIconByType('branch') . t('branch') . ': ' . $aData["branch"] . '<br>'
                        . $this->_oHtml->getIconByType('revision') . t('revision') . ': ' . $this->_renderRevision($aData["revision"]) . '<br>'
                        . $this->_oHtml->getIconByType('comment') . t('commitmessage') . ':<br>'
                    )
                        . '<pre>' . $this->transformCommitMessage($aData["message"]) . '</pre>'
                    ;
                    if ($sPlace == "deployed" && array_key_exists("url", $this->_aPrjConfig["phases"][$sPhase])) {
                        $sUrl = $this->_aPrjConfig["phases"][$sPhase]["url"];
                        $sReturn .= $this->_oHtml->getIconByType('link-extern') . ' ' . t('url') . ': <a href="' . $sUrl . '">' . $sUrl . '</a><br>';
                    }
                } else {
                    $sReturn .= $this->_getChecksumDiv(
                        $aData["revision"],
                        $this->_oHtml->getIconByType('calendar') . ' ' . date($sDateFormat, $oPkgDate)
                    );
                    if ($sPlace == "deployed" && array_key_exists("url", $this->_aPrjConfig["phases"][$sPhase])) {
                        $sMore = $this->_oHtml->getIconByType('link-extern') . ' '
                            . t('url')
                            . ': <a href="' . $this->_aPrjConfig["phases"][$sPhase]["url"] . '">' . $this->_aPrjConfig["phases"][$sPhase]["url"] . '</a><br>';
                    }
    
                    $sReturn .= ' ' . $this->renderInfoLink(
                        $aData,
                        [
                            'title' => $this->getLabel() . " :: $sPhase :: $sPlace",
                            'more' => $sMore,
                        ]
                    );
                }
    
                switch ($sPlace) {
                    case "onhold":
                        if (array_key_exists("phases", $this->_aConfig) && array_key_exists($sPhase, $this->_aConfig["phases"])) {
                            // $sReturn .= print_r($this->_aConfig["phases"][$sPhase], true);
                            if (count($this->_getDeploytimes($sPhase))) {
                                $sReturn .= '<br>' . $this->_oHtml->getIcon('time') . t('deploytimes') . ':<br>'
                                    . implode("<br>", array_values($this->_getDeploytimes($sPhase)))
                                    . '<br>';
                            }
                            if ($bActions) {
                                $sReturn .= ' ' . $this->renderLink("deploy", $sPhase);
                            }
                        }
                        break;
    
                    case "ready2install":
                        /*
                        // IDEA: try to install the same phase again. Needs update of method project->deploy() 
                        if ($bActions) {
                            $sReturn .= ' ' . $this->renderLink("deploy", $sPhase);
                        }
                        */
                        break;
    
                    case "deployed":
                        if ($bActions && $this->canAcceptPhase($sPhase)) {
                            $sReturn .= ' ' . $this->renderLink("accept", $sPhase);
                        }
                        break;
                    default:
                        break;
                }
                // $this->_getChecksumDiv($aData["revision"])
            } else {
                if (array_key_exists("error", $aData)) {
                    $sReturn .= ''
                        . $this->renderInfoLink(['error' => $aData["error"], [] ])
                    ;
                } else if (array_key_exists("warning", $aData)) {
                    $sReturn .= '<div class="warning">' . $this->_oHtml->getIcon('sign-info') . t('warning') . ':<br>' . $aData["warning"] . '</div>';
                } else {
    
                    // OK = 1 ... for the queue we show no hint
                    return '';
                    /*
                    return $sPlace=='onhold' 
                        ? t('class-project-queue-empty')
                        : ''
                        ;
                    */
                }
            } // if
            return $sReturn;
        }
    
        /**
         * Get html for the project overview; it shows the defined phases for 
         * the project as a table
         * @return string
         */
        public function renderPhaseInfo(): string
        {
            $sRow1 = false;
            $sRow2 = false;
    
            $renderAdminLTE = new renderadminlte();
    
            $iWidth = min(12 / count($this->getActivePhases()), 4);
            foreach ($this->getActivePhases() as $sPhase) {
                $sRow1 .= $renderAdminLTE->addCol(
                    '<table class="nomargin"><tr><th class="' . $sPhase . ' tdphase">' . $sPhase . '</th></tr></table>'
                    ,
                    $iWidth
                );
    
                $sDetails = t('url') . ': <a href="' . $this->_aPrjConfig["phases"][$sPhase]["url"] . '">' . $this->_aPrjConfig["phases"][$sPhase]["url"] . '</a><br>'
                    . '<br>' . t('deploytimes') . ':<br>';
                if (count($this->_getDeploytimes($sPhase))) {
                    $sDetails .= implode("<br>", $this->_getDeploytimes($sPhase));
                } else {
                    $sDetails .= t('deploytimes-immediately');
                }
                $sDetails .= '<br>' . $this->renderLink("phase", $sPhase)
                    . $this->_renderHosts($sPhase)
                    . '<br>'
                    . $this->_renderFiles($sPhase)
                ;
    
                $sRow2 .= $renderAdminLTE->addCol(
                    $renderAdminLTE->getCard([
                        'class' => $sPhase,
                        'variant' => '',
                        'tb-remove' => 1,
                        'tb-collapse' => 1,
                        'title' => '',
                        'tools' => '',
                        'text' => $sDetails,
                        'footer' => '',
                    ]),
                    $iWidth
                );
            }
            return ''
                . $renderAdminLTE->addRow($sRow1)
                . $renderAdminLTE->addRow($sRow2)
            ;
    
        }
    
        /**
         * Get html for a row with td for all places (first row)
         * @param string $sPhase  phase (just needed for coloring)
         * @return string
         */
        public function renderPlacesAsTd(string $sPhase): string
        {
            $sRow1 = '';
            foreach (array_keys($this->_aPlaces) as $sPlace) {
                $sRow1 .= '<td class="' . $sPhase . ' ' . $this->_aConfig["id"] . ' tdphase">' . t($sPlace) . '</td>';
            }
            return $sRow1;
        }
    
        /**
         * Get html code for the setup form of an exsiting project
         * @return string
         */
        public function renderProjectSetup(): string
        {
            if (!$this->oUser->hasPermission("project-action-setup")) {
                return $this->oUser->showDenied();
            }
            $sMessages = '';
            require_once("formgen.class.php");
    
            $aSelectProjectGroup = [
                'type' => 'select',
                'name' => 'projectgroup',
                'label' => t("projectgroup"),
                'options' => [
                    OPTION_NONE => [
                        'label' => t('none'),
                    ],
                    '' => [
                        'label' => '- - - - - - - - - - - - - - - - - - - - ',
                    ],
                ],
            ];
            foreach ($this->_aConfig['projectgroups'] as $sGroupid => $sGroupLabel) {
                $bActive = $this->getProjectGroup() === $sGroupid;
                $aSelectProjectGroup['options'][$sGroupid] = [
                    'label' => $sGroupLabel,
                    'selected' => $bActive ? 'selected' : false,
                ];
            }
    
            $aSelectSlack = [
                'type' => 'hidden',
                'name' => 'messenger[slack]',
                'value' => false,
            ];
            if (
                isset($this->_aConfig['messenger']['slack']['presets'])
                && count($this->_aConfig['messenger']['slack']['presets'])
            ) {
                $aSelectSlack = [
                    'type' => 'select',
                    'name' => 'messenger[slack]',
                    'label' => t("messenger-slack"),
                    'options' => [
                        OPTION_NONE => [
                            'label' => t('none'),
                        ],
                        '' => [
                            'label' => '- - - - - - - - - - - - - - - - - - - - ',
                        ],
                    ],
                ];
                foreach ($this->_aConfig['messenger']['slack']['presets'] as $sSlackUrl => $aSlackCfg) {
                    $bActive = $this->_aPrjConfig['messenger']['slack'] === $sSlackUrl;
                    $aSelectSlack['options'][$sSlackUrl] = [
                        'label' => array_key_exists('label', $aSlackCfg) ? $aSlackCfg['label'] : $sSlackUrl,
                        'selected' => $bActive ? 'selected' : false,
                    ];
                }
    
            }
            // ---------- Build plugins
            /*
            
            $aPluginsBuild = [
                'select' => [
                    'type' => 'checkbox',
                    'name' => 'build[enabled_build_plugins]',
                    'label' => t("build-plugins"),
                    'options' => [],
                ],
                // 'project-config' => '',
            ];
            foreach (array_keys($this->getConfiguredPlugins('build')) as $sPluginName){
    
                $sPluginFile=$this->_getPluginFilename('build', $sPluginName);
                $TmpRolloutPlugin = false;
                $sMyClassname='build_'. $sPluginName;
                if(file_exists($sPluginFile)){
                try{
                    include_once $this->_getPluginFilename('build', $sPluginName);
                    $TmpRolloutPlugin = new $sMyClassname([]);
                    echo "FOUND $sMyClassname<br>";
                    $aPluginsBuild['select']['options'][$sPluginName]=[
                            'label' => $TmpRolloutPlugin->getName(),
                            'checked' => $bActive,
                            // 'onclick' => '$(\'.'.$sMyDivClass.'\').hide(); $(\'.' . $sMyDivClassActive . '\').show();',
                    ];
                    } catch (Exception $ex) {
    
                    }
                } else {
                    $aRollout['project-select']['options'][$sPluginName]=[
                            'label' => 'not found: <span class="error">' . $sMyClassname . '</span>',
                            'checked' => false,
                            'disabled' => "disabled",
                    ];
    
                    
                }
            }
            echo '<pre>'; print_r($aPluginsBuild); die(__METHOD__);
            */
    
            // ---------- /Build plugins
    
            // ---------- Rollout plugins
            $aRollout = [
                'project-select' => [
                    'type' => 'radio',
                    'name' => 'deploy[enabled_rollout_plugin]',
                    'label' => t("deploy-rollout-plugin"),
                ],
                'project-config' => '',
            ];
            foreach (array_keys($this->getConfiguredPlugins('rollout')) as $sPluginName) {
    
                $sPluginFile = $this->_getPluginFilename('rollout', $sPluginName);
                $TmpRolloutPlugin = false;
                $sMyClassname = 'rollout_' . $sPluginName;
                $sMyDivId = 'rollout-' . $sPluginName . '-config';
                $sMyDivClass = 'rolloutconfigdiv';
                $sMyDivClassActive = 'rolloutconfigdiv-' . $sPluginName;
                $bActive = $sPluginName === $this->oRolloutPlugin->getId();
    
                if (file_exists($sPluginFile)) {
                    try {
                        include_once $this->_getPluginFilename('rollout', $sPluginName);
                        $TmpRolloutPlugin = new $sMyClassname([
                            'lang' => $this->_aConfig['lang'],
                            'phase' => false,
                            'globalcfg' => $this->_aConfig['plugins']['rollout'][$sPluginName],
                            'projectcfg' => $this->_aPrjConfig,
                        ]);
                        $aRollout['project-select']['options'][$sPluginName] = [
                            'label' => $TmpRolloutPlugin->getName(),
                            'checked' => $bActive,
                            'onclick' => '$(\'.' . $sMyDivClass . '\').hide(); $(\'.' . $sMyDivClassActive . '\').show();',
                        ];
    
                        $aRollout['project-config'] .= ''
                            . '<div id="' . $sMyDivId . '" class="' . $sMyDivClass . ' ' . $sMyDivClassActive . '"'
                            . ($bActive ? '' : ' style="display: none;"')
                            . '>'
                            . $TmpRolloutPlugin->renderFormdata4Project()
                            . '</div>'
                        ;
    
                        // generate form firlds for each phase
                        foreach (array_keys($this->getPhases()) as $sMyPhase) {
                            $aRollout[$sMyPhase] .= ''
                                . '<div id="' . $sMyDivId . '-' . $sMyPhase . '" class="' . $sMyDivClass . ' ' . $sMyDivClassActive . '"'
                                . ($bActive ? '' : ' style="display: none;"')
                                . '>'
                                . $TmpRolloutPlugin->renderFormdata4Phase($sMyPhase)
                                . '</div>'
                            ;
                        }
                    } catch (Exception $ex) {
    
                    }
                } else {
                    $aRollout['project-select']['options'][$sPluginName] = [
                        'label' => 'not found: <span class="error">' . $sMyClassname . '</span>',
                        'checked' => false,
                        'disabled' => "disabled",
                    ];
    
    
                }
            }
            // ---------- /Rollout plugins
    
            $aForemanHostgroups = false;
            $iForemanHostgroupDefault = false;
            $sForemanHostgroupDefault = false;
            if (array_key_exists('foreman', $this->_aConfig)) {
                // echo '<pre>' . print_r($this->_aPrjConfig, 1) . '</pre>';
                $iForemanHostgroupDefault = (int) $this->_aPrjConfig['deploy']['foreman']['hostgroup'];
                require_once('foremanapi.class.php');
                $oForeman = new ForemanApi($this->_aConfig['foreman']);
                // $oForeman->setDebug(1);
                // $oForeman->selfcheck(); die(__FUNCTION__);
    
                $aForemanHostgroups = $oForeman->read([
                    'request' => [
                        ['hostgroups'],
                        // ['operatingsystems',4],
                    ],
                    'response' => [
                        'id',
                        'title'
                    ],
                ]);
                $aSelectForemanGroups = [
                    'type' => 'select',
                    'name' => 'deploy[foreman][hostgroup]',
                    'label' => $this->_oHtml->getIcon('foreman') . t("foreman-hostgroup"),
                    'options' => [
                        OPTION_NONE => [
                            'label' => t('none'),
                        ],
                        '' => [
                            'label' => '- - - - - - - - - - - - - - - - - - - - ',
                        ],
                    ],
                ];
                if ($aForemanHostgroups && count($aForemanHostgroups)) {
                    foreach ($aForemanHostgroups as $aItem) {
                        $bActive = $iForemanHostgroupDefault === (int) $aItem['id'];
                        $aSelectForemanGroups['options'][$aItem['id']] = [
                            'label' => $aItem['title'],
                            'selected' => $bActive ? 'selected' : false,
                        ];
                        $sForemanHostgroupDefault = $bActive ? $aItem['title'] : $sForemanHostgroupDefault;
                    }
                }
            }
    
    
            $i = 0;
    
            $aPrefixItem = count($this->getVersions()) ?
                [
                    'type' => 'markup',
                    'value' => '<div class="form-group">
                            <label class="col-sm-2">' . t('fileprefix') . '</label>
                            <div class="col-sm-10">
                                <input id="inputprefix" type="hidden" name="fileprefix" value="' . $this->_aPrjConfig["fileprefix"] . '">
                                ' . $this->_aPrjConfig["fileprefix"] . '
                            </div></div>
                                ',
                ] : [
                    'type' => 'text',
                    'name' => 'fileprefix',
                    // 'disabled' => 'disabled',
                    'label' => t('fileprefix-label'),
                    'value' => $this->_aPrjConfig["fileprefix"],
                    'required' => 'required',
                    'validate' => 'isastring',
                    'pattern' => '[a-z0-9\-_]*',
                    'size' => 100,
                    'placeholder' => '',
                ];
    
            // detect access to repo url
            $aBranches = $this->getRemoteBranches(true);
            // $aRepodata = $this->getRepoRevision();
    
            // if (is_array($aRepodata) && array_key_exists("message", $aRepodata)) {
            if (is_array($aBranches) && count($aBranches)) {
                $sRepoCheck = '<span class="ok">' . sprintf(t('class-project-info-repoaccess'), count($aBranches)) . '</span>';
            } else {
                $sRepoError = sprintf(t('class-project-error-no-repoaccess'), $aRepodata["error"]);
                $sRepoCheck = '<span class="error">' . $sRepoError . '</span>';
                $sMessages .= $this->_oHtml->getBox("error", $sRepoError);
            }
    
            // generate datalist with exisating ssh keys for auth field
            $sAuthListitems = '';
            foreach ($this->_getSshKeys() as $sKey) {
                $sAuthListitems .= '<option value="' . $sKey . '">';
            }
            $aForms = [
                'setup' => [
                    'meta' => [
                        'method' => 'POST',
                        'action' => '?',
                    ],
                    'validate' => [],
                    'form' => [
                        'input' . $i++ => [
                            'type' => 'hidden',
                            'name' => 'setupaction',
                            'value' => 'save',
                        ],
                        'input' . $i++ => [
                            'type' => 'hidden',
                            'name' => 'id',
                            'value' => $this->_aConfig["id"],
                        ],
                        'input' . $i++ => [
                            'type' => 'markup',
                            'value' => '<div class="tabbable">
                                <ul class="nav nav-tabs">
                                    <li class="active"><a href="#tab1" class="nav-link active" data-toggle="tab" aria-selected="1">' . $this->_oHtml->getIcon('list') . t('setup-metadata') . '</a></li>
                                    <li><a href="#tab2" class="nav-link" data-toggle="tab">' . $this->_oHtml->getIcon('repository') . t('repositoryinfos') . '</a></li>
    
                                    <li><a href="#tab3" class="nav-link" data-toggle="tab">' . $this->_oHtml->getIcon('deploy-configfile') . t('deploy-configfile') . '</a></li>
                                    <li><a href="#tab4" class="nav-link" data-toggle="tab">' . $this->_oHtml->getIcon('deploy-rollout-plugin') . t('deploy-rollout-plugin') . '</a></li>
                                    <li><a href="#tab5" class="nav-link" data-toggle="tab">' . $this->_oHtml->getIcon('phase') . t('phases') . '</a></li>
                                    <li><a href="#tab6" class="nav-link" data-toggle="tab">' . $this->_oHtml->getIcon('raw-data') . t('raw-data') . '</a></li>
                                </ul>
                                <div class="tab-content">
                                <div class="tab-pane fade active show" id="tab1">
                                <br>
                                
                                ',
                        ],
    
                        // --------------------------------------------------
                        // Tab for metadata
                        // -------------------------------------------------
                        'input' . $i++ => [
                            'type' => 'text',
                            'name' => 'label',
                            'label' => t('projectname'),
                            'value' => $this->_aPrjConfig["label"],
                            'required' => 'required',
                            'validate' => 'isastring',
                            'size' => 100,
                            'placeholder' => 'Projekt',
                        ],
                        'input' . $i++ => [
                            'type' => 'text',
                            'name' => 'description',
                            'label' => t('projectdescription'),
                            'value' => $this->_aPrjConfig["description"],
                            'required' => 'required',
                            'validate' => 'isastring',
                            'size' => 100,
                            'placeholder' => '',
                        ],
                        'input' . $i++ => [
                            'type' => 'text',
                            'name' => 'contact',
                            'label' => t('contact'),
                            'value' => $this->_aPrjConfig["contact"],
                            'required' => 'required',
                            'validate' => 'isastring',
                            'size' => 100,
                            'placeholder' => '',
                        ],
    
                        'input' . $i++ => $aSelectProjectGroup,
    
                        'input' . $i++ => [
                            'type' => 'markup',
                            'value' => '<p>' . t('messenger') . '</p>',
                        ],
                        'input' . $i++ => [
                            'type' => 'text',
                            'name' => 'messenger[email]',
                            'label' => t("messenger-email"),
                            'value' => $this->_aPrjConfig["messenger"]["email"],
                            'validate' => 'isastring',
                            'size' => 100,
                            'placeholder' => '',
                            'autocomplete' => 'off',
                        ],
    
                        'input' . $i++ => $aSelectSlack,
    
                        // --------------------------------------------------
                        // Tab soources repository & build
                        // --------------------------------------------------
                        'input' . $i++ => [
                            'type' => 'markup',
                            'value' => ' </div><div class="tab-pane fade" id="tab2">
                                <p>' . t('setup-hint-build') . '</p>',
                        ],
                        'input' . $i++ => [
                            'type' => 'text',
                            'name' => 'build[type]',
                            'label' => t("build-type"),
                            'value' => $this->_aPrjConfig["build"]["type"],
                            'required' => 'required',
                            'validate' => 'isastring',
                            'size' => 100,
                            'placeholder' => '',
                        ],
                        'input' . $i++ => [
                            'type' => 'text',
                            'name' => 'build[url]',
                            'label' => t("repository-url"),
                            'value' => $this->_aPrjConfig["build"]["url"],
                            // 'required' => 'required',
                            'validate' => 'isastring',
                            'size' => 100,
                            'placeholder' => '',
                        ],
                        'input' . $i++ => [
                            'type' => 'text',
                            'name' => 'build[auth]',
                            'label' => t("repository-auth"),
                            'value' => $this->_aPrjConfig["build"]["auth"],
                            // 'required' => 'required',
                            'list' => 'listauth', // listauth is the next form id below
                            'validate' => 'isastring',
                            'size' => 100,
                            'placeholder' => '',
                        ],
                        'input' . $i++ => [
                            'type' => 'markup',
                            'value' => '<datalist id="listauth">' . $sAuthListitems . '</datalist>',
                        ],
                        'input' . $i++ => [
                            'type' => 'markup',
                            'value' => '<div class="form-group">'
                                . '<label class="col-sm-2"> </label><div class="col-sm-10">'
                                . $sRepoCheck
                                . '</div></div>',
                        ],
                        'input' . $i++ => [
                            'type' => 'text',
                            'name' => 'build[webaccess]',
                            'label' => t("repository-urlwebgui"),
                            'value' => $this->_aPrjConfig["build"]["webaccess"],
                            'validate' => 'isastring',
                            'size' => 100,
                            'placeholder' => '',
                        ],
                        'input' . $i++ => $aPrefixItem,
                        'input' . $i++ => [
                            'type' => 'markup',
                            'value' => '<div style="clear: both"></div>',
                        ],
                        // task#1498 - handle project without "public" directory
                        'input' . $i++ => [
                            'type' => 'checkbox',
                            'name' => 'build[haspublic]',
                            'label' => t("repository-has-public-dir"),
                            'required' => false,
                            'validate' => 'isastring',
                            'options' => [
                                '1' => [
                                    'label' => t("yes"),
                                    'checked' => (array_key_exists('haspublic', $this->_aPrjConfig["build"]) ? $this->_aPrjConfig["build"]["haspublic"] : 0),
                                ],
                            ],
                        ],
    
                        // --------------------------------------------------
                        // Tab for config and API key
                        // --------------------------------------------------
                        'input' . $i++ => [
                            'type' => 'markup',
                            'value' => ' </div><div class="tab-pane fade" id="tab3">
                                <p>' . t('deploy-configfile-hint') . '</p>',
                        ],
                        'textarea' . $i++ => [
                            'type' => 'textarea',
                            'name' => 'deploy[configfile]',
                            'label' => t("deploy-configfile"),
                            'value' => $this->_aPrjConfig['deploy']["configfile"],
                            // 'required' => 'required',
                            'validate' => 'isastring',
                            'cols' => 100,
                            'rows' => 10,
                            'placeholder' => 'export myvariable=&quot;hello world&quot;',
                        ],
    
                        'input' . $i++ => [
                            'type' => 'text',
                            'name' => 'api[secret]',
                            'label' => t("api-secret"),
                            'value' => $this->_aPrjConfig["api"]["secret"],
                            'validate' => 'isastring',
                            'size' => 100,
                            'placeholder' => '',
                        ],
                        'input' . $i++ => [
                            'type' => 'markup',
                            'value' => '<div class="col-sm-12">'
                                . '<p>' . t('api-secret-hint') . '<br>'
                                . '<a href="#" class="btn btn-default" onclick="$(\'#input' . ($i - 2) . '\').val(generateSecret(64)); return false">' . t("api-secret-generate") . '</a>'
                                . '</p></div>',
                        ],
    
                        // --------------------------------------------------
                        // Tab rollout plugin
                        // --------------------------------------------------
                        'input' . $i++ => [
                            'type' => 'markup',
                            'value' => ' </div><div class="tab-pane fade" id="tab4">
                                <p>' . t('deploy-rollout-plugin-hint') . '</p>',
                        ],
                        // select box for active rollout plugin
                        $aRollout['project-select'],
    
                        // project based config 
                        'input' . $i++ => [
                            'type' => 'markup',
                            'value' => ''
                                . '<hr>'
                                . '<label class="col-sm-2">' . t('deploy-rollout-plugin-config') . '</label>'
                                . '<div class="col-sm-10">' . $aRollout['project-config'] . '</div>'
                        ],
                        // --------------------------------------------------
                        'input' . $i++ => [
                            'type' => 'markup',
                            'value' => ' </div><div class="tab-pane fade" id="tab5">
                                <p>' . sprintf(t("class-project-info-setup-phaseinfos"), $this->getNextPhase()) . '</p>',
                        ],
                    ],
                ],
            ];
            // --------------------------------------------------
            // Tab for phases
            // --------------------------------------------------
            if ($aSelectForemanGroups) {
                $aForms["setup"]["form"]['input' . $i++] = [
                    'type' => 'markup',
                    'value' => '<strong>' . t("defaults-all-phases") . '</strong><br><br>',
                ];
                $aForms["setup"]["form"]['input' . $i++] = $aSelectForemanGroups;
                $aForms["setup"]["form"]['input' . $i++] = [
                    'type' => 'markup',
                    'value' => '<br><br>',
                ];
            }
            foreach (array_keys($this->getPhases()) as $sPhase) {
    
                $bActivePhase = $this->isActivePhase($sPhase);
                $sUrl = array_key_exists("url", $this->_aPrjConfig["phases"][$sPhase]) ? $this->_aPrjConfig["phases"][$sPhase]["url"] : "";
                $sDeploymethod = array_key_exists("deploymethod", $this->_aPrjConfig["phases"][$sPhase]) ? $this->_aPrjConfig["phases"][$sPhase]["deploymethod"] : "";
                $sDeployhosts = array_key_exists("hosts", $this->_aPrjConfig["phases"][$sPhase]) ? $this->_aPrjConfig["phases"][$sPhase]["hosts"] : "";
    
                /*
                 * task-1847 - reove adding ssh key
                if($sDeployhosts){
                    echo "$sDeployhosts<br>";
                    if(!strpos($sDeployhosts, ",")){
                        $sCmd=sprintf($this->_aConfig["installPackages"]["addkeycommand"], $sDeployhosts, $sDeployhosts);
                        exec($sCmd . " 2>&1", $aOut);
                        echo "<pre>\$ $sCmd<br>"
                            . implode('<br>', $aOut)
                            ."</pre>"
                            ;
                    }
                }
                 */
                $sDeploytimes = array_key_exists("deploytimes", $this->_aPrjConfig["phases"][$sPhase]) ? $this->_aPrjConfig["phases"][$sPhase]["deploytimes"] : "";
                $sDivId4PhaseSettings = 'divSettings' . $sPhase;
                $sDivId4TargetHosts = 'divSettings' . $sPhase . 'hosts';
    
                if ($aSelectForemanGroups) {
                    $iForemanHostgroup = (int) $this->_aPrjConfig['phases'][$sPhase]['foreman-hostgroup'];
                    $aSelectForemanHostGroup = [
                        'type' => 'select',
                        'name' => 'phases[' . $sPhase . '][foreman-hostgroup]',
                        'label' => $this->_oHtml->getIcon('foreman') . t("foreman-hostgroup"),
                        'options' => [
                            OPTION_DEFAULT => [
                                'label' => t('default') . ' (' . $sForemanHostgroupDefault . ')',
                                'selected' => $iForemanHostgroup === OPTION_DEFAULT ? 'selected' : false,
                            ],
                            OPTION_NONE => [
                                'label' => t('none'),
                                'selected' => $iForemanHostgroup === OPTION_NONE ? 'selected' : false,
                            ],
                            '' => [
                                'label' => '- - - - - - - - - - - - - - - - - - - - ',
                            ],
                        ],
                    ];
                    if (is_array($aForemanHostgroups) && count($aForemanHostgroups)) {
                        foreach ($aForemanHostgroups as $aItem) {
                            $aSelectForemanHostGroup['options'][$aItem['id']] = [
                                'label' => $aItem['title'],
                                'selected' => ($iForemanHostgroup === $aItem['id']) ? 'selected' : false,
                            ];
                        }
                    }
                }
                $aForms["setup"]["form"]['input' . $i++] = [
                    'type' => 'markup',
                    'value' => ''
                        // .'<pre>'.print_r($this->_aPrjConfig["phases"][$sPhase], 1).'</pre>'
                        /*
                          . '<a class="'.$sPhase.'">'
                          . t("phase") . ' ' . $sPhase
                          . '</a>'
                         */
                        . '<table class="table">'
                        . '<tbody>'
                        . '<tr><th class="' . $sPhase . '">' . $this->_oHtml->getIcon('phase') . t("phase") . ' ' . $sPhase . '</th></tr>'
                        . '<tr><td class="' . ($bActivePhase ? $sPhase : '') . '">'
                        . ''
                ];
    
                $aForms["setup"]["form"]['input' . $i++] = [
                    'type' => 'checkbox',
                    'name' => 'phases[' . $sPhase . '][active]',
                    'label' => t("phase-is-active"),
                    // 'value' => $bUsePuppet,
                    'required' => false,
                    'validate' => 'isastring',
                    // 'size' => 100,
                    // 'placeholder' => '...',
                    'options' => [
                        '1' => [
                            'label' => t("yes"),
                            'checked' => $bActivePhase,
                            'onclick' => '$(\'#' . $sDivId4PhaseSettings . '\').css(\'display\', (this.checked ? \'block\' : \'none\') )',
                        ],
                    ],
                ];
                $aForms["setup"]["form"]['input' . $i++] = [
                    'type' => 'markup',
                    'value' => ''
                        . '<div id="' . $sDivId4PhaseSettings . '" ' . ($bActivePhase ? '' : ' style="display: none;"') . '>'
                ];
                $aForms["setup"]["form"]['input' . $i++] = [
                    'type' => 'text',
                    'name' => 'phases[' . $sPhase . '][url]',
                    'label' => $this->_oHtml->getIcon('url') . t("url-project-website"),
                    'value' => $sUrl,
                    // 'required' => 'required',
                    'validate' => 'isastring',
                    'size' => 100,
                    'placeholder' => 'https://' . $sPhase . '.[' . t("project") . '].[...]/',
                ];
                $aForms["setup"]["form"]['input' . $i++] = [
                    'type' => 'radio',
                    'name' => 'phases[' . $sPhase . '][deploymethod]',
                    'label' => $this->_oHtml->getIcon('method') . t("deploymethod"),
                    // 'value' => $bUsePuppet,
                    // 'required' => 'required',
                    'validate' => 'isastring',
                    // 'size' => 100,
                    // 'placeholder' => '...',
                    'options' => [
                        'none' => [
                            'label' => t("deploymethod-none"),
                            'checked' => $sDeploymethod === "none",
                            'onclick' => '$(\'#' . $sDivId4TargetHosts . '\').css(\'display\', (this.checked ? \'none\' : \'block\') )',
                        ],
                        'rolloutplugin' => [
                            // 'label' => t("deploymethod-puppet").' - '.  $this->oRolloutPlugin->getName(),
                            'label' => t("deploymethod-rolloutplugin"),
                            'checked' => $sDeploymethod === "rolloutplugin",
                            'onclick' => '$(\'#' . $sDivId4TargetHosts . '\').css(\'display\', (this.checked ? \'block\' : \'none\') )',
                        ],
                        /*
                         * see deploy method to handle an action
                          'sshproxy' => [
                            'label' => t("deploymethod-sshproxy"),
                            'checked' => $sDeploymethod==="sshproxy",
                            'onclick' => '$(\'#'.$sDivId4TargetHosts.'\').css(\'display\', (this.checked ? \'block\' : \'none\') )',
                            ],
                         */
                    ],
                ];
    
    
                $aForms["setup"]["form"]['input' . $i++] = [
                    'type' => 'markup',
                    'value' => ''
                        . '<div id="' . $sDivId4TargetHosts . '" ' . ($sDeploymethod !== "none" ? '' : ' style="display: none;"') . '>'
                ];
    
                // rollout plugin: phase specific overrides
                $aForms["setup"]["form"]['input' . $i++] = [
                    'type' => 'markup',
                    'value' => ''
                        // . '<hr>'
                        . '<label class="col-sm-2">' . t('deploy-rollout-plugin-config') . '</label>'
                        . '<div class="col-sm-10">' . $aRollout[$sPhase] . '</div>'
                ];
    
                $aForms["setup"]["form"]['input' . $i++] = [
                    'type' => 'text',
                    'name' => 'phases[' . $sPhase . '][hosts]',
                    'label' => $this->_oHtml->getIcon('host') . t("phase-targethosts"),
                    'value' => $sDeployhosts,
                    // 'required' => 'required',
                    'validate' => 'isastring',
                    'size' => 100,
                    'placeholder' => 'FQDN1,FQDN2',
                ];
    
                /*
                  if ($sPuppethost) {
    
                  // add ssh host key
                  $sOut0 = shell_exec(sprintf($this->_aConfig["installPackages"]["addkeycommand"], $sPuppethost, $sPuppethost));
    
                  $sCmd2 = 'ssh ' . $this->_aConfig["installPackages"]["user"]
                  . '@' . $sPuppethost
                  . ' ' . $this->_aConfig["installPackages"]["testcommand"];
                  $sOut = 'skip';
                  // $sOut = shell_exec($sCmd2);
                  // Check auf Versionsnummer - mehr als n Zeichen ist mutmasslich eine Fehlermeldung
                  if (strlen($sOut) > 7) {
                  $sMessages.=$this->getBox("error", sprintf(t("class-project-error-setup-sudo-pupet-agent-failed"), $sPhase, $sCmd, $sOut));
                  $sOut = '<span class="error" title="' . $sCmd . '">' . $sOut . '</span>';
                  } else {
                  $sOut = '<span class="ok">' . sprintf(t("class-project-info-setup-ssh-and-puppet-ok"), $sPuppethost) . '</span>';
                  }
                  $aForms["setup"]["form"]['input' . $i++] = [
                  'type' => 'markup',
                  'value' => '<div class="form-group">'
                  . '<label class="col-sm-2"> </label><div class="col-sm-10">'
                  . $sOut
                  . '</div></div>',
                ];
                  }
                 */
    
                // when to deploy
                $aForms["setup"]["form"]['input' . $i++] = [
                    'type' => 'text',
                    'name' => 'phases[' . $sPhase . '][deploytimes]',
                    'label' => $this->_oHtml->getIcon('time') . t("deploytimes"),
                    'value' => $sDeploytimes,
                    // 'required' => 'required',
                    'validate' => 'isastring',
                    'size' => 100,
                    'placeholder' => isset($this->_aConfig["phases"][$sPhase]["deploytimes"]) ? implode(", ", $this->_aConfig["phases"][$sPhase]["deploytimes"]) : '',
                ];
                $aForms["setup"]["form"]['input' . $i++] = [
                    'type' => 'markup',
                    'value' => ''
                        . '</div>'
                ];
    
                if ($aSelectForemanGroups) {
                    $aForms["setup"]["form"]['input' . $i++] = $aSelectForemanHostGroup;
                }
    
                $aForms["setup"]["form"]['input' . $i++] = [
                    'type' => 'markup',
                    'value' => ''
                        . '</div>'
                ]; // close div for active phase
    
    
                $aForms["setup"]["form"]['input' . $i++] = [
                    'type' => 'markup',
                    'value' => '</td></tr></tbody></table>',
                ];
            } // END: loop over phases
    
            // --------------------------------------------------
            // Tab for raw data
            // --------------------------------------------------
    
            $sRolloutDebug = '<hr>DEBUG:<br>';
            foreach (array_keys($this->getPhases()) as $sPhase) {
                if ($this->isActivePhase($sPhase)) {
                    $sRolloutDebug .= '<strong>' . $sPhase . '</strong>'
                        . '<pre>Config = ' . print_r($this->oRolloutPlugin->getConfig($sPhase, 1), 1) . '</pre>'
                        . '<pre>Commands = ' . print_r($this->oRolloutPlugin->getDeployCommands($sPhase, 1), 1) . '</pre>'
                    ;
                }
            }
    
            $aForms["setup"]["form"]['input' . $i++] = [
                'type' => 'markup',
                'value' => '</div>'
    
                    . '<div class="tab-pane fade" id="tab6">'
                    . '<br><pre>' . print_r($this->_aPrjConfig, 1) . '</pre>'
                    . $sRolloutDebug
                    . '</div>'
    
                    . '</div>'
                    . '</div>'
                    . '<div style="clear: both; margin-bottom: 1em;"></div>'
    
    
                    . '<hr>',
            ];
            $aForms["setup"]["form"]['input' . $i++] = [
                'type' => 'submit',
                'name' => 'btnsave',
                'label' => t("save"),
                'value' => $this->_oHtml->getIcon('sign-ok') . t("save"),
            ];
    
            $oForm = new formgen($aForms);
            return $sMessages . $oForm->renderHtml("setup");
        }
    
        /**
         * Get html code for the installed version in the repository
         * @param boolean  $bRefresh  optional: refresh flag; default: use cached information
         * @return string
         */
        public function renderRepoInfo(bool $bRefresh = false): string
        {
            $sReturn = "";
            switch ($this->_aPrjConfig["build"]["type"]) {
                case "git":
    
                    $aRepodata = $this->getRepoRevision($bRefresh);
                    if (array_key_exists("revision", $aRepodata)) {
                        $sReturn .= $this->_getChecksumDiv(
                            $aRepodata["revision"],
                            $this->_oHtml->getIconByType('branch') . t('branch') . ': ' . (array_key_exists("branch", $aRepodata) ? $aRepodata["branch"] : '-') . '<br>'
                            . $this->_oHtml->getIconByType('revision') . t('revision') . ': ' . $this->_renderRevision($aRepodata["revision"]) . '<br>'
                            . $this->_oHtml->getIconByType('comment') . t('commitmessage') . ':<br>'
                        )
                            . "<pre>" . htmlentities($aRepodata["message"]) . "</pre>";
                    } else {
                        $sReturn .= $this->_oHtml->getBox("error", sprintf(t('class-project-error-no-repoinfo'), $aRepodata["error"]))
                            . $this->renderLink("setup") . '<br>';
                    }
    
                    break;
    
                default:
                    $sReturn .= $this->_oHtml->getBox("error", sprintf(t('class-project-error-wrong-buildtype'), $this->_aPrjConfig["build"]["type"]));
            }
            if (array_key_exists("url", $this->_aPrjConfig["build"])) {
                $sReturn .= t('repository-url') . ': ' . $this->_aPrjConfig["build"]["url"] . '<br>';
            }
            if (array_key_exists("webaccess", $this->_aPrjConfig["build"])) {
                $sReturn .= t('repository-access-browser') . ':<br><a href="' . $this->_aPrjConfig["build"]["webaccess"] . '">' . $this->_aPrjConfig["build"]["webaccess"] . '</a><br>';
            }
            return $sReturn;
        }
    
        /**
         * Get html code for a link to the commit
         * (works for guithub and gitlab instances)
         * 
         * @param null|string  $sRevision
         * @return string
         */
        public function _renderRevision(null|string $sRevision): string
        {
            $sUrl = str_replace('/tree/master', '', $this->_aPrjConfig["build"]["webaccess"]) . '/commit/' . $sRevision;
            return '<a href="' . $sUrl . '">' . $sRevision . '</a>';
            // return $sUrl;
        }
    
        /**
         * Get html form with selectr for remote branches
         * 
         * @param string $sActiveBranchname  force active branch name
         * @param bool $bIgnoreCache  flag to ignore exiting cached data
         * @return string
         */
        public function renderSelectRemoteBranches(string $sActiveBranchname = '', bool $bIgnoreCache = false): string
        {
            $this->log(__FUNCTION__ . "(sActiveBranchname = $sActiveBranchname, bIgnoreCache = " . ($bIgnoreCache ? 'true' : 'false') . ") start");
            $aReturn = [];
            $aRadios = [];
            $bFoundActive = false;
            $i = 0;
            if (!$this->_oVcs) {
                $this->_initVcs();
            }
            require_once("formgen.class.php");
            if (!$sActiveBranchname) {
                $sActiveBranchname = $this->_sBranchname;
            }
            if ($this->_oVcs) {
                if (!method_exists($this->_oVcs, "getRemoteBranches")) {
                    // the version control class does not have this method
                    return '';
                }
                foreach ($this->_oVcs->getRemoteBranches($bIgnoreCache) as $aBranch) {
                    $sBranch = $aBranch['name'];
                    $aRadios[$sBranch] = [
                        'value' => $sBranch,
                        'label' => $aBranch['label'],
                    ];
                    // if no param was given the first branch will be marked
                    if (!$sActiveBranchname) {
                        $sActiveBranchname = $sBranch;
                    }
                    if ($sBranch == $sActiveBranchname) {
                        $bFoundActive = true;
                        // $aRadios[$sBranch]['checked'] = 'checked';
                        $aRadios[$sBranch]['selected'] = 'selected';
                    } else {
                        // for SELECT we need the onclick even on select element
                        // not on the option (Chrome)
                        // $aRadios[$sBranch]['onclick'] = 'document.getElementById(\'submitBranch\').click()';
                    }
                }
                ;
            }
            // no branches were found
            if (count($aRadios) == 0) {
                return '';
            }
    
            $aForms = [
                'frmSelectBranch' => [
                    'meta' => [
                        'method' => 'POST',
                        'action' => '?',
                        'id' => 'frmSelectBranch',
                    ],
                    'validate' => [],
                    'form' => [
                        'branchname' => [
                            'inline' => true,
                            'type' => 'select',
                            'onchange' => 'document.getElementById(\'submitBranch\').click()',
                            'name' => 'branchname',
                            'label' => '<strong>' . t('branch-select') . '</strong>',
                            'validate' => 'isastring',
                            'options' => $aRadios,
                        ],
                    ],
                ],
            ];
    
            // submit to switch branches - only if a selection is available
            if (count($aRadios) > 1 || !$bFoundActive) {
                $aForms['frmSelectBranch']['form']['submitBranch'] = [
                    'type' => 'submit',
                    'name' => 'btnsave',
                    'onclick' => 'showModalMessage(\'' . t('branch-switch') . '\'); ',
                    'label' => t("change"),
                    'value' => $this->_oHtml->getIcon('sign-ok') . t("change"),
                ];
            }
    
            $oFrm = new formgen($aForms);
            return $oFrm->renderHtml('frmSelectBranch')
                . '<script>$("#submitBranch").hide();</script>';
            // return $oFrm->renderHtmlElement('dummy',$aFormData);
        }
    
        /**
         * Get html code for a list of all built packages and their usage
         * @return string
         */
        public function renderVersionUsage(): string
        {
            $sReturn = false;
            $sRowHead1 = false;
            $sRowHead2 = '<td></td>';
    
            $aAllVersions = $this->_getVersionUsage();
            if (!count($aAllVersions)) {
                return $this->_oHtml->getBox("info", t('class-project-info-no-package'));
            }
    
            foreach ($this->getActivePhases() as $sPhase) {
                $sRowHead1 .= '<th class="' . $sPhase . ' tdphase" colspan="' . (count($this->_aPlaces) + 1) . '">' . $sPhase . '</th>';
                $sRowHead2 .= '<td></td>' . $this->renderPlacesAsTd($sPhase);
            }
    
            krsort($aAllVersions);
            foreach ($aAllVersions as $sVersion => $aData) {
                $sReturn .= '<tr>';
    
                $sInfos = $this->renderInfoLink($aData["info"], ['hpos' => 'left']);
                $sReturn .= '<td>'
                    . $this->_getChecksumDiv(
                        $aData['info']['revision'],
                        $this->_oHtml->getIconByType('calendar') . t('build-from') . ': ' . $sVersion . '<br>'
                        . $this->_oHtml->getIconByType('branch') . t('branch') . ': ' . $aData['info']["branch"] . '<br>'
                        . $this->_oHtml->getIconByType('revision') . t('revision') . ': ' . $this->_renderRevision($aData['info']["revision"]) . '<br>'
                    )
                    . '</td><td>'
                    . '&nbsp;&nbsp;' . $sInfos . '&nbsp;&nbsp;'
                    . '</td>'
                ;
    
                foreach ($this->getActivePhases() as $sPhase) {
                    $sTLine = '';
                    $bCanRollback = $aData["rollback"][$sPhase];
    
                    // $sReturn.=$aData["rollback"][$sPhase] ? '<td>'.$this->renderLink("rollback", $sPhase, $sVersion).'</td>' : '<td>Rollback NOT possible</td>';
                    // $sReturn.=$aData["rollback"][$sPhase] ? '<td> Y </td>' : '<td> N </td>';
                    $sReturn .= '<td>  </td>';
    
                    foreach (array_keys($this->_aPlaces) as $sPlace) {
                        $bFound = false;
                        $sReturn .= $aData["usage"][$sPhase][$sPlace]
                            ? '<td class="' . $sPhase . '" style="text-align: center;">'
                            . $this->_getChecksumDiv($aData['info']['revision'], 'X')
                            . '</td>'
                            : '<td> </td>'
                        ;
                    }
                }
                $sReturn .= '</tr>';
            }
    
            $sReturn = t('class-project-info-table-packages') . '<br><br>'
                . '<table>'
                . '<thead><tr><td>Version</td><td></td>'
                . $sRowHead1
                . '</tr><tr><td>'
                . $sRowHead2
                . '</tr></thead>'
                . '<tbody>'
                . $sReturn
                . '</tbody>'
                . '</table>';
            return $sReturn;
        }
    
        /**
         * Get html code for graphical overview of process (in project overview)
         * @return string
         */
        public function renderVisual(): string
        {
            $sReturn = '';
    
            $renderAdminLTE = new renderadminlte();
    
            $sBarHeightBg = '1.0em';
            $sBarHeight = '0.8em';
            $sContinue = '<span style="font-size: 300%; color:#ace;">&raquo;&raquo;</span><br>';
    
            $aBranches = $this->getRemoteBranches();
            if (!is_array($aBranches)) {
                return '<br>' . $this->_oHtml->getBox("error", t("project-setup-incomplete"));
            }
    
            $sRepoBar = '';
            /*
                Speedup:
                
            $aRepodata = $this->getRepoRevision();
            if (array_key_exists("revision", $aRepodata)) {
                $sRepoBar = $this->_getChecksumDiv($aRepodata["revision"]);
            } else {
                $sRepoBar = '<span class="error">' . t("error") . '</span>';
            }
            */
    
            $sPackagebar = '';
            $aVersions = $this->_getVersionUsage();
            foreach ($aVersions as $sVersion => $aData) {
                $sBar = $aData["info"]["revision"] ? $this->_getChecksumDiv($aData["info"]["revision"], '', $sBarHeight) : '';
                $sPackagebar .= '<span title="' . $sVersion . '" style="float: left; background:#eee; height: ' . $sBarHeightBg . '; width:' . (100 / count($aVersions)) . '%">' . $sBar . '&nbsp;</span>';
            }
    
            $sPhaseImg = '';
            $sLastPhase = '';
            foreach ($this->getActivePhases() as $sPhase) {
                if ($sPhaseImg) {
                    $sAction = $sContinue;
                    if ($this->canAcceptPhase($sLastPhase)) {
                        $sAction .= $this->renderLink("accept", $sLastPhase);
                    }
                    $sPhaseImg .= '<div class="action">' . $sAction . '</div>';
                }
                $sLastPhase = $sPhase;
    
                $sFullbar = '';
                foreach (array_keys($this->_aPlaces) as $sPlace) {
                    $sFullbar .= '<span title="' . $this->_aPlaces[$sPlace] . '" style="float: left; background:#eee; height: ' . $sBarHeightBg . '; width:' . (100 / count($this->_aPlaces)) . '%">' . $this->_renderBar($sPhase, $sPlace, $sBarHeight) . '&nbsp;</span>';
                }
                // $sDetail = $sFullbar . '<br><a href="#h3phases" class="scroll-link">' . $sPhase . '</a>';
                $sDetail = $sFullbar . '<br>' . $sPhase;
    
                $sPhaseImg .= '
                <div class="process">
                    <div class="details">' . $sDetail . ' </div>
                </div>';
            }
    
            $sReturn .= ''
                . '<div class="visualprocess">'
                . $renderAdminLTE->addRow(
                    ''
                    . $renderAdminLTE->addCol(
                        $renderAdminLTE->getCard(
                            [
                                'type' => '',
                                'variant' => 'outline',
                                'title' => '',
                                'text' => '<h3>' . t("versioncontrol") . '</h3>' . $sRepoBar . '<br>
                                ' . t("repositoryinfos") . '<br>
                                ' // . $this->_aPrjConfig["build"]["type"] 
                                    . preg_replace('/.*\@(.*):.*/', '$1', $this->_aPrjConfig["build"]["url"])
                                    . '<br>(<strong title="' . t('branch-select') . '">' . count($aBranches) . '</strong>)',
                            ]
                        ),
                        2
                    )
                    . $renderAdminLTE->addCol(
                        '<div class="action">' . $sContinue . t("build-hint-overview") . '<br><br>' . ($this->canAcceptPhase() ? $this->renderLink("build") : '') . '</div>',
                        1
                    )
                    . $renderAdminLTE->addCol(
                        $renderAdminLTE->getCard(
                            [
                                'type' => '',
                                'variant' => 'outline',
                                'title' => '',
                                'text' => '<h3>' . $this->_oHtml->getIcon('package') . t("archive") . '</h3>'
                                    . '<div class="archive">'
                                    . '<div class="details">'
                                    . $sPackagebar . '<br>'
                                    . '</div>'
                                    . t("packages") . '<br>'
                                    . '(<strong>' . count($this->_getVersionUsage()) . '</strong>)'
                                    . '</div>'
                            ]
                        ),
                        2
                    )
                    . $renderAdminLTE->addCol(
                        '<div class="action">' . $sContinue . sprintf(t("queue-hint-overview"), $this->getNextPhase()) . '</div>',
                        1
                    )
                    . $renderAdminLTE->addCol(
                        $renderAdminLTE->getCard(
                            [
                                'type' => '',
                                'variant' => 'outline',
                                'title' => '',
                                'text' => '<h3>' . $this->_oHtml->getIcon('phase') . t("phases") . '</h3>'
                                    . ($sPhaseImg ? $sPhaseImg : '<div class="process">' . t("none") . '</div>')
                            ]
                        ),
                        6
                    )
                )
                . '</div>'
            ;
    
            return $sReturn;
        }
    
    }