Compare commits

..

48 Commits

Author SHA1 Message Date
Eric Espie
3c80c93b4f Dashlet extraction 2026-01-16 18:47:28 +01:00
Eric Espie
efc6e6730b Dashlet extraction 2026-01-16 09:57:14 +01:00
Eric Espie
3769bc1024 Dashlet definition for core dashlets 2026-01-16 09:34:07 +01:00
Stephen Abello
e5338f28eb Save actions submit the form, it has yet to validate the form 2026-01-15 15:45:52 +01:00
Stephen Abello
5cfe7fa6eb Allow dashboard to restore state (either saved state or from backend). Restore old state on edition cancel 2026-01-15 11:51:49 +01:00
Stephen Abello
27c16a782c Remove done todo 2026-01-15 11:09:44 +01:00
Stephen Abello
7c21178e1d Handle backend save response in dashboard js 2026-01-15 11:08:20 +01:00
Stephen Abello
3a6e148c11 Prepare steps for custom dashboard handling 2026-01-15 10:56:35 +01:00
Eric Espie
4d9e18890a fix warnings 2026-01-15 10:44:20 +01:00
Eric Espie
50ffaa2b55 Read custom dashboards with grid format 2026-01-15 10:19:33 +01:00
Eric Espie
15c8f5903b save dashboard controller 2026-01-14 16:57:19 +01:00
Stephen Abello
0bc6f5d56a Align posted dashboard data to new data denormalized format 2026-01-14 11:39:56 +01:00
Stephen Abello
d248524cc8 Align posted dashboard data to new data denormalized format 2026-01-14 11:15:15 +01:00
Stephen Abello
410dc152d7 Make cloning dashlet actually use original dashlet data to get a new similar dashlet 2026-01-14 10:01:38 +01:00
Stephen Abello
423413e3a0 Fix form interaction with buttons when screen is smaller 2026-01-14 09:42:37 +01:00
Stephen Abello
8896c576d7 Make form style + button style match mockup 2026-01-14 09:32:11 +01:00
Benjamin DALSASS
975046da17 Add ChoiceImageFormBlock 2026-01-14 08:19:03 +01:00
Benjamin DALSASS
e4a281c3ff Add ChoiceImageFormBlock 2026-01-14 07:32:29 +01:00
Eric Espie
90729f84b6 Polymorphic type 2026-01-13 18:00:34 +01:00
Stephen Abello
c075a5c145 Make dashlet edition / refresh better interact with listener and gridstack library in order to allow multiple edits without breaking 2026-01-13 16:30:00 +01:00
Benjamin DALSASS
cd6d130bcb Move dashboard blocks to itop project and keep demonstrator one 2026-01-13 11:24:56 +01:00
Stephen Abello
7ca2c56dad Send denormalized data to backend in order to persist dashboard 2026-01-13 11:18:18 +01:00
Benjamin DALSASS
dd0ac58643 Prepare property service for form block dashboard 2026-01-13 07:36:47 +01:00
Stephen Abello
df943ec8b5 Change DashletBadge preferred height 2026-01-12 16:28:18 +01:00
Stephen Abello
076a6d0495 Make dashlet position non-random. Use their preferred width/height for now 2026-01-12 16:26:52 +01:00
Stephen Abello
02e59c906b Remove dummy buttons used for development purpose 2026-01-12 16:11:43 +01:00
Stephen Abello
49f91961e7 Make dashlet rendering computing request data 2026-01-12 15:09:14 +01:00
Stephen Abello
441519d71d Rename parameter 2026-01-12 15:08:33 +01:00
Eric Espie
5c75d0ce7c Fix some value types 2026-01-12 13:50:28 +01:00
Eric Espie
2efe80265d Pass form values from dashlet 2026-01-12 12:02:45 +01:00
Eric Espie
b58a64e143 Add Icon selection formBlock 2026-01-12 11:44:44 +01:00
Stephen Abello
ff11aec7fe Make clone dashlet ask for a rendered dashlet to the backend 2026-01-12 11:10:08 +01:00
Benjamin DALSASS
f79bb9d51c Add option display to form (css needs updates)
Add options with_run_button and with_book_button to FormBlockOql
2026-01-12 08:00:17 +01:00
Stephen Abello
3c365fc103 Make add / edit in dashboard interactive, and pass on data 2026-01-09 16:31:36 +01:00
Stephen Abello
7b193dd737 Use npm for gridstack, remove unnecessary folders using our dedicated service 2026-01-09 15:10:10 +01:00
Eric Espie
1538596db8 PHP CS-Fixer 2026-01-09 11:04:38 +01:00
Eric Espie
9d4fc345bc Fix (de)normalized data for Forms 2026-01-09 10:51:32 +01:00
Eric Espie
08c9309572 Fix (de)normalized data for Forms 2026-01-08 17:41:58 +01:00
Eric Espie
d072aa05f1 Prepare (de)normalized data for Forms 2026-01-08 17:28:48 +01:00
Eric Espie
85c1f091e2 Prepare normalized data for Forms 2026-01-08 17:25:52 +01:00
Eric Espie
be6a8abdf4 N°9066 - Split XML serializer into normalizer and encoder 2026-01-08 17:23:06 +01:00
Benjamin DALSASS
60bcf0c85f Change AbstractForm BindingReceivedEvent 2026-01-08 16:02:23 +01:00
Benjamin DALSASS
4a8804b8ac Dashlet form in new dashboard layout (suite) 2026-01-08 15:30:51 +01:00
Benjamin DALSASS
b014b9f638 Dashlet form in new dashboard layout 2026-01-08 14:42:16 +01:00
Eric Espie
1c633c6173 typo 2026-01-08 13:45:11 +01:00
Stephen Abello
cdc95aca7b Dump autoloader 2026-01-08 10:18:30 +01:00
Stephen Abello
b4460999ef Dump autoloader 2026-01-08 10:17:35 +01:00
Stephen Abello
a713e1b56e N°8641 - Dashboard editor front-end first commit for Form SDK integration.
* No dashlet edition
* Dashboard are not persisted
* Unable to load a dashboard from an endpoint (refresh)
* Grid library need proper npm integration
2026-01-08 10:17:30 +01:00
224 changed files with 23533 additions and 4781 deletions

View File

@@ -97,77 +97,33 @@ class RestUtils
* @throws Exception
* @api
*/
public static function GetFieldList($sClass, $oData, $sParamName, $bFailIfNotFound = true)
public static function GetFieldList($sClass, $oData, $sParamName)
{
$sFields = self::GetOptionalParam($oData, $sParamName, '*');
return match($sFields) {
'*' => self::GetFieldListForClass($sClass),
'*+' => self::GetFieldListForParentClass($sClass),
default => self::GetLimitedFieldListForClass($sClass, $sFields, $sParamName, $bFailIfNotFound),
};
}
public static function HasRequestedExtendedOutput(string $sFields): bool
{
return match($sFields) {
'*' => false,
'*+' => true,
default => substr_count($sFields, ':') > 1,
};
}
public static function HasRequestedAllOutputFields(string $sFields): bool
{
return match($sFields) {
'*', '*+' => true,
default => false,
};
}
protected static function GetFieldListForClass(string $sClass): array
{
return [$sClass => array_keys(MetaModel::ListAttributeDefs($sClass))];
}
protected static function GetFieldListForParentClass(string $sClass): array
{
$aFieldList = array();
foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass) {
$aFieldList = array_merge($aFieldList, self::GetFieldListForClass($sRefClass));
}
return $aFieldList;
}
protected static function GetLimitedFieldListForSingleClass(string $sClass, string $sFields, string $sParamName, bool $bFailIfNotFound = true): array
{
$aFieldList = [$sClass => []];
foreach (explode(',', $sFields) as $sAttCode) {
$sAttCode = trim($sAttCode);
if (($sAttCode == 'id') || (MetaModel::IsValidAttCode($sClass, $sAttCode))) {
$aFieldList[$sClass][] = $sAttCode;
} else {
if ($bFailIfNotFound) {
throw new Exception("$sParamName: invalid attribute code '$sAttCode' for class '$sClass'");
$aShowFields = [];
if ($sFields == '*') {
foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef) {
$aShowFields[$sClass][] = $sAttCode;
}
} elseif ($sFields == '*+') {
foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sRefClass) {
foreach (MetaModel::ListAttributeDefs($sRefClass) as $sAttCode => $oAttDef) {
$aShowFields[$sRefClass][] = $sAttCode;
}
}
}
return $aFieldList;
}
protected static function GetLimitedFieldListForClass(string $sClass, string $sFields, string $sParamName, bool $bFailIfNotFound = true): array
{
if (!str_contains($sFields, ':')) {
return self::GetLimitedFieldListForSingleClass($sClass, $sFields, $sParamName, $bFailIfNotFound);
} else {
foreach (explode(',', $sFields) as $sAttCode) {
$sAttCode = trim($sAttCode);
if (($sAttCode != 'id') && (!MetaModel::IsValidAttCode($sClass, $sAttCode))) {
throw new Exception("$sParamName: invalid attribute code '$sAttCode'");
}
$aShowFields[$sClass][] = $sAttCode;
}
}
$aFieldList = [];
$aFieldListParts = explode(';', $sFields);
foreach ($aFieldListParts as $sClassFields) {
list($sSubClass, $sSubClassFields) = explode(':', $sClassFields);
$aFieldList = array_merge($aFieldList, self::GetLimitedFieldListForSingleClass(trim($sSubClass), trim($sSubClassFields), $sParamName, $bFailIfNotFound));
}
return $aFieldList;
return $aShowFields;
}
/**
* Read and interpret object search criteria from a Rest/Json structure
*

View File

@@ -5,6 +5,9 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Application\Dashboard\Layout\DashboardLayoutGrid;
use Combodo\iTop\Application\Dashlet\DashletFactory;
use Combodo\iTop\Application\Dashlet\Service\DashletService;
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableSettings;
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu;
@@ -12,9 +15,12 @@ use Combodo\iTop\Application\UI\Base\Component\Toolbar\ToolbarUIBlockFactory;
use Combodo\iTop\Application\UI\Base\Layout\Dashboard\DashboardLayout as DashboardLayoutUIBlock;
use Combodo\iTop\Application\WebPage\iTopWebPage;
use Combodo\iTop\Application\WebPage\WebPage;
use Combodo\iTop\DesignDocument;
use Combodo\iTop\DesignElement;
use Combodo\iTop\PropertyType\PropertyTypeDesign;
use Combodo\iTop\Service\DependencyInjection\ServiceLocator;
require_once(APPROOT.'application/dashboardlayout.class.inc.php');
require_once(APPROOT.'application/dashlet.class.inc.php');
require_once(APPROOT.'core/modelreflection.class.inc.php');
/**
@@ -24,7 +30,7 @@ require_once(APPROOT.'core/modelreflection.class.inc.php');
*/
abstract class Dashboard
{
/** @var string $sTitle*/
/** @var string $sTitle */
protected $sTitle;
/** @var bool $bAutoReload */
protected $bAutoReload;
@@ -34,7 +40,7 @@ abstract class Dashboard
protected $sLayoutClass;
/** @var array $aWidgetsData */
protected $aWidgetsData;
/** @var \DOMNode|null $oDOMNode */
/** @var DesignElement|null $oDOMNode */
protected $oDOMNode;
/** @var string $sId */
protected $sId;
@@ -43,6 +49,11 @@ abstract class Dashboard
/** @var \ModelReflection $oMetaModel */
protected $oMetaModel;
/** @var array Array of dashlets with position */
protected array $aGridDashlets = [];
protected $oDashletFactory;
/**
* Dashboard constructor.
*
@@ -57,6 +68,7 @@ abstract class Dashboard
$this->aCells = [];
$this->oDOMNode = null;
$this->sId = $sId;
$this->oDashletFactory = DashletFactory::GetInstance();
}
/**
@@ -68,7 +80,7 @@ abstract class Dashboard
{
$this->aCells = []; // reset the content of the dashboard
set_error_handler(['Dashboard', 'ErrorHandler']);
$oDoc = new DOMDocument();
$oDoc = new PropertyTypeDesign();
$oDoc->loadXML($sXml);
restore_error_handler();
$this->FromDOMDocument($oDoc);
@@ -77,10 +89,15 @@ abstract class Dashboard
/**
* @param \DOMDocument $oDoc
*/
public function FromDOMDocument(DOMDocument $oDoc)
public function FromDOMDocument(DesignDocument $oDoc)
{
$this->oDOMNode = $oDoc->getElementsByTagName('dashboard')->item(0);
if ($this->oDOMNode->getElementsByTagName('cells')->count() === 0) {
$this->FromDOMDocumentV2($oDoc);
return;
}
if ($oLayoutNode = $this->oDOMNode->getElementsByTagName('layout')->item(0)) {
$this->sLayoutClass = $oLayoutNode->textContent;
} else {
@@ -121,7 +138,7 @@ abstract class Dashboard
$aDashletOrder = [];
/** @var \DOMElement $oDomNode */
foreach ($oDashletList as $oDomNode) {
$oRank = $oDomNode->getElementsByTagName('rank')->item(0);
$oRank = $oDomNode->getElementsByTagName('rank')->item(0);
if ($oRank) {
$iRank = (float)$oRank->textContent;
}
@@ -147,7 +164,39 @@ abstract class Dashboard
}
/**
* @param \DOMElement $oDomNode
* @param \DOMDocument $oDoc
*/
public function FromDOMDocumentV2(DesignDocument $oDoc)
{
$this->oDOMNode = $oDoc->getElementsByTagName('dashboard')->item(0);
$this->sLayoutClass = DashboardLayoutGrid::class;
$this->sTitle = $this->oDOMNode->GetChildText('title', '');
$iRefresh = intval($this->oDOMNode->GetChildText('refresh', '0'));
$this->bAutoReload = $iRefresh > 0;
$this->iAutoReloadSec = $iRefresh;
$oDashletsNode = $this->oDOMNode->GetUniqueElement('pos_dashlets');
$oDashletList = $oDashletsNode->getElementsByTagName('pos_dashlet');
foreach ($oDashletList as $oPosDashletNode) {
$aGridDashlet = [];
$aGridDashlet['position_x'] = intval($oPosDashletNode->GetChildText('position_x', '0'));
$aGridDashlet['position_y'] = intval($oPosDashletNode->GetChildText('position_y', '0'));
$aGridDashlet['width'] = intval($oPosDashletNode->GetChildText('width', '2'));
$aGridDashlet['height'] = intval($oPosDashletNode->GetChildText('height', '1'));
$oDashletNode = $oPosDashletNode->GetUniqueElement('dashlet');
$sId = $oPosDashletNode->getAttribute('id');
$oDashlet = $this->InitDashletFromDOMNode($oDashletNode);
$oDashlet->SetID($sId);
$aGridDashlet['dashlet'] = $oDashlet;
$this->aGridDashlets[] = $aGridDashlet;
}
}
/**
* @param DesignElement $oDomNode
*
* @return mixed
*/
@@ -160,7 +209,7 @@ abstract class Dashboard
// Test if dashlet can be instantiated, otherwise (uninstalled, broken, ...) we display a placeholder
$sClass = static::GetDashletClassFromType($sDashletType);
/** @var \Dashlet $oNewDashlet */
$oNewDashlet = new $sClass($this->oMetaModel, $sId);
$oNewDashlet = $this->oDashletFactory->CreateDashlet($sClass, $sId);
$oNewDashlet->SetDashletType($sDashletType);
$oNewDashlet->FromDOMNode($oDomNode);
@@ -204,20 +253,33 @@ abstract class Dashboard
*/
public function ToXml()
{
$oDoc = new DOMDocument();
$oDoc->formatOutput = true; // indent (must be loaded with option LIBXML_NOBLANKS)
$oDoc->preserveWhiteSpace = true; // otherwise the formatOutput option would have no effect
$oMainNode = $oDoc->createElement('dashboard');
$oMainNode->setAttribute('xmlns:xsi', "http://www.w3.org/2001/XMLSchema-instance");
$oDoc->appendChild($oMainNode);
$oMainNode = $this->CreateEmptyDashboard();
$this->ToDOMNode($oMainNode);
$sXml = $oDoc->saveXML();
$sXml = $oMainNode->ownerDocument->saveXML();
return $sXml;
}
/**
* @return DesignElement
* @throws \DOMException
*/
public function CreateEmptyDashboard(): DesignElement
{
$oDoc = new DesignDocument();
$oDoc->formatOutput = true; // indent (must be loaded with option LIBXML_NOBLANKS)
$oDoc->preserveWhiteSpace = true; // otherwise the formatOutput option would have no effect
/** @var DesignElement $oMainNode */
$oMainNode = $oDoc->createElement('dashboard');
$oMainNode->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
$oDoc->appendChild($oMainNode);
return $oMainNode;
}
/**
* @param \DOMElement $oDefinition
*/
@@ -279,7 +341,7 @@ abstract class Dashboard
}
$this->sTitle = $aParams['title'];
$this->bAutoReload = $aParams['auto_reload'] == 'true';
$this->iAutoReloadSec = max(MetaModel::GetConfig()->Get('min_reload_interval'), (int) $aParams['auto_reload_sec']);
$this->iAutoReloadSec = max(MetaModel::GetConfig()->Get('min_reload_interval'), (int)$aParams['auto_reload_sec']);
foreach ($aParams['cells'] as $aCell) {
$aCellDashlets = [];
@@ -287,7 +349,7 @@ abstract class Dashboard
$sDashletClass = $aDashletParams['dashlet_class'];
$sId = $aDashletParams['dashlet_id'];
/** @var \Dashlet $oNewDashlet */
$oNewDashlet = new $sDashletClass($this->oMetaModel, $sId);
$oNewDashlet = $this->oDashletFactory->CreateDashlet($sDashletClass, $sId);
if (isset($aDashletParams['dashlet_type'])) {
$oNewDashlet->SetDashletType($aDashletParams['dashlet_type']);
}
@@ -305,7 +367,11 @@ abstract class Dashboard
public function Save()
{
}
public function PersistDashboard(string $sXml): bool
{
return true;
}
/**
@@ -503,7 +569,7 @@ EOF
{
$aExtraParams['dashboard_div_id'] = utils::Sanitize($aExtraParams['dashboard_div_id'] ?? null, $this->GetId(), utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
/** @var \DashboardLayoutMultiCol $oLayout */
/** @var \DashboardLayout $oLayout */
$oLayout = new $this->sLayoutClass();
foreach ($this->aCells as $iCellIdx => $aDashlets) {
@@ -513,7 +579,13 @@ EOF
}
}
$oDashboard = $oLayout->Render($oPage, $this->aCells, $bEditMode, $aExtraParams);
if (count($this->aCells) > 0) {
$aDashlets = $this->aCells;
} else {
$aDashlets = $this->aGridDashlets;
}
$oDashboard = $oLayout->Render($oPage, $aDashlets, $bEditMode, $aExtraParams);
$oPage->AddUiBlock($oDashboard);
$bFromDasboardPage = isset($aExtraParams['from_dashboard_page']) ? isset($aExtraParams['from_dashboard_page']) : false;
@@ -603,29 +675,12 @@ JS
* Return an array of dashlets available for selection.
*
* @return array
* @throws \ReflectionException
* @throws \Combodo\iTop\Application\Dashlet\DashletException
* @throws \DOMFormatException
*/
protected function GetAvailableDashlets()
protected function GetAvailableDashlets(): array
{
$aDashlets = [];
foreach (get_declared_classes() as $sDashletClass) {
// DashletUnknown is not among the selection as it is just a fallback for dashlets that can't instantiated.
if (is_subclass_of($sDashletClass, 'Dashlet') && !in_array($sDashletClass, ['DashletUnknown', 'DashletProxy'])) {
$oReflection = new ReflectionClass($sDashletClass);
if (!$oReflection->isAbstract()) {
$aCallSpec = [$sDashletClass, 'IsVisible'];
$bVisible = call_user_func($aCallSpec);
if ($bVisible) {
$aCallSpec = [$sDashletClass, 'GetInfo'];
$aInfo = call_user_func($aCallSpec);
$aDashlets[$sDashletClass] = $aInfo;
}
}
}
}
return $aDashlets;
return DashletService::GetInstance()->GetAvailableDashlets();
}
/**
@@ -640,6 +695,7 @@ JS
$iNewId = max($iNewId, (int)$oDashlet->GetID());
}
}
return $iNewId + 1;
}
@@ -674,6 +730,7 @@ JS
if (is_subclass_of($sType, 'Dashlet')) {
return $sType;
}
return 'DashletUnknown';
}
@@ -726,6 +783,8 @@ class RuntimeDashboard extends Dashboard
{
parent::__construct($sId);
$this->oMetaModel = new ModelReflectionRuntime();
$this->oDashletFactory->SetModelReflectionRuntime($this->oMetaModel);
ServiceLocator::GetInstance()->RegisterService('ModelReflection', $this->oMetaModel);
$this->bCustomized = false;
}
@@ -740,6 +799,7 @@ class RuntimeDashboard extends Dashboard
/**
* @param bool $bCustomized
*
* @since 2.7.0
*/
public function SetCustomFlag($bCustomized)
@@ -764,6 +824,26 @@ class RuntimeDashboard extends Dashboard
public function Save()
{
$sXml = $this->ToXml();
return $this->PersistDashboard($sXml);
}
/**
* @param string $sXml
*
* @return bool
* @throws \ArchivedObjectException
* @throws \CoreCannotSaveObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \CoreWarning
* @throws \MissingQueryArgument
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
* @throws \OQLException
*/
public function PersistDashboard(string $sXml): bool
{
$oUDSearch = new DBObjectSearch('UserDashboard');
$oUDSearch->AddCondition('user_id', UserRights::GetUserId(), '=');
$oUDSearch->AddCondition('menu_code', $this->sId, '=');
@@ -784,6 +864,7 @@ class RuntimeDashboard extends Dashboard
utils::PushArchiveMode(false);
$oUserDashboard->DBWrite();
utils::PopArchiveMode();
return $bIsNew;
}
@@ -1077,6 +1158,7 @@ JS
$sId = utils::Sanitize($this->GetId(), '', 'element_identifier');
$sMenuTogglerId = "ibo-dashboard-menu-toggler-{$sId}";
$sActionEditId = "ibo-dashboard-menu-edit-{$sId}";
$sPopoverMenuId = "ibo-dashboard-menu-popover-{$sId}";
$sName = 'UI:Dashboard:Actions';
@@ -1090,6 +1172,21 @@ JS
} else {
$oToolbar = $oDashboard->GetToolbar();
}
// TODO 3.3 Check if we need different action for custom dashboard creation / edition
$oActionEditButton = ButtonUIBlockFactory::MakeIconAction(
'fas fa-pen',
$this->HasCustomDashboard() ? Dict::S('UI:Dashboard:EditCustom') : Dict::S('UI:Dashboard:CreateCustom'),
$sActionEditId,
'',
false,
$sActionEditId
)
->AddCSSClass('ibo-top-bar--toolbar-dashboard-edit-button')
->AddCSSClass('ibo-action-button');
$oToolbar->AddSubBlock($oActionEditButton);
$oActionButton = ButtonUIBlockFactory::MakeIconAction('fas fa-ellipsis-v', Dict::S($sName), $sName, '', false, $sMenuTogglerId)
->AddCSSClass('ibo-top-bar--toolbar-dashboard-menu-toggler')
->AddCSSClass('ibo-action-button');
@@ -1099,8 +1196,8 @@ JS
$sFile = addslashes(utils::LocalPath($this->sDefinitionFile));
$sJSExtraParams = json_encode($aExtraParams);
if ($this->HasCustomDashboard()) {
$oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:EditCustom'), "return EditDashboard('{$this->sId}', '$sFile', $sJSExtraParams)");
$aActions[$oEdit->GetUID()] = $oEdit->GetMenuItem();
// $oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:EditCustom'), "return EditDashboard('{$this->sId}', '$sFile', $sJSExtraParams)");
// $aActions[$oEdit->GetUID()] = $oEdit->GetMenuItem();
$oRevert = new JSPopupMenuItem(
'UI:Dashboard:RevertConfirm',
Dict::S('UI:Dashboard:DeleteCustom'),
@@ -1108,8 +1205,8 @@ JS
);
$aActions[$oRevert->GetUID()] = $oRevert->GetMenuItem();
} else {
$oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:CreateCustom'), "return EditDashboard('{$this->sId}', '$sFile', $sJSExtraParams)");
$aActions[$oEdit->GetUID()] = $oEdit->GetMenuItem();
// $oEdit = new JSPopupMenuItem('UI:Dashboard:Edit', Dict::S('UI:Dashboard:CreateCustom'), "return EditDashboard('{$this->sId}', '$sFile', $sJSExtraParams)");
// $aActions[$oEdit->GetUID()] = $oEdit->GetMenuItem();
}
utils::GetPopupMenuItems($oPage, iPopupMenuExtension::MENU_DASHBOARD_ACTIONS, $this, $aActions);
@@ -1223,7 +1320,7 @@ EOF
$sId = json_encode($this->sId);
$sLayoutClass = json_encode($this->sLayoutClass);
$sAutoReload = $this->bAutoReload ? 'true' : 'false';
$sAutoReloadSec = (string) $this->iAutoReloadSec;
$sAutoReloadSec = (string)$this->iAutoReloadSec;
$sTitle = json_encode($this->sTitle);
$sFile = json_encode($this->GetDefinitionFile());
$sUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
@@ -1395,19 +1492,11 @@ JS
// Get the list of possible dashlets that support a creation from
// an OQL
$aAllDashlets = DashletService::GetInstance()->GetAvailableDashlets();
$aDashlets = [];
foreach (get_declared_classes() as $sDashletClass) {
if (is_subclass_of($sDashletClass, 'Dashlet')) {
$oReflection = new ReflectionClass($sDashletClass);
if (!$oReflection->isAbstract()) {
$aCallSpec = [$sDashletClass, 'CanCreateFromOQL'];
$bShorcutMode = call_user_func($aCallSpec);
if ($bShorcutMode) {
$aCallSpec = [$sDashletClass, 'GetInfo'];
$aInfo = call_user_func($aCallSpec);
$aDashlets[$sDashletClass] = ['label' => $aInfo['label'], 'class' => $sDashletClass, 'icon' => $aInfo['icon']];
}
}
foreach ($aAllDashlets as $sDashletClass => $aInfo) {
if ($aInfo['can_create_by_oql']) {
$aDashlets[$sDashletClass] = ['label' => $aInfo['label'], 'class' => $sDashletClass, 'icon' => $aInfo['icon']];
}
}
@@ -1417,7 +1506,7 @@ JS
$oSubForm = new DesignerForm();
$oMetaModel = new ModelReflectionRuntime();
/** @var \Dashlet $oDashlet */
$oDashlet = new $sDashletClass($oMetaModel, 0);
$oDashlet = DashletFactory::GetInstance()->CreateDashlet($sDashletClass, 0);
$oDashlet->GetPropertiesFieldsFromOQL($oSubForm, $sOQL);
$oSelectorField->AddSubForm($oSubForm, $aDashletInfo['label'], $aDashletInfo['class']);
@@ -1571,7 +1660,7 @@ JS
*/
private function UpdateDashletUserPrefs(Dashlet $oDashlet, $sDashletIdOrig, array $aExtraParams)
{
$bIsDashletWithListPref = ($oDashlet instanceof DashletObjectList);
$bIsDashletWithListPref = ($oDashlet instanceof DashletObjectList);
if (!$bIsDashletWithListPref) {
return;
}
@@ -1619,6 +1708,7 @@ JS
//on error, return default value
return null;
}
return DataTableSettings::GetAppUserPreferenceKey($aClassAliases, $sDataTableId);
}
}

View File

@@ -30,7 +30,7 @@ use Combodo\iTop\Application\WebPage\WebPage;
*/
abstract class DashboardLayout
{
abstract public function Render($oPage, $aDashlets, $bEditMode = false);
abstract public function Render($oPage, $aDashlets, $bEditMode = false, array $aExtraParams = []);
/**
* @param int $iCellIdx
@@ -43,8 +43,8 @@ abstract class DashboardLayout
public static function GetInfo()
{
return [
'label' => '',
'icon' => '',
'label' => '',
'icon' => '',
'description' => '',
];
}
@@ -74,6 +74,7 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
}
$idx++;
}
return $aDashlets;
}
@@ -94,6 +95,7 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
}
$idx++;
}
return $aCells;
}
@@ -109,7 +111,8 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
// Trim the list of cells to remove the invisible/empty ones at the end of the array
$aCells = $this->TrimCellsArray($aCells);
$oDashboardLayout = new DashboardLayoutUIBlock();
// TODO 3.3 Handle dashboard new format, convert old format if needed
$oDashboardLayout = new DashboardLayoutUIBlock($aExtraParams['dashboard_div_id']);
//$oPage->AddUiBlock($oDashboardLayout);
$iCellIdx = 0;
@@ -117,15 +120,16 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
//Js given by each dashlet to reload
$sJSReload = "";
$oDashboardGrid = new \Combodo\iTop\Application\UI\Base\Layout\Dashboard\DashboardGrid();
$oDashboardLayout->SetGrid($oDashboardGrid);
for ($iRows = 0; $iRows < $iNbRows; $iRows++) {
$oDashboardRow = new DashboardRow();
$oDashboardLayout->AddDashboardRow($oDashboardRow);
//$oDashboardLayout->AddDashboardRow($oDashboardRow);
for ($iCols = 0; $iCols < $this->iNbCols; $iCols++) {
$oDashboardColumn = new DashboardColumn($bEditMode);
$oDashboardColumn->SetCellIndex($iCellIdx);
$oDashboardRow->AddDashboardColumn($oDashboardColumn);
//$oDashboardRow->AddDashboardColumn($oDashboardColumn);
if (array_key_exists($iCellIdx, $aCells)) {
$aDashlets = $aCells[$iCellIdx];
@@ -133,6 +137,18 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
/** @var \Dashlet $oDashlet */
foreach ($aDashlets as $oDashlet) {
if ($oDashlet::IsVisible()) {
$sDashletId = $oDashlet->GetID();
$sDashletClass = get_class($oDashlet);
$aDashletDenormalizedProperties = $oDashlet->GetDenormalizedProperties();
// $aDashletsInfo = $sDashletClass::GetInfo();
//
// // TODO 3.3 Gather real position and height/width if any.
// // Also set minimal height/width
// $iPositionX = null;
// $iPositionY = null;
// $iWidth = array_key_exists('preferred_width', $aDashletsInfo) ? $aDashletsInfo['preferred_width'] : 1;
// $iHeight = array_key_exists('preferred_height', $aDashletsInfo) ? $aDashletsInfo['preferred_height'] : 1;
// $oDashboardGrid->AddDashlet($oDashlet->DoRender($oPage, $bEditMode, true /* bEnclosingDiv */, $aExtraParams), $sDashletId, $sDashletClass, $aDashletDenormalizedProperties, $iPositionX, $iPositionY, $iWidth, $iHeight);
$oDashboardColumn->AddUIBlock($oDashlet->DoRender($oPage, $bEditMode, true /* bEnclosingDiv */, $aExtraParams));
}
}
@@ -147,6 +163,7 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
$sJSReload .= $oDashboardRow->GetJSRefreshCallback()." ";
}
// TODO 3.3 We can probably do better with the new dashboard
$oPage->add_script("function updateDashboard".$aExtraParams['dashboard_div_id']."(){".$sJSReload."}");
if ($bEditMode) { // Add one row for extensibility
@@ -168,8 +185,8 @@ abstract class DashboardLayoutMultiCol extends DashboardLayout
*/
public function GetDashletCoordinates($iCellIdx)
{
$iColNumber = (int) $iCellIdx % $this->iNbCols;
$iRowNumber = (int) floor($iCellIdx / $this->iNbCols);
$iColNumber = (int)$iCellIdx % $this->iNbCols;
$iRowNumber = (int)floor($iCellIdx / $this->iNbCols);
return [$iColNumber, $iRowNumber];
}
@@ -182,11 +199,12 @@ class DashboardLayoutOneCol extends DashboardLayoutMultiCol
parent::__construct();
$this->iNbCols = 1;
}
public static function GetInfo()
{
return [
'label' => 'One Column',
'icon' => 'images/layout_1col.png',
'label' => 'One Column',
'icon' => 'images/layout_1col.png',
'description' => '',
];
}
@@ -199,11 +217,12 @@ class DashboardLayoutTwoCols extends DashboardLayoutMultiCol
parent::__construct();
$this->iNbCols = 2;
}
public static function GetInfo()
{
return [
'label' => 'Two Columns',
'icon' => 'images/layout_2col.png',
'label' => 'Two Columns',
'icon' => 'images/layout_2col.png',
'description' => '',
];
}
@@ -216,11 +235,12 @@ class DashboardLayoutThreeCols extends DashboardLayoutMultiCol
parent::__construct();
$this->iNbCols = 3;
}
public static function GetInfo()
{
return [
'label' => 'Two Columns',
'icon' => 'images/layout_3col.png',
'label' => 'Two Columns',
'icon' => 'images/layout_3col.png',
'description' => '',
];
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
<class id="AbstractResource" _delta="define">
<parent>cmdbAbstractObject</parent>
<properties>
<comment>/* Resource access control abstraction. Can be herited by abstract resource access control classes. Generally controlled using UR_ACTION_MODIFY access right. */</comment>
<comment>/* Resource access control abstraction. Can be inherited by abstract resource access control classes. Generally controlled using UR_ACTION_MODIFY access right. */</comment>
<abstract>true</abstract>
</properties>
<presentation/>
@@ -851,9 +851,295 @@ Call $this->AddInitialAttributeFlags($sAttCode, $iFlags) for all the initial att
</methods>
</class>
</classes>
<dashlets>
<dashlet id="DashletGroupByTable" _delta="define">
<label>UI:DashletGroupByTable:Label</label>
<icon>images/dashlets/icons8-transaction-list-48.png</icon>
<description>UI:DashletGroupByTable:Description</description>
<min_width>2</min_width>
<min_height>2</min_height>
<preferred_width>3</preferred_width>
<preferred_height>3</preferred_height>
<can_create_by_oql>true</can_create_by_oql>
<configuration/>
</dashlet>
<dashlet id="DashletGroupByBars" _delta="define">
<label>UI:DashletGroupByBars:Label</label>
<icon>images/dashlets/icons8-bar-chart-48.png</icon>
<description>UI:DashletGroupByBars:Description</description>
<min_width>2</min_width>
<min_height>2</min_height>
<preferred_width>3</preferred_width>
<preferred_height>3</preferred_height>
<can_create_by_oql>true</can_create_by_oql>
<configuration/>
</dashlet>
<dashlet id="DashletGroupByPie" _delta="define">
<label>UI:DashletGroupByPie:Label</label>
<icon>images/dashlets/icons8-pie-chart-48.png</icon>
<description>UI:DashletGroupByPie:Description</description>
<min_width>2</min_width>
<min_height>2</min_height>
<preferred_width>3</preferred_width>
<preferred_height>3</preferred_height>
<can_create_by_oql>true</can_create_by_oql>
<configuration/>
</dashlet>
<dashlet id="DashletBadge" _delta="define">
<label>UI:DashletBadge:Label</label>
<icon>images/dashlets/icons8-badge-48.png</icon>
<description>UI:DashletBadge:Description</description>
<min_width>2</min_width>
<min_height>1</min_height>
<preferred_width>2</preferred_width>
<preferred_height>1</preferred_height>
<configuration/>
</dashlet>
<dashlet id="DashletHeaderDynamic" _delta="define">
<label>UI:DashletHeaderDynamic:Label</label>
<icon>images/dashlets/icons8-header-altered-48.png</icon>
<description>UI:DashletHeaderDynamic:Description</description>
<min_width>2</min_width>
<min_height>1</min_height>
<preferred_width>4</preferred_width>
<preferred_height>3</preferred_height>
<configuration/>
</dashlet>
<dashlet id="DashletHeaderStatic" _delta="define">
<label>UI:DashletHeaderStatic:Label</label>
<icon>images/dashlets/icons8-header-48.png</icon>
<description>UI:DashletHeaderStatic:Description</description>
<min_width>4</min_width>
<min_height>1</min_height>
<preferred_width>4</preferred_width>
<preferred_height>1</preferred_height>
<configuration/>
</dashlet>
<dashlet id="DashletObjectList" _delta="define">
<label>UI:DashletObjectList:Label</label>
<icon>images/dashlets/icons8-list-48.png</icon>
<description>UI:DashletObjectList:Description</description>
<min_width>2</min_width>
<min_height>1</min_height>
<preferred_width>4</preferred_width>
<preferred_height>3</preferred_height>
<can_create_by_oql>true</can_create_by_oql>
<configuration/>
</dashlet>
<dashlet id="DashletPlainText" _delta="define">
<label>UI:DashletPlainText:Label</label>
<icon>images/dashlets/icons8-text-box-48.png</icon>
<description>UI:DashletPlainText:Description</description>
<min_width>2</min_width>
<min_height>1</min_height>
<preferred_width>2</preferred_width>
<preferred_height>1</preferred_height>
<configuration/>
</dashlet>
</dashlets>
<property_types _delta="define">
<property_type id="Dashboard" xsi:type="Combodo-AbstractPropertyType"/>
<property_type id="DashboardGrid" xsi:type="Combodo-PropertyType">
<extends>Dashboard</extends>
<definition xsi:type="Combodo-ValueType-PropertyTree">
<label>Dashboard</label>
<nodes>
<node id="title" xsi:type="Combodo-ValueType-String">
<label>UI:DashboardEdit:DashboardTitle</label>
</node>
<node id="refresh" xsi:type="Combodo-ValueType-Choice"> <!-- Possible de le cacher, etc celui-ci nous met dedans -->
<label>UI:DashboardEdit:AutoReload</label>
<values>
<value id="0">
<label>No auto-refresh</label>
</value>
<value id="30">
<label>Every 30 seconds</label>
</value>
<value id="60">
<label>Every 1 minute</label>
</value>
<value id="300">
<label>Every 5 minutes</label>
</value>
<value id="600">
<label>Every 10 minutes</label>
</value>
<value id="1800">
<label>Every 30 minutes</label>
</value>
<value id="3600">
<label>Every 1 hour</label>
</value>
</values>
</node>
<node id="pos_dashlets" xsi:type="Combodo-ValueType-Collection">
<label>Dashlet List</label>
<xml-format xsi:type="Combodo-XMLFormat-CollectionWithId">
<tag-name>pos_dashlet</tag-name>
</xml-format>
<prototype>
<node id="position_x" xsi:type="Combodo-ValueType-Integer">
<label>X</label>
</node>
<node id="position_y" xsi:type="Combodo-ValueType-Integer">
<label>Y</label>
</node>
<node id="width" xsi:type="Combodo-ValueType-Integer">
<label>W</label>
</node>
<node id="height" xsi:type="Combodo-ValueType-Integer">
<label>H</label>
</node>
<node id="dashlet" xsi:type="Combodo-ValueType-Polymorphic">
<label>Dashlet</label>
<allowed-types>
<allowed-type>Dashlet</allowed-type>
</allowed-types>
</node>
</prototype>
</node>
</nodes>
</definition>
</property_type>
<property_type id="Dashlet" xsi:type="Combodo-AbstractPropertyType"/>
<property_type id="DashletGroupBy" xsi:type="Combodo-PropertyType">
<property_type id="DashletGroupByTable" xsi:type="Combodo-PropertyType">
<extends>Dashlet</extends>
<definition xsi:type="Combodo-ValueType-PropertyTree">
<label>UI:DashletGroupBy:Title</label>
<nodes>
<node id="title" xsi:type="Combodo-ValueType-Label">
<label>UI:DashletGroupBy:Prop-Title</label>
</node>
<node id="query" xsi:type="Combodo-ValueType-OQL">
<label>UI:DashletGroupBy:Prop-Query</label>
</node>
<node id="group_by" xsi:type="Combodo-ValueType-ClassAttributeGroupBy">
<label>UI:DashletGroupBy:Prop-GroupBy</label>
<class>{{query.selected_class}}</class>
</node>
<node id="style" xsi:type="Combodo-ValueType-Choice"> <!-- Possible de le cacher, etc celui-ci nous met dedans -->
<label>UI:DashletGroupBy:Prop-Style</label>
<values>
<value id="bars">
<label>UI:DashletGroupByBars:Label</label>
</value>
<value id="pie">
<label>UI:DashletGroupByPie:Label</label>
</value>
<value id="table">
<label>UI:DashletGroupByTable:Label</label>
</value>
</values>
</node>
<node id="aggregation_function" xsi:type="Combodo-ValueType-AggregateFunction">
<label>UI:DashletGroupBy:Prop-Function</label>
<class>{{query.selected_class}}</class> <!-- pour savoir si il y a des attributs additionnables -->
</node>
<node id="aggregation_attribute" xsi:type="Combodo-ValueType-ClassAttribute">
<label>UI:DashletGroupBy:Prop-FunctionAttribute</label>
<relevance-condition>{{aggregation_function.value != 'count'}}</relevance-condition>
<class>{{query.selected_class}}</class>
<category>numeric</category>
</node>
<node id="order_by" xsi:type="Combodo-ValueType-ChoiceFromInput">
<label>UI:DashletGroupBy:Prop-OrderField</label>
<values>
<value id="attribute">
<label>{{aggregation_attribute.label}}</label>
</value>
<value id="function">
<label>{{aggregation_function.label}}</label>
</value>
</values>
</node>
<node id="limit" xsi:type="Combodo-ValueType-Integer">
<label>UI:DashletGroupBy:Prop-Limit</label>
<relevance-condition>{{order_by.value = 'function'}}</relevance-condition>
</node>
<node id="order_direction" xsi:type="Combodo-ValueType-Choice">
<label>UI:DashletGroupBy:Prop-OrderDirection</label>
<values>
<value id="asc">
<label>UI:DashletGroupBy:Order:asc</label>
</value>
<value id="desc">
<label>UI:DashletGroupBy:Order:desc</label>
</value>
</values>
</node>
</nodes>
</definition>
</property_type>
<property_type id="DashletGroupByBars" xsi:type="Combodo-PropertyType">
<extends>Dashlet</extends>
<definition xsi:type="Combodo-ValueType-PropertyTree">
<label>UI:DashletGroupBy:Title</label>
<nodes>
<node id="title" xsi:type="Combodo-ValueType-Label">
<label>UI:DashletGroupBy:Prop-Title</label>
</node>
<node id="query" xsi:type="Combodo-ValueType-OQL">
<label>UI:DashletGroupBy:Prop-Query</label>
</node>
<node id="group_by" xsi:type="Combodo-ValueType-ClassAttributeGroupBy">
<label>UI:DashletGroupBy:Prop-GroupBy</label>
<class>{{query.selected_class}}</class>
</node>
<node id="style" xsi:type="Combodo-ValueType-Choice"> <!-- Possible de le cacher, etc celui-ci nous met dedans -->
<label>UI:DashletGroupBy:Prop-Style</label>
<values>
<value id="bars">
<label>UI:DashletGroupByBars:Label</label>
</value>
<value id="pie">
<label>UI:DashletGroupByPie:Label</label>
</value>
<value id="table">
<label>UI:DashletGroupByTable:Label</label>
</value>
</values>
</node>
<node id="aggregation_function" xsi:type="Combodo-ValueType-AggregateFunction">
<label>UI:DashletGroupBy:Prop-Function</label>
<class>{{query.selected_class}}</class> <!-- pour savoir si il y a des attributs additionnables -->
</node>
<node id="aggregation_attribute" xsi:type="Combodo-ValueType-ClassAttribute">
<label>UI:DashletGroupBy:Prop-FunctionAttribute</label>
<relevance-condition>{{aggregation_function.value != 'count'}}</relevance-condition>
<class>{{query.selected_class}}</class>
<category>numeric</category>
</node>
<node id="order_by" xsi:type="Combodo-ValueType-ChoiceFromInput">
<label>UI:DashletGroupBy:Prop-OrderField</label>
<values>
<value id="attribute">
<label>{{aggregation_attribute.label}}</label>
</value>
<value id="function">
<label>{{aggregation_function.label}}</label>
</value>
</values>
</node>
<node id="limit" xsi:type="Combodo-ValueType-Integer">
<label>UI:DashletGroupBy:Prop-Limit</label>
<relevance-condition>{{order_by.value = 'function'}}</relevance-condition>
</node>
<node id="order_direction" xsi:type="Combodo-ValueType-Choice">
<label>UI:DashletGroupBy:Prop-OrderDirection</label>
<values>
<value id="asc">
<label>UI:DashletGroupBy:Order:asc</label>
</value>
<value id="desc">
<label>UI:DashletGroupBy:Order:desc</label>
</value>
</values>
</node>
</nodes>
</definition>
</property_type>
<property_type id="DashletGroupByPie" xsi:type="Combodo-PropertyType">
<extends>Dashlet</extends>
<definition xsi:type="Combodo-ValueType-PropertyTree">
<label>UI:DashletGroupBy:Title</label>

View File

@@ -6,6 +6,7 @@
*/
use Combodo\iTop\Application\Helper\WebResourcesHelper;
use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory;
use Combodo\iTop\Application\WebPage\ErrorPage;
use Combodo\iTop\Application\WebPage\iTopWebPage;
use Combodo\iTop\Application\WebPage\WebPage;
@@ -1279,13 +1280,14 @@ class DashboardMenuNode extends MenuNode
if ($oDashboard != null) {
WebResourcesHelper::EnableC3JSToWebPage($oPage);
// TODO 3.3 this works for dashboard menu, what about other places ?
$oPageLayout = PageContentFactory::MakeForDashboard();
$oPage->SetContentLayout($oPageLayout, $oPage);
$sDivId = utils::Sanitize($this->sMenuId, '', 'element_identifier');
$oPage->add('<div id="'.$sDivId.'" class="ibo-dashboard" data-role="ibo-dashboard">');
$aExtraParams['dashboard_div_id'] = $sDivId;
$aExtraParams['from_dashboard_page'] = true;
$oDashboard->SetReloadURL($this->GetHyperlink($aExtraParams));
$oDashboard->Render($oPage, false, $aExtraParams);
$oPage->add('</div>');
$bEdit = utils::ReadParam('edit', false);
if ($bEdit) {

View File

@@ -1954,6 +1954,11 @@ class Config
*/
protected $m_sDefaultLanguage;
/**
* @var string Type of login process allowed: form|basic|url|external
*/
protected $m_sAllowedLoginTypes;
/**
* @var string Name of the PHP variable in which external authentication information is passed by the web server
*/
@@ -2027,6 +2032,7 @@ class Config
$this->m_iFastReloadInterval = DEFAULT_FAST_RELOAD_INTERVAL;
$this->m_bSecureConnectionRequired = DEFAULT_SECURE_CONNECTION_REQUIRED;
$this->m_sDefaultLanguage = 'EN US';
$this->m_sAllowedLoginTypes = DEFAULT_ALLOWED_LOGIN_TYPES;
$this->m_sExtAuthVariable = DEFAULT_EXT_AUTH_VARIABLE;
$this->m_aCharsets = [];
$this->m_bQueryCacheEnabled = DEFAULT_QUERY_CACHE_ENABLED;
@@ -2173,6 +2179,7 @@ class Config
$this->m_aModuleSettings = isset($MyModuleSettings) ? $MyModuleSettings : [];
$this->m_sDefaultLanguage = isset($MySettings['default_language']) ? trim($MySettings['default_language']) : 'EN US';
$this->m_sAllowedLoginTypes = isset($MySettings['allowed_login_types']) ? trim($MySettings['allowed_login_types']) : DEFAULT_ALLOWED_LOGIN_TYPES;
$this->m_sExtAuthVariable = isset($MySettings['ext_auth_variable']) ? trim($MySettings['ext_auth_variable']) : DEFAULT_EXT_AUTH_VARIABLE;
$this->m_sEncryptionKey = isset($MySettings['encryption_key']) ? trim($MySettings['encryption_key']) : $this->m_sEncryptionKey;
$this->m_sEncryptionLibrary = isset($MySettings['encryption_library']) ? trim($MySettings['encryption_library']) : $this->m_sEncryptionLibrary;
@@ -2332,7 +2339,7 @@ class Config
public function GetAllowedLoginTypes()
{
return explode('|', $this->m_aSettings['allowed_login_types']['value']);
return explode('|', $this->m_sAllowedLoginTypes);
}
public function GetExternalAuthenticationVariable()
@@ -2410,6 +2417,7 @@ class Config
public function SetAllowedLoginTypes($aAllowedLoginTypes)
{
$this->m_sAllowedLoginTypes = implode('|', $aAllowedLoginTypes);
$this->Set('allowed_login_types', implode('|', $aAllowedLoginTypes));
}
@@ -2487,6 +2495,7 @@ class Config
$aSettings['fast_reload_interval'] = $this->m_iFastReloadInterval;
$aSettings['secure_connection_required'] = $this->m_bSecureConnectionRequired;
$aSettings['default_language'] = $this->m_sDefaultLanguage;
$aSettings['allowed_login_types'] = $this->m_sAllowedLoginTypes;
$aSettings['ext_auth_variable'] = $this->m_sExtAuthVariable;
$aSettings['encryption_key'] = $this->m_sEncryptionKey;
$aSettings['encryption_library'] = $this->m_sEncryptionLibrary;
@@ -2590,6 +2599,7 @@ class Config
// Old fashioned remaining values
$aOtherValues = [
'default_language' => $this->m_sDefaultLanguage,
'allowed_login_types' => $this->m_sAllowedLoginTypes,
'ext_auth_variable' => $this->m_sExtAuthVariable,
'encryption_key' => $this->m_sEncryptionKey,
'encryption_library' => $this->m_sEncryptionLibrary,

View File

@@ -389,7 +389,7 @@ JS
* Resize an image so that it fits the maximum width/height defined in the config file
* @param ormDocument $oImage The original image stored as an array (content / mimetype / filename)
* @return ormDocument The resampled image (or the original one if it already fit)
* @deprecated since 3.3.0 Replaced by ormDocument::ResizeImageToFit
* @deprecated Replaced by ormDocument::ResizeImageToFit
*/
public static function ResizeImageToFit(ormDocument $oImage, &$aDimensions = null)
{

View File

@@ -1,20 +1,18 @@
<?php
/**
* @copyright Copyright (C) 2010-2026 Combodo SAS
* @copyright Copyright (C) 2010-2024 Combodo SAS
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Application\EventRegister\ApplicationEvents;
use Combodo\iTop\Core\Kpi\KpiLogData;
use Combodo\iTop\Service\Events\EventService;
use Combodo\iTop\Service\Events\iEventServiceSetup;
use Combodo\iTop\Service\Module\ModuleService;
/**
* Measures operations duration, memory usage, etc. (and some other KPIs)
*/
class ExecutionKPI implements iEventServiceSetup
class ExecutionKPI
{
protected static $m_bEnabled_Duration = false;
protected static $m_bEnabled_Memory = false;
@@ -25,18 +23,15 @@ class ExecutionKPI implements iEventServiceSetup
protected static $m_aStats = []; // Recurrent operations
protected static $m_aExecData = []; // One shot operations
/** @var true */
private static bool $bMetamodelStarted = false;
/**
* @var array[ExecutionKPI]
*/
protected static $m_aExecutionStack = []; // embedded execution stats
private static ?float $fLastReportTime = null;
private static ?float $iLastReportMemory = null;
// For stats
protected $m_fStarted = null;
protected $m_fChildrenDuration = 0; // Count embedded
protected $m_iInitialMemory = null;
private static array $aBootstrapOperations = [];
public static function EnableDuration($iLevel)
{
if ($iLevel > 0) {
@@ -76,7 +71,6 @@ class ExecutionKPI implements iEventServiceSetup
return true;
}
}
return false;
}
@@ -103,7 +97,7 @@ class ExecutionKPI implements iEventServiceSetup
$sFor = self::$m_sAllowedUser == '*' ? 'EVERYBODY' : "'".trim(self::$m_sAllowedUser)."'";
$sSlowQueries = '';
if (self::$m_fSlowQueries > 0) {
$sSlowQueries = '. Slow Queries: '.self::$m_fSlowQueries.'s';
$sSlowQueries = ". Slow Queries: ".self::$m_fSlowQueries."s";
}
$aExtensions = [];
@@ -133,7 +127,7 @@ class ExecutionKPI implements iEventServiceSetup
$sRequest .= ' operation: '.$_POST['operation'];
}
$fStop = microtime(true);
$fStop = MyHelpers::getmicrotime();
if (($fStop - $fItopStarted) > self::$m_fSlowQueries) {
// Invoke extensions to log the KPI operation
/** @var \iKPILoggerExtension $oExtensionInstance */
@@ -157,17 +151,17 @@ class ExecutionKPI implements iEventServiceSetup
$sTableStyle = 'background-color: #ccc; margin: 10px;';
$sHtml = '<hr/>';
$sHtml = "<hr/>";
$sHtml .= "<div style=\"background-color: grey; padding: 10px;\">";
$sHtml .= "<h3><a name=\"".md5($sExecId)."\">KPIs</a> - $sRequest</h3>";
$oStarted = DateTime::createFromFormat('U.u', $fItopStarted);
$sHtml .= '<p>'.$oStarted->format('Y-m-d H:i:s.u').'</p>';
$sHtml .= '<p>log_kpi_user_id: '.UserRights::GetUserId().'</p>';
$sHtml .= '<div>';
$sHtml .= "<p>log_kpi_user_id: ".UserRights::GetUserId()."</p>";
$sHtml .= "<div>";
$sHtml .= "<table border=\"1\" style=\"$sTableStyle\">";
$sHtml .= '<thead>';
$sHtml .= ' <th>Operation</th><th>Begin</th><th>End</th><th>Duration</th><th>Memory start</th><th>Memory end</th><th>Memory peak</th>';
$sHtml .= '</thead>';
$sHtml .= "<thead>";
$sHtml .= " <th>Operation</th><th>Begin</th><th>End</th><th>Duration</th><th>Memory start</th><th>Memory end</th><th>Memory peak</th>";
$sHtml .= "</thead>";
foreach (self::$m_aExecData as $aOpStats) {
$sOperation = $aOpStats['op'];
$sBegin = round($aOpStats['time_begin'], 3);
@@ -186,12 +180,12 @@ class ExecutionKPI implements iEventServiceSetup
}
}
$sHtml .= '<tr>';
$sHtml .= "<tr>";
$sHtml .= " <td>$sOperation</td><td>$sBegin</td><td>$sEnd</td><td>$sDuration</td><td>$sMemBegin</td><td>$sMemEnd</td><td>$sMemPeak</td>";
$sHtml .= '</tr>';
$sHtml .= "</tr>";
}
$sHtml .= '</table>';
$sHtml .= '</div>';
$sHtml .= "</table>";
$sHtml .= "</div>";
$aConsolidatedStats = [];
foreach (self::$m_aStats as $sOperation => $aOpStats) {
@@ -214,20 +208,20 @@ class ExecutionKPI implements iEventServiceSetup
}
}
$aConsolidatedStats[$sOperation] = [
'count' => $iTotalOp,
'count' => $iTotalOp,
'duration' => $fTotalOp,
'min' => $fMinOp,
'max' => $fMaxOp,
'avg' => $fTotalOp / $iTotalOp,
'min' => $fMinOp,
'max' => $fMaxOp,
'avg' => $fTotalOp / $iTotalOp,
'max_args' => $sMaxOpArguments,
];
}
$sHtml .= '<div>';
$sHtml .= "<div>";
$sHtml .= "<table border=\"1\" style=\"$sTableStyle\">";
$sHtml .= '<thead>';
$sHtml .= ' <th>Operation</th><th>Count</th><th>Duration</th><th>Min</th><th>Max</th><th>Avg</th>';
$sHtml .= '</thead>';
$sHtml .= "<thead>";
$sHtml .= " <th>Operation</th><th>Count</th><th>Duration</th><th>Min</th><th>Max</th><th>Avg</th>";
$sHtml .= "</thead>";
foreach ($aConsolidatedStats as $sOperation => $aOpStats) {
$sOperation = '<a href="#'.md5($sExecId.$sOperation).'">'.$sOperation.'</a>';
$sCount = $aOpStats['count'];
@@ -236,14 +230,14 @@ class ExecutionKPI implements iEventServiceSetup
$sMax = '<a href="#'.md5($sExecId.$aOpStats['max_args']).'">'.round($aOpStats['max'], 3).'</a>';
$sAvg = round($aOpStats['avg'], 3);
$sHtml .= '<tr>';
$sHtml .= "<tr>";
$sHtml .= " <td>$sOperation</td><td>$sCount</td><td>$sDuration</td><td>$sMin</td><td>$sMax</td><td>$sAvg</td>";
$sHtml .= '</tr>';
$sHtml .= "</tr>";
}
$sHtml .= '</table>';
$sHtml .= '</div>';
$sHtml .= "</table>";
$sHtml .= "</div>";
$sHtml .= '</div>';
$sHtml .= "</div>";
$sHtml .= "<p><a href=\"#end-".md5($sExecId)."\">Next page stats</a></p>";
@@ -293,18 +287,18 @@ class ExecutionKPI implements iEventServiceSetup
$sOperationHtml = '<a name="'.md5($sExecId.$sOperation).'">'.$sOperation.'</a>';
$sHtml .= "<h4>$sOperationHtml</h4>";
$sHtml .= "<table border=\"1\" style=\"$sTableStyle\">";
$sHtml .= '<thead>';
$sHtml .= ' <th>Operation details (+ blame caller if log_kpi_duration = 2)</th><th>Count</th><th>Duration</th><th>Min</th><th>Max</th>';
$sHtml .= '</thead>';
$sHtml .= "<thead>";
$sHtml .= " <th>Operation details (+ blame caller if log_kpi_duration = 2)</th><th>Count</th><th>Duration</th><th>Min</th><th>Max</th>";
$sHtml .= "</thead>";
$bDisplayHeader = false;
}
$sHtml .= '<tr>';
$sHtml .= "<tr>";
$sHtml .= " <td>$sHtmlArguments</td><td>$iCountInter</td><td>$sTotalInter</td><td>$sMinInter</td><td>$sMaxInter</td>";
$sHtml .= '</tr>';
$sHtml .= "</tr>";
}
}
if (!$bDisplayHeader) {
$sHtml .= '</table>';
$sHtml .= "</table>";
$sHtml .= "<p><a href=\"#".md5($sExecId)."\">Back to page stats</a></p>";
}
self::Report($sHtml);
@@ -339,50 +333,39 @@ class ExecutionKPI implements iEventServiceSetup
$aNewEntry = null;
if (is_null(static::$fLastReportTime)) {
static::$fLastReportTime = $fItopStarted;
}
if (is_null(static::$iLastReportMemory)) {
global $iItopInitialMemory;
static::$iLastReportMemory = $iItopInitialMemory;
}
$fStarted = static::$fLastReportTime;
$fStopped = microtime(true);
$fStarted = $this->m_fStarted;
$fStopped = $this->m_fStarted;
if (self::$m_bEnabled_Duration) {
$fStopped = MyHelpers::getmicrotime();
$aNewEntry = [
'op' => $sOperationDesc,
'time_begin' => $fStarted - $fItopStarted,
'time_begin' => $this->m_fStarted - $fItopStarted,
'time_end' => $fStopped - $fItopStarted,
];
// Reset for the next operation (if the object is recycled)
$this->m_fStarted = $fStopped;
}
static::$fLastReportTime = $fStopped;
$iInitialMemory = static::$iLastReportMemory;
$iCurrentMemory = $iInitialMemory;
$iPeakMemory = $iInitialMemory;
$iInitialMemory = is_null($this->m_iInitialMemory) ? 0 : $this->m_iInitialMemory;
$iCurrentMemory = 0;
$iPeakMemory = 0;
if (self::$m_bEnabled_Memory) {
$iCurrentMemory = self::memory_get_usage();
if (is_null($aNewEntry)) {
$aNewEntry = ['op' => $sOperationDesc];
}
$aNewEntry['mem_begin'] = $iInitialMemory;
$aNewEntry['mem_begin'] = $this->m_iInitialMemory;
$aNewEntry['mem_end'] = $iCurrentMemory;
$iPeakMemory = self::memory_get_peak_usage();
$aNewEntry['mem_peak'] = $iPeakMemory;
// Reset for the next operation (if the object is recycled)
static::$iLastReportMemory = $iCurrentMemory;
$this->m_iInitialMemory = $iCurrentMemory;
}
if (self::$m_bEnabled_Duration || self::$m_bEnabled_Memory) {
$aCallstack = ['callstack' => $this->GetCallStack()];
if (static::$bMetamodelStarted) {
foreach (static::$aBootstrapOperations as $oLog) {
$this->LogOperation($oLog);
}
static::$aBootstrapOperations = [];
// Invoke extensions to log the KPI operation
// Invoke extensions to log the KPI operation
/** @var \iKPILoggerExtension $oExtensionInstance */
foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) {
$sExtension = ModuleService::GetInstance()->GetModuleNameFromCallStack(1);
$oKPILogData = new KpiLogData(
KpiLogData::TYPE_REPORT,
@@ -393,24 +376,9 @@ class ExecutionKPI implements iEventServiceSetup
$sExtension,
$iInitialMemory,
$iCurrentMemory,
$iPeakMemory,
$aCallstack
$iPeakMemory
);
$this->LogOperation($oKPILogData);
} else {
$oKPILogData = new KpiLogData(
KpiLogData::TYPE_REPORT,
'Step',
$sOperationDesc,
$fStarted,
$fStopped,
'',
$iInitialMemory,
$iCurrentMemory,
$iPeakMemory,
$aCallstack
);
static::$aBootstrapOperations[] = $oKPILogData;
$oExtensionInstance->LogOperation($oKPILogData);
}
}
@@ -420,21 +388,13 @@ class ExecutionKPI implements iEventServiceSetup
$this->ResetCounters();
}
private function LogOperation(KpiLogData $oKPILogData): void
{
/** @var \iKPILoggerExtension $oExtensionInstance */
foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) {
$oExtensionInstance->LogOperation($oKPILogData);
}
}
/**
* Compute statistics for a call to an extension
* Note: not working in dev mode (with links to env-production)
*
* @param object|string $object object called
* @param string $sMethod method called on the object
* @param string $sMessage additional message
* @param string $sMethod method called on the object
* @param string $sMessage additional message
*
* @return bool true if an extension was found for this object::method
* @throws \ReflectionException
@@ -463,23 +423,21 @@ class ExecutionKPI implements iEventServiceSetup
$fDuration = 0;
if (self::$m_bEnabled_Duration) {
$fStopped = microtime(true);
$fStopped = MyHelpers::getmicrotime();
$fDuration = $fStopped - $this->m_fStarted;
$aCallstack = [];
if (self::$m_bGenerateLegacyReport) {
if (self::$m_bBlameCaller) {
$aCallstack = MyHelpers::get_callstack(1);
self::$m_aStats[$sOperation][$sArguments][] = [
'time' => $fDuration,
'callers' => $aCallstack,
'time' => $fDuration,
'callers' => $aCallstack,
];
} else {
self::$m_aStats[$sOperation][$sArguments][] = [
'time' => $fDuration,
'time' => $fDuration,
];
}
} else {
$aCallstack = ['callstack' => $this->GetCallStack()];
}
$iInitialMemory = is_null($this->m_iInitialMemory) ? 0 : $this->m_iInitialMemory;
@@ -490,45 +448,33 @@ class ExecutionKPI implements iEventServiceSetup
$iPeakMemory = self::memory_get_peak_usage();
}
if (static::$bMetamodelStarted) {
foreach (static::$aBootstrapOperations as $oLog) {
$this->LogOperation($oLog);
}
static::$aBootstrapOperations = [];
// Invoke extensions to log the KPI operation
/** @var \iKPILoggerExtension $oExtensionInstance */
foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) {
//$sExtension = ModuleService::GetInstance()->GetModuleNameFromCallStack(1);
$sExtension = '';
$oKPILogData = new KpiLogData(
KpiLogData::TYPE_STATS,
$sOperation,
$sArguments,
$this->m_fStarted,
$fStopped,
'',
$sExtension,
$iInitialMemory,
$iCurrentMemory,
$iPeakMemory,
$aCallstack
);
$this->LogOperation($oKPILogData);
} else {
$oKPILogData = new KpiLogData(
KpiLogData::TYPE_STATS,
$sOperation,
$sArguments,
$this->m_fStarted,
$fStopped,
'',
$iInitialMemory,
$iCurrentMemory,
$iPeakMemory,
$aCallstack
);
static::$aBootstrapOperations[] = $oKPILogData;
$oExtensionInstance->LogOperation($oKPILogData);
}
}
}
protected function ResetCounters()
{
$this->m_fStarted = microtime(true);
if (self::$m_bEnabled_Duration) {
$this->m_fStarted = microtime(true);
}
if (self::$m_bEnabled_Memory) {
$this->m_iInitialMemory = self::memory_get_usage();
@@ -557,33 +503,7 @@ class ExecutionKPI implements iEventServiceSetup
if (function_exists('memory_get_peak_usage')) {
return memory_get_peak_usage($bRealUsage);
}
// PHP > 5.2.1 - this verb depends on a compilation option
return 0;
}
/*
* ModuleHandlerApiInterface methods
*/
public static function OnMetaModelStarted()
{
static::$bMetamodelStarted = true;
}
public function RegisterEventsAndListeners()
{
EventService::RegisterListener(ApplicationEvents::APPLICATION_EVENT_METAMODEL_STARTED, [$this, 'OnMetaModelStarted']);
}
private function GetCallStack(): string
{
$aCallStack = MyHelpers::get_callstack(2);
$sCallStack = "Call stack:\n";
foreach ($aCallStack as $index => $aLine) {
$sCallStack .= "#$index ".$aLine['File'].'('.$aLine['Line'].'): '.$aLine['Function']."\n";
}
return $sCallStack;
}
}

View File

@@ -89,9 +89,17 @@ abstract class ModelReflection
* @param string $defaultValue
*
* @return \RunTimeIconSelectionField
* @deprecated since 3.3.0 replaced by GetAvailableIcons
*/
abstract public function GetIconSelectionField($sCode, $sLabel = '', $defaultValue = '');
/**
* Find available icons for the current context
*
* @return array of ['value', 'label', 'icon'] where 'value' is the relative path on disk, 'label' the name to display and 'icon' is the URL to get the image
*/
abstract public function GetAvailableIcons(): array;
abstract public function GetRootClass($sClass);
abstract public function EnumChildClasses($sClass, $iOption = ENUM_CHILD_CLASSES_EXCLUDETOP);
}
@@ -109,6 +117,8 @@ abstract class QueryReflection
class ModelReflectionRuntime extends ModelReflection
{
private static array $aAllIcons = [];
public function __construct()
{
}
@@ -255,6 +265,52 @@ class ModelReflectionRuntime extends ModelReflection
return new RunTimeIconSelectionField($sCode, $sLabel, $defaultValue);
}
public function GetAvailableIcons(): array
{
$aFolderList = [
APPROOT.'env-'.utils::GetCurrentEnvironment() => utils::GetAbsoluteUrlModulesRoot(),
APPROOT.'images/icons' => utils::GetAbsoluteUrlAppRoot().'images/icons',
];
if (count(self::$aAllIcons) == 0) {
foreach ($aFolderList as $sFolderPath => $sUrlPrefix) {
$aIcons = self::FindIconsOnDisk($sFolderPath);
ksort($aIcons);
foreach ($aIcons as $sFilePath) {
self::$aAllIcons[] = ['value' => $sFilePath, 'label' => basename($sFilePath), 'icon' => $sUrlPrefix.$sFilePath];
}
}
}
return self::$aAllIcons;
}
private static function FindIconsOnDisk(string $sBaseDir, string $sDir = '', array &$aFilesSpecs = []): array
{
$aResult = [];
// Populate automatically the list of icon files
if ($hDir = @opendir($sBaseDir.'/'.$sDir)) {
while (($sFile = readdir($hDir)) !== false) {
$aMatches = [];
if (($sFile != '.') && ($sFile != '..') && ($sFile != 'lifecycle') && is_dir($sBaseDir.'/'.$sDir.'/'.$sFile)) {
$sDirSubPath = ($sDir == '') ? $sFile : $sDir.'/'.$sFile;
$aResult = array_merge($aResult, self::FindIconsOnDisk($sBaseDir, $sDirSubPath, $aFilesSpecs));
}
$sSize = filesize($sBaseDir.'/'.$sDir.'/'.$sFile);
if (isset($aFilesSpecs[$sFile]) && $aFilesSpecs[$sFile] == $sSize) {
continue;
}
if (preg_match('/\.(png|jpg|jpeg|gif|svg)$/i', $sFile, $aMatches)) { // png, jp(e)g, gif and svg are considered valid
$aResult[$sFile.'_'.$sDir] = $sDir.'/'.$sFile;
$aFilesSpecs[$sFile] = $sSize;
}
}
closedir($hDir);
}
return $aResult;
}
public function GetRootClass($sClass)
{
return MetaModel::GetRootClass($sClass);

View File

@@ -248,45 +248,6 @@ class RestResultWithObjects extends RestResult
}
}
/**
* @package RESTAPI
* @api
*/
class RestResultWithObjectSets extends RestResultWithObjects
{
private $current_object = null;
public function MakeNewObjectSet()
{
$arr = array();
$this->current_object = &$arr;
$this->objects[] = &$arr;
}
/**
* Report the given object
*
* @api
* @param string $sObjectAlias Name of the subobject, usually the OQL class alias
* @param int $iCode An error code (RestResult::OK is no issue has been found)
* @param string $sMessage Description of the error if any, an empty string otherwise
* @param DBObject $oObject The object being reported
* @param array|null $aFieldSpec An array of class => attribute codes (Cf. RestUtils::GetFieldList). List of the attributes to be reported.
* @param boolean $bExtendedOutput Output all of the link set attributes ?
*
* @return void
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public function AppendSubObject($sObjectAlias, $iCode, $sMessage, $oObject, $aFieldSpec = null, $bExtendedOutput = false)
{
$oObjRes = ObjectResult::FromDBObject($oObject, $aFieldSpec, $bExtendedOutput, $iCode, $sMessage);
$this->current_object[$sObjectAlias] = $oObjRes;
}
}
/**
* @package RESTAPI
* @api
@@ -539,22 +500,15 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
break;
case 'core/get':
$sClassParam = RestUtils::GetMandatoryParam($aParams, 'class');
$sClass = RestUtils::GetClass($aParams, 'class');
$key = RestUtils::GetMandatoryParam($aParams, 'key');
$sShowFields = RestUtils::GetOptionalParam($aParams, 'output_fields', '*');
$aShowFields = RestUtils::GetFieldList($sClass, $aParams, 'output_fields');
$bExtendedOutput = (RestUtils::GetOptionalParam($aParams, 'output_fields', '*') == '*+');
$iLimit = (int)RestUtils::GetOptionalParam($aParams, 'limit', 0);
$iPage = (int)RestUtils::GetOptionalParam($aParams, 'page', 1);
// Validate the class(es)
$aClass = explode(',', $sClassParam);
foreach ($aClass as $sClass) {
if (!MetaModel::IsValidClass(trim($sClass))) {
throw new Exception("class '$sClass' is not valid");
}
}
$oObjectSet = RestUtils::GetObjectSetFromKey($sClassParam, $key, $iLimit, self::getOffsetFromLimitAndPage($iLimit, $iPage));
$sTargetClass = $oObjectSet->GetFilter()->GetClass();
$oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key, $iLimit, self::getOffsetFromLimitAndPage($iLimit, $iPage));
$sTargetClass = $oObjectSet->GetFilter()->GetClass();
if (UserRights::IsActionAllowed($sTargetClass, UR_ACTION_READ) != UR_ALLOWED_YES) {
$oResult->code = RestResult::UNAUTHORIZED;
@@ -565,67 +519,19 @@ class CoreServices implements iRestServiceProvider, iRestInputSanitizer
} elseif ($iPage < 1) {
$oResult->code = RestResult::INVALID_PAGE;
$oResult->message = "The request page number is not valid. It must be an integer greater than 0";
} elseif (count($oObjectSet->GetSelectedClasses()) > 1) {
$oResult = new RestResultWithObjectSets();
$aCache = [];
$aShowFields = [];
foreach ($oObjectSet->GetSelectedClasses() as $sSelectedClass) {
$aShowFields = array_merge( $aShowFields, RestUtils::GetFieldList($sSelectedClass, $aParams, 'output_fields', false));
}
while ($oObjects = $oObjectSet->FetchAssoc()) {
$oResult->MakeNewObjectSet();
foreach ($oObjects as $sAlias => $oObject) {
if (!$oObject) {
continue;
}
if (!array_key_exists($sAlias, $aCache)) {
$sClass = get_class($oObject);
$bExtendedOutput = RestUtils::HasRequestedExtendedOutput($sShowFields);
if (!RestUtils::HasRequestedAllOutputFields($sShowFields)) {
$aFields = $aShowFields[$sClass];
//Id is not a valid attribute to optimize
if ($aFields && in_array('id', $aFields)) {
unset($aFields[array_search('id', $aFields)]);
}
$aAttToLoad = [$sAlias => $aFields];
$oObjectSet->OptimizeColumnLoad($aAttToLoad);
}
$aCache[$sAlias] = [
'aShowFields' => $aShowFields,
'bExtendedOutput' => $bExtendedOutput,
];
} else {
$aShowFields = $aCache[$sAlias]['aShowFields'];
$bExtendedOutput = $aCache[$sAlias]['bExtendedOutput'];
}
$oResult->AppendSubObject($sAlias, 0, '', $oObject, $aShowFields, $bExtendedOutput);
}
}
$oResult->message = "Found: ".$oObjectSet->Count();
} else {
$aShowFields =[];
foreach ($aClass as $sSelectedClass) {
$sSelectedClass = trim($sSelectedClass);
$aShowFields = array_merge($aShowFields, RestUtils::GetFieldList($sSelectedClass, $aParams, 'output_fields', false));
}
if (!RestUtils::HasRequestedAllOutputFields($sShowFields) && count($aShowFields) == 1) {
$aFields = $aShowFields[$sClass];
//Id is not a valid attribute to optimize
if (in_array('id', $aFields)) {
unset($aFields[array_search('id', $aFields)]);
}
if (!$bExtendedOutput && RestUtils::GetOptionalParam($aParams, 'output_fields', '*') != '*') {
$aFields = $aShowFields[$sClass];
//Id is not a valid attribute to optimize
if (in_array('id', $aFields)) {
unset($aFields[array_search('id', $aFields)]);
}
$aAttToLoad = [$oObjectSet->GetClassAlias() => $aFields];
$oObjectSet->OptimizeColumnLoad($aAttToLoad);
}
$oObjectSet->OptimizeColumnLoad($aAttToLoad);
}
while ($oObject = $oObjectSet->Fetch()) {
$oResult->AddObject(0, '', $oObject, $aShowFields, RestUtils::HasRequestedExtendedOutput($sShowFields));
$oResult->AddObject(0, '', $oObject, $aShowFields, $bExtendedOutput);
}
$oResult->message = "Found: ".$oObjectSet->Count();
}

View File

@@ -75,3 +75,26 @@ collection-entry-element {
resize: vertical;
}
.ibo-form-compact{
.ibo-field{
display: flex;
flex-direction: column;
gap: 4px;
label{
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: unset;
padding-right: 0;
}
}
}

View File

@@ -19,7 +19,10 @@ $ibo-dashlet-blocker--height: 100% !default;
.ibo-dashlet {
position: relative;
width: calc(#{$ibo-dashlet--width} - #{$ibo-dashlet--elements-spacing-x});
margin: calc(#{$ibo-dashlet--elements-spacing-y} / 2) calc(#{$ibo-dashlet--elements-spacing-x} / 2);
//margin: calc(#{$ibo-dashlet--elements-spacing-y} / 2) calc(#{$ibo-dashlet--elements-spacing-x} / 2);
height: 100% !important;
width: 100% !important;
&.dashlet-selected {
position: relative;
@@ -38,4 +41,21 @@ $ibo-dashlet-blocker--height: 100% !default;
width: $ibo-dashlet-blocker--width;
height: $ibo-dashlet-blocker--height;
cursor: not-allowed;
}
.ibo-dashlet--actions {
position: absolute;
z-index: 1000;
top: 0px;
right: 0px;
display: none;
padding: 4px;
border-radius: 4px;
background-color: $ibo-color-white-100;
@extend %ibo-elevation-100;
}
ibo-dashlet[data-edit-mode="edit"] {
z-index: 3;
}

View File

@@ -54,3 +54,31 @@ $ibo-input-select-icon--menu--icon--margin-right: 10px !default;
}
}
}
.ts-control > .ibo-input-select-icon--menu--item{
display: flex;
column-gap: 5px;
align-items: center;
padding: 5px 0;
> img{
width: 20px;
}
&:hover{
background: transparent;
}
}
.ts-dropdown > .ts-dropdown-content > .ibo-input-select-icon--menu--item{
display: flex;
column-gap: 5px;
> img{
width: 32px;
}
}

View File

@@ -15,3 +15,4 @@
@import "wizard-container/wizard-container";
@import "object/all";
@import "activity-panel/all";
@import "dashlet-panel/all";

View File

@@ -175,3 +175,73 @@ input:checked + .ibo-dashboard--slider:before {
input:checked + .ibo-dashboard--slider:after {
content: $ibo-dashboard--slider--before--content;
}
// TODO 3.3 Cleanup variables
// TODO 3.3 Move to vendor what's from gridstack
.grid-stack {
display: block;
}
.ibo-dashboard[data-edit-mode="edit"] .grid-stack{
background-size: calc(100% / 12) var(--gs-cell-height);
background-color: $ibo-color-white-100;
background-image: linear-gradient(to right, $ibo-color-white-200 8px, transparent 8px), linear-gradient(to bottom, $ibo-color-white-200 8px, transparent 8px);
--gs-item-margin-top: 8px;
--gs-item-margin-bottom: 0;
--gs-item-margin-right: 0;
--gs-item-margin-left: 8px;
}
ibo-dashboard[data-edit-mode="view"] {
.ibo-dashboard--form {
display: none;
}
}
.ibo-dashboard--form {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 55px;
background-color: $ibo-color-blue-200;
margin: -16px -36px 24px -36px;
padding: 0 36px;
}
.ibo-dashboard--form--inputs {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
@extend %common-font-ral-med-250;
}
.ibo-dashboard[data-edit-mode="edit"] ibo-dashlet:not([data-edit-mode="edit"]):hover .ibo-dashlet--actions {
display: block;
}
.ibo-dashboard[data-edit-mode="error"] .grid-stack{
background-image: url($approot-relative + '/images/alpha-fatal-error.gif');
}
// Our edit mode dashboard already has its own header, so we hide the standard one
#ibo-page-header:has(+ ibo-dashboard[data-edit-mode="edit"]) {
display: none;
}
.ibo-dashboard[data-edit-mode="edit"] .ibo-dashboard--grid:has(ibo-dashboard-grid-slot > ibo-dashlet[data-edit-mode="edit"]) .ibo-dashboard--grid--backdrop {
position: absolute;
height: calc(100% + 24px);
// 36px is $ibo-page-container--elements-padding-x, handle variable resolution
width: calc(100% + 36px + 36px);
margin: -24px -#{36px} 0 -#{36px};
background-color: $ibo-color-grey-400;
z-index: 2;
opacity: 60%;
}

View File

@@ -0,0 +1,2 @@
@import "dashlet-entry";
@import "dashlet-panel";

View File

@@ -0,0 +1,51 @@
// TODO 3.3 Cleanup variables
.ibo-dashlet-entry {
display: flex;
flex-direction: row;
gap: 10px;
border: 1px solid $ibo-color-grey-300;
border-radius: 5px;
padding: 12px;
background-color: $ibo-color-grey-100;
cursor: pointer;
text-align: left;
&:hover {
background-color: $ibo-color-grey-200;
border-color: $ibo-color-grey-400;
}
&:active {
background-color: $ibo-color-grey-50;
border-color: $ibo-color-grey-500;
}
}
.ibo-dashlet-entry--icon {
flex-shrink: 0;
height: 36px;
width: 36px;
}
.ibo-dashlet-entry--content {
display: flex;
flex-direction: column;
justify-content: center;
overflow-x: hidden;
gap: 4px;
}
.ibo-dashlet-entry--title {
font-size: 14px;
font-weight: 600;
color: $ibo-color-grey-900;
}
.ibo-dashlet-entry--description {
font-size: 12px;
color: $ibo-color-grey-700;
}

View File

@@ -0,0 +1,66 @@
// TODO 3.3 Cleanup variables
.ibo-dashlet-panel {
height: 100%;
display: flex;
flex-direction: column;
width: 326px;
background-color: $ibo-color-white-100;
}
.ibo-dashlet-panel--title {
display: flex;
flex-direction: column;
justify-content: center;
height: 55px;
background-color: $ibo-color-white-200;
padding: 0 16px;
flex-grow: 0;
@extend %common-font-ral-med-300;
}
.ibo-dashlet-panel--entries, .ibo-dashlet-panel--form-container {
flex-grow: 1;
display: flex;
flex-direction: column;
overflow: auto;
padding: 16px;
gap: 12px;
&.ibo-is-hidden {
display: none;
}
}
.ibo-center-container:has(ibo-dashboard[data-edit-mode="view"]) .ibo-dashlet-panel{
display: none;
}
.ibo-dashlet-panel--form-container turbo-frame {
height: 100%;
}
.ibo-dashlet-panel--form-container .ibo-form {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
> .form {
overflow: auto;
margin-bottom: 16px;
}
}
.ibo-dashlet-panel--form-container--buttons {
display: flex;
flex-direction: row !important;
justify-content: end;
align-items: center;
margin: 0 -16px -16px -16px;
padding: 0 16px;
min-height: 60px;
background-color: $ibo-color-grey-50;
border-top: solid 1px $ibo-color-grey-400;
}

View File

@@ -5,11 +5,9 @@
$ibo-multi-column--margin-x: -$ibo-spacing-500 !default; /* This is to compensate columns padding and make the whole multicolumn align with the parent borders (cf. Bootstrap rows / cols) */
$ibo-multi-column--margin-y: $ibo-spacing-0 !default;
$ibo-multi-column--row-gap: $ibo-spacing-800 !default;
.ibo-multi-column {
display: flex;
flex-wrap: wrap;
margin: $ibo-multi-column--margin-y $ibo-multi-column--margin-x;
row-gap: $ibo-multi-column--row-gap;
}

View File

@@ -1,205 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<Set>
<Brand alias="Brand" id="1">
<name>Acer</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icon-acer.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmc6c3ZnIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIiB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiIHhtbG5zOnN2Zz0iaH
R0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgaWQ9ImFjZXIiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iODMiIGhlaWdodD0iODMiIHZpZXdCb3g9IjAgMCA4MyA4MyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgc29kaXBvZGk6ZG9jbmFtZT0iaWNvbi1hY2VyLnN2ZyIgaW5rc2NhcGU6dmVyc2lvbj0iMS40LjMgKDBkMTVmNzUwNDIsIDIwMjUtMTItMjUpIj48c3ZnOmRlZnMgaWQ9ImRlZnMxIi8+PHNvZGlwb2RpOm5hbWVkdmlldyBpZD0ibmFtZWR2
aWV3MSIgcGFnZWNvbG9yPSIjZmZmZmZmIiBib3JkZXJjb2xvcj0iIzAwMDAwMCIgYm9yZGVyb3BhY2l0eT0iMC4yNSIgaW5rc2NhcGU6c2hvd3BhZ2VzaGFkb3c9IjIiIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiIGlua3NjYXBlOnBhZ2VjaGVja2VyYm9hcmQ9IjAiIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIgc2hvd2d1aWRlcz0idHJ1ZSIgaW5rc2NhcGU6em9vbT0iMTAuNDgzMDcxIiBpbmtzY2FwZTpjeD0iNzUuNzg4ODYiIGlua3NjYXBlOmN5PSIzNS
42NzY1NjgiIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMjU2MCIgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTM2MCIgaW5rc2NhcGU6d2luZG93LXg9IjAiIGlua3NjYXBlOndpbmRvdy15PSIwIiBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIiBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJhY2VyIj48c29kaXBvZGk6Z3VpZGUgcG9zaXRpb249IjQwLjMwNzkxOSw0MS4yMjAyMTQiIG9yaWVudGF0aW9uPSIwLC0xIiBpZD0iZ3VpZGUxIiBpbmtzY2FwZTpsb2NrZWQ9
ImZhbHNlIi8+PC9zb2RpcG9kaTpuYW1lZHZpZXc+Cjxzdmc6cGF0aCBpZD0iYWNlci1sb2dvIiBzdHlsZT0iZmlsbDojODBjMzQzIiBkPSJtIDgyLjg1LDMyLjc2MDg5MyBjIC0wLjI5LC0wLjQyIC0wLjYyLC0wLjcgLTIuMDQsLTAuODEgLTAuMzcsLTAuMDMgLTEuNiwtMC4wNyAtMy42NiwtMC4wNyAtNS40MiwwIC05LjEsMS42NSAtMTAuOTgsNC45MSAwLjUyLC0zLjI4IC0yLjY3LC00LjkyIC05LjU1LC00LjkyIC04LjA5LDAgLTEzLjE0LDMuMjcgLTE1LjE1LDkuOD
EgLTAuNzQsMS45MSAtMC44Miw0LjAyIC0wLjIzLDUuOTggbCAtMS45MSwwLjEzIGMgLTEuNDksMC4xMSAtMy40NiwwLjIyIC00LjY2LDAuMjIgLTIuOTYsMCAtNC44LC0wLjUgLTUuNTEsLTEuNSAtMC42NCwtMC45IC0wLjU3LC0yLjU2IDAuMTcsLTQuOTggMS4yMywtNC4wMiA0LjI5LC01LjgyIDkuMTgsLTUuODIgMi41MywwIDUsMC4yNyA1LDAuMjcgMC4zLC0wLjA1IDAuNTEsLTAuMzMgMC40NiwtMC42NCAwLC0wLjAxIDAsLTAuMDIgLTAuMDEsLTAuMDMgbCAtMC4w
NSwtMC4zOCAtMC4xNywtMS4zNSBjIC0wLjExLC0wLjg5IC0wLjc3LC0xLjQxIC0xLjk3LC0xLjU3IC0xLjEsLTAuMDcgLTEuOTEsLTAuMTMgLTMuMTIsLTAuMTMgaCAtMC4wNyBjIC02LjM5LDAuMDEgLTEwLjg0LDIuMTIgLTEzLjM2LDYuMzIgMC45MiwtNC4yMSAtMi4wNCwtNi4zMiAtOC44NywtNi4zMiAtMi43MywtMC4wNSAtNS40NiwwLjA2IC04LjE4LDAuMzMgLTEuMDMsLTAuMDMgLTEuOTYsMC42MSAtMi4zMywxLjU3IGwgLTAuNTQsMS43MyBjIC0wLjEsMC4zMi
AwLjA1LDAuNDggMC40NiwwLjQ4IGggMC4wMSBjIDAuMywtMC4wNCAxLjQ2LC0wLjE1IDMuNDQsLTAuMzEgMi42MiwtMC4yMSA0LjU1LC0wLjMyIDUuODMsLTAuMzIgMy43NywwIDUuMzUsMC45NSA0Ljc2LDIuODQgLTAuMTEsMC40IC0wLjQ0LDAuNzEgLTAuODUsMC43OCAtMy41NiwwLjU5IC02LjM5LDEuMDIgLTguNDgsMS4yOCAtNS44OSwwLjc1IC05LjI5LDIuNTkgLTEwLjIsNS41MiAtMS4yNSw0LjAzIDEuODksNi4wNCA5LjQ0LDYuMDQgMy4xNywwLjA0IDYuMzQs
LTAuMTggOS40OCwtMC42NSAxLjQ3LC0wLjI1IDIuMjIsLTAuNjMgMi41MSwtMS41OCBsIDEuNDEsLTQuNDkgYyAtMC4xNSw0LjQ5IDIuOTksNi43NSA5LjQxLDYuNzUgMi4yMywwLjA0IDQuNDUsLTAuMDYgNi42NiwtMC4zMSAxLjMyLC0wLjIzIDEuOTUsLTAuNTIgMi4yNCwtMS41MSBsIDAuMjgsLTEuNDEgYyAxLjM3LDIuMTQgNC4zNywzLjIxIDkuMDEsMy4yMSA0Ljg5LDAgNy43NCwtMC4xNCA4LjU1LC0wLjQxIDAuNTcsLTAuMTIgMS4wNCwtMC41MiAxLjI1LC0xLj
A2IDAuMDMsLTAuMDkgMC4wNSwtMC4xOSAwLjA2LC0wLjI4IGwgMC41OSwtMS45MSBjIDAuMSwtMC4zMyAtMC4wNSwtMC40OSAtMC40NSwtMC40OCBsIC0zLjEzLDAuMTUgYyAtMi4wOSwwLjEyIC0zLjcyLDAuMTggLTQuOSwwLjE3IC0xLjU0LDAuMDggLTMuMDgsLTAuMDcgLTQuNTgsLTAuNDYgLTEuMiwtMC40MSAtMS45NCwtMS42MSAtMS43NywtMi44NiBsIDkuOTEsLTEuMjcgYyA0LjQ0LC0wLjU3IDcuNDIsLTEuOTQgOC45MiwtNC4xMiBsIC0zLjM3LDExLjczIGMg
LTAuMDcsMC4xOCAtMC4wNCwwLjM4IDAuMDcsMC41MyAwLjE4LDAuMTcgMC40MSwwLjI1IDAuNjUsMC4yMiBoIDQuMSBjIDAuNDcsMC4wNyAwLjkzLC0wLjIyIDEuMDYsLTAuNjggbCAzLjYyLC0xMi42NyBjIDAuNDgsLTEuNjcgMiwtMi40OCA0LjY3LC0yLjQ4IDIuNDEsMCA0LjIyLDAuMDIgNS4zOCwwLjA3IDAuMDMsMCAwLjA2LDAgMC4wOSwwIDAuMzksMCAwLjc0LC0wLjI2IDAuODQsLTAuNjMgbCAwLjYzLC0xLjc0IGMgMC4xNSwtMC4zIDAuMTIsLTAuNjMgLTAuMD
UsLTAuODkgbSAtNjYuMTIsMTUuMjQgYyAtMS44MywwLjIzIC0zLjY4LDAuMzMgLTUuNTIsMC4zIC00LjE3LDAgLTUuOTksLTAuODQgLTUuNDYsLTIuNTMgMC4zOCwtMS4yMSAxLjQ3LC0xLjk0IDMuMjgsLTIuMTkgbCA5LjQ4LC0xLjI4IHogbSA0My44MywtMTAuMjcgYyAtMC40LDEuMyAtMi4yNSwyLjE5IC01LjU2LDIuNjcgbCAtNy45LDEuMTMgMC4yLC0wLjY1IGMgMC4zOSwtMS43NCAxLjM3LC0zLjI4IDIuNzksLTQuMzcgMS4yLC0wLjc3IDMuMTUsLTEuMTYgNS44NiwtMS4xNiAzLjU2LDAgNS4xLDAuOCA0LjYxLDIuMzgiLz4KPC9zdmc6c3ZnPgo=</data><downloads_count>0</downloads_count></logo>
<finalclass>Brand</finalclass>
<friendlyname>Acer</friendlyname>
</Brand>
<Brand alias="Brand" id="2">
<name>Apple</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-mac-os.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiMwODgzZDkiIGQ9Ik0zNi4yMzIsMjMuOTg1YzAtNS44NjUsNC43NjYtOC41MSw0Ljk2Ni04LjYzNmMtMi41OTYtMy45OTMtNi43OS00LjQ2Ny
04LjM2Mi00LjQ2OCBjLTMuNjQzLDAtNi44NjMsMi4wMjItOC41ODUsMi4wMjJjLTEuNzk3LDAtNC40MTgtMi4xMjEtNy4zNjMtMi4wMjJjLTMuODQzLDAuMDc1LTcuMzYzLDIuMzQ2LTkuMzM0LDUuNjkxIGMtMS4zOTcsMi4zOTYtMS45NDcsNS4yMTctMS44OTYsOC4wODdjMC4wMDIsMC4xMTMsMC4wMTcsMC4yMjgsMC4wMiwwLjM0MUgzNi4zMkMzNi4yNzksMjQuNjcxLDM2LjI0MywyNC4zMzcsMzYuMjMyLDIzLjk4NXoiLz48cGF0aCBmaWxsPSIjMDg4M2Q5IiBkPSJN
MzAuNTY1LDcuMDYzQzMyLjI2MSw1LjE5MSwzMy4yMSwyLjYyMSwzMy4wNiwwYy0yLjM0NiwwLTUuMDY2LDEuMzcyLTYuNzg4LDMuMzk0IGMtMS4zNDgsMS42NzItMi43OTUsNC4yOTMtMi4yNzEsNi45MTNDMjYuNDIyLDEwLjYwNywyOS4wNDMsOS4wODUsMzAuNTY1LDcuMDYzeiIvPjxwYXRoIGZpbGw9IiMwMzcwYzgiIGQ9Ik0xNy41MTEsNDVjMi43NzEsMCwzLjc5NC0xLjg0OCw3LjQxMy0xLjg0OGMzLjM3LDAsNC40MTgsMS44NDgsNy4zMzgsMS44NDggYzMuMDcsMCw1LjA5Mi0yLjc5NSw2LjkxMy01LjU2N2MyLjI5NS0zLjIxOCwzLjA3LTYuMjg4LDMuMTY5LTYuNDE0Yy0wLjA5NCwwLTUuMjg3LTIuMTEyLTYuMDI2LTguMDE5SDUuNjc4IGMwLjE1Nyw1LjMxMSwyLjIyOCwxMC43OSw0LjY3MSwxNC4zMDlDMTIuMjcsNDIuMDU1LDE0LjQ0MSw0NSwxNy41MTEsNDV6Ii8+PC9zdmc+Cg==</data><downloads_count>0</downloads_count></logo>
<finalclass>Brand</finalclass>
<friendlyname>Apple</friendlyname>
</Brand>
<Brand alias="Brand" id="3">
<name>Asus</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-asus.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIgeG1sbnM9Imh0dHA6Ly93
d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwLDAsMjU2LDI1NiIgd2lkdGg9IjY0cHgiIGhlaWdodD0iNjRweCIgZmlsbC1ydWxlPSJub256ZXJvIiB2ZXJzaW9uPSIxLjEiIGlkPSJzdmcxMyIgc29kaXBvZGk6ZG9jbmFtZT0iaWNvbnM4LWFzdXMgKDEpLnN2ZyIgaW5rc2NhcGU6dmVyc2lvbj0iMS40LjMgKDBkMTVmNzUwNDIsIDIwMjUtMTItMjUpIj48c29kaXBvZGk6bmFtZWR2aWV3IGlkPS
JuYW1lZHZpZXcxMyIgcGFnZWNvbG9yPSIjZmZmZmZmIiBib3JkZXJjb2xvcj0iIzAwMDAwMCIgYm9yZGVyb3BhY2l0eT0iMC4yNSIgaW5rc2NhcGU6c2hvd3BhZ2VzaGFkb3c9IjIiIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiIGlua3NjYXBlOnBhZ2VjaGVja2VyYm9hcmQ9IjAiIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIgaW5rc2NhcGU6em9vbT0iMTcuMTg3NSIgaW5rc2NhcGU6Y3g9IjMxLjk3MDkwOSIgaW5rc2NhcGU6Y3k9IjMyIiBpbmtzY2FwZTp3
aW5kb3ctd2lkdGg9IjI1NjAiIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjEzNjAiIGlua3NjYXBlOndpbmRvdy14PSIwIiBpbmtzY2FwZTp3aW5kb3cteT0iMCIgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0iZzEzIi8+PGRlZnMgaWQ9ImRlZnMxMCI+PHJhZGlhbEdyYWRpZW50IGN4PSIzNC42MjUiIGN5PSIzMS44NzUiIHI9IjIzLjIwNiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJjb2xvci0xIj
48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNmNGU5YzMiIGlkPSJzdG9wMSIvPjxzdG9wIG9mZnNldD0iMC4yMTkiIHN0b3AtY29sb3I9IiNmOGVlY2QiIGlkPSJzdG9wMiIvPjxzdG9wIG9mZnNldD0iMC42NDQiIHN0b3AtY29sb3I9IiNmZGY0ZGMiIGlkPSJzdG9wMyIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZmZjZlMSIgaWQ9InN0b3A0Ii8+PC9yYWRpYWxHcmFkaWVudD48bGluZWFyR3JhZGllbnQgeDE9IjAuMzY1IiB5MT0iMzIuNSIgeDI9
IjY0IiB5Mj0iMzIuNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJjb2xvci0yIj48c3RvcCBvZmZzZXQ9IjAuMDUzIiBzdG9wLWNvbG9yPSIjMDAwMDAwIiBpZD0ic3RvcDUiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwMDAwMDAiIGlkPSJzdG9wNiIvPjwvbGluZWFyR3JhZGllbnQ+PHJhZGlhbEdyYWRpZW50IGN4PSIzNC42MjUiIGN5PSIzMS44NzUiIHI9IjIzLjIwNiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPS
Jjb2xvci0zIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNmNGU5YzMiIGlkPSJzdG9wNyIvPjxzdG9wIG9mZnNldD0iMC4yMTkiIHN0b3AtY29sb3I9IiNmOGVlY2QiIGlkPSJzdG9wOCIvPjxzdG9wIG9mZnNldD0iMC42NDQiIHN0b3AtY29sb3I9IiNmZGY0ZGMiIGlkPSJzdG9wOSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2ZmZjZlMSIgaWQ9InN0b3AxMCIvPjwvcmFkaWFsR3JhZGllbnQ+PC9kZWZzPjxnIGZpbGw9Im5vbmUiIGZpbGwtcnVs
ZT0ibm9uemVybyIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIHN0cm9rZS1saW5lY2FwPSJidXR0IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1kYXNoYXJyYXk9IiIgc3Ryb2tlLWRhc2hvZmZzZXQ9IjAiIGZvbnQtZmFtaWx5PSJub25lIiBmb250LXdlaWdodD0ibm9uZSIgZm9udC1zaXplPSJub25lIiB0ZXh0LWFuY2hvcj0ibm9uZSIgc3R5bGU9Im1peC1ibGVuZC1tb2RlOiBub3JtYWwiIGlkPSJnMT
MiPjxnIHRyYW5zZm9ybT0ic2NhbGUoNCw0KSIgaWQ9ImcxMiI+PHBhdGggZD0iTTYuMTU2LDI5LjI0M2wtNS43MjMsOS45MTljLTAuMTkyLDAuMzMzIDAuMDQ4LDAuNzUgMC40MzMsMC43NWgyLjc2NGMwLjM1NiwwIDAuNjg2LC0wLjE5IDAuODY1LC0wLjQ5OGw1LjYyOSwtOS43MDF6TTYuMjk5LDI5LjAwNWMwLDAgMC44MTUsLTEuNDg2IDEuNjI1LC0yLjkyYzAuMzU0LC0wLjYyOCAxLjAxOSwtMS4wMTQgMS43NCwtMS4wMTRjMS40MDgsMC4wMDEgMy43NTcsMC4wMDMg
NS4wODYsMC4wMDRjMC41NTIsMCAwLjk5NSwwLjQ0OCAwLjk5NSwxLjAwMXYyLjYxYzAsMCAwLjMyMywtMS4zODYgMC44OTEsLTIuMDQzYzAuNTM0LC0wLjYxOSAxLjI0NiwtMS40NzggMi44NzIsLTEuNTkyaDEwLjk2M2MwLjU1MiwwIDEsMC40NDggMSwxdjEuOTU1YzAsMC41NTIgLTAuNDQ4LDEgLTEsMXpNNjQsMjguMDA4di0xLjk1OGMwLC0wLjU1MiAtMC40NDgsLTEgLTEsLTFoLTExLjhjLTEuNjIyLDAuMTE0IC0yLjMzNywwLjk3MiAtMi44NzIsMS41OTJjLTAuNT
Y5LDAuNjU3IC0wLjg5LDIuMDQzIC0wLjg5LDIuMDQzdjAuMzE4bDE1LjU2MiwwLjAwNWMwLjU1MiwwIDEsLTAuNDQ4IDEsLTF6TTMzLjA5NSwyNS4wN2gxLjU3OGMwLjU1MiwwIDEsMC40NDggMSwxdjIuOTM1aC0zLjU3OHYtMi45MzVjMCwtMC41NTIgMC40NDgsLTEgMSwtMXpNNDQuODM5LDI1LjA3aDEuNTc0YzAuNTUyLDAgMSwwLjQ0OCAxLDF2Mi45MzVoLTMuNTc0di0yLjkzNWMwLC0wLjU1MiAwLjQ0NywtMSAxLC0xek00Ny40MTMsMzAuNDI1bC0zLjUxNCwtMC4y
MzN2NC41NjZjMCwwIC0wLjAxLDEuMzE0IC0xLjE1OCwxLjMxNGMtMS4xMzgsMCAtNi4wMzksMCAtNi4wMzksMGMwLDAgLTEuMDIzLC0wLjA5MyAtMS4wMjMsLTEuMzAzYzAsLTEuMjA3IDAsLTUuMTk2IDAsLTUuMTk2bC0zLjU0OSwtMC4yODV2Ny4wMDFjMC41NzQsMy4zNTkgMy4zMTEsMy42MTcgMy4zMTEsMy42MTdjMCwwIDAuMjc4LDAuMDIyIDAuMzI0LDAuMDI2aDguMTU2YzAsMCAzLjQ5MSwtMC4zMTEgMy40OTEsLTQuMDIzYzAuMDAxLC0zLjcxMyAwLjAwMSwtNS
40ODQgMC4wMDEsLTUuNDg0ek0xMy4xMTEsMzkuOTE0aDE1LjczYzIuOTE0LC0wLjY0NyAzLjIxOCwtMy41NzcgMy4yMTgsLTMuNTc3YzAuMTM2LC0wLjg3MSAwLjA1NywtMS41NzQgMC4wNTcsLTEuNTc0Yy0wLjA4OCwtMC41ODggLTEuMDk0LC0zLjE3OCAtMy4yODEsLTMuNDA1Yy0xLjMwNCwtMC4xMzQgLTEyLjkxLC0xLjE1NSAtMTIuOTEsLTEuMTU1YzAuMjI1LDEuMjg4IDAuNzQ4LDEuOTM4IDEuMDk0LDIuMzExYzAuODA1LDAuODU3IDIuMDg3LDEuMDk5IDIuMDg3
LDEuMDk5YzAuMzA5LDAuMDMzIDguOTQ1LDAuODI0IDguOTQ1LDAuODI0YzAuMjc0LDAuMDE3IDAuNzk3LDAuMDkzIDAuNzksMC44NDljMCwwLjA5MSAtMC4wNzUsMC43NTcgLTAuNzM4LDAuNzU3aC0xMi4wMTRjLTAuMjc2LDAgLTAuNSwtMC4yMjQgLTAuNSwtMC41di01LjM3M2wtMy40NzgsLTAuMjgxdjkuMDI3YzAsMC41NSAwLjQ0OCwwLjk5OCAxLDAuOTk4ek00Ny40NDQsMzcuMDE3YzAuMDAxLC0wLjU1MiAwLjQ0OCwtMC45OTggMSwtMC45OThoMTEuNDI5YzAuMj
g2LDAgMC41OTUsLTAuMTg3IDAuNTk1LC0wLjE4N2MwLjEzLC0wLjEyOSAwLjIzNSwtMC4zNjQgMC4yMzUsLTAuNTkxYzAsLTAuNzUzIC0wLjU2MSwtMC43ODYgLTAuODQzLC0wLjgwM2MwLDAgLTguNzMyLC0wLjgwOCAtOS4wMzYsLTAuODM0YzAsMCAtMS4yMTEsLTAuMjA3IC0yLjAxNywtMS4wNjhjLTAuMzUxLC0wLjM2OSAtMC44MTUsLTAuNzcxIC0xLjE2MSwtMi4wOTljMCwwIDExLjY3MSwwLjc2MyAxMi45NjgsMC44OTdjMi4xODksMC4yMzIgMy4yMTUsMi42MzIgMy4zMDgsMy40M2MwLDAgMC4wOTMsMC43MjIgLTAuMDIsMS42MDdjMCwwIC0wLjQ1NCwzLjM3NCAtMy42NjQsMy41NzloLTExLjc5N2MtMC41NTMsMCAtMS4wMDEsLTAuNDQ5IC0xLC0xLjAwMnoiIGZpbGw9InVybCgjY29sb3ItMikiIGlkPSJwYXRoMTEiLz48L2c+PC9nPjwvc3ZnPgo=</data><downloads_count>0</downloads_count></logo>
<finalclass>Brand</finalclass>
<friendlyname>Asus</friendlyname>
</Brand>
<Brand alias="Brand" id="4">
<name>Cisco</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icon-cisco.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIgeG1sbnM9Imh0dHA6Ly93d
3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgd2lkdGg9IjEwOCIgaGVpZ2h0PSIxMDgiIHZpZXdCb3g9IjAgMCAxMDggMTA4IiByb2xlPSJpbWciIHZlcnNpb24
9IjEuMSIgaWQ9InN2ZzExIiBzb2RpcG9kaTpkb2NuYW1lPSJpY29uLWNpc2NvLnN2ZyIgaW5rc2NhcGU6dmVyc2lvbj0iMS40LjMgKDBkMTVmNzUwNDIsIDIwMjUtMTItMjUpIj48c29kaXBvZGk6bmFtZWR2aWV3IGlkPSJuYW1lZHZpZXcxMSIgcGFnZWNvbG9yPSIjZmZmZmZmIiBib3JkZXJjb2xvcj0iIzAwMDAwMCIgYm9yZGVyb3BhY2l0eT0iMC4yNSIgaW5rc2NhcGU6c2hvd3BhZ2VzaGFkb3c9IjIiIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiIGlua3NjYXBlO
nBhZ2VjaGVja2VyYm9hcmQ9IjAiIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIgc2hvd2d1aWRlcz0idHJ1ZSIgaW5rc2NhcGU6em9vbT0iNi44MjI5NjAyIiBpbmtzY2FwZTpjeD0iNDguODc5MDc4IiBpbmtzY2FwZTpjeT0iNTEuNzM3MDc1IiBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjI1NjAiIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjEzNjAiIGlua3NjYXBlOndpbmRvdy14PSIwIiBpbmtzY2FwZTp3aW5kb3cteT0iMCIgaW5rc2NhcGU6d2luZG93LW1heGl
taXplZD0iMSIgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnMTEiPjxzb2RpcG9kaTpndWlkZSBwb3NpdGlvbj0iMTYuOTM5NTg3LDUzLjM3NjMwMyIgb3JpZW50YXRpb249IjAsLTEiIGlkPSJndWlkZTExIiBpbmtzY2FwZTpsb2NrZWQ9ImZhbHNlIi8+PC9zb2RpcG9kaTpuYW1lZHZpZXc+PHRpdGxlIGlkPSJmdy1jLWhlYWRlcl9fbG9nby10aXRsZSI+Q2lzY28uY29tIEZyYW5jZTwvdGl0bGU+PGRlZnMgaWQ9ImRlZnMxIj48cGF0aCBkPSJtIDAsMjIuNzY5IGEgM
i4zNDgsMi4zNDggMCAwIDAgMy45MzUsMS41NDUgYyAwLjQzNCwtMC40MDMgMC43LC0wLjk1NSAwLjc0NCwtMS41NDUgdiAtNS4yIGEgMi4zNCwyLjM0IDAgMCAwIC00LjY3OSwwIHYgNS4yIiBpZD0iYmFyX3Nob3J0IiBjbGFzcz0iYmFyIi8+PHBhdGggZD0ibSAxMi45NSwyMi43NjkgYSAyLjM0OSwyLjM0OSAwIDAgMCAyLjM0LDIuMTcxIDIuMzQ2LDIuMzQ2IDAgMCAwIDIuMzM5LC0yLjE3MSBWIDExLjExMiBhIDIuMzQxLDIuMzQxIDAgMCAwIC00LjY3OSwwIFYgMjI
uNzciIGlkPSJiYXJfdGFsbCIgY2xhc3M9ImJhciIvPjxwYXRoIGQ9Im0gMjUuODMyLDI3LjQ2NCBhIDIuMzQ1LDIuMzQ1IDAgMCAwIDQuNjc4LDAgViAyLjI0OSBhIDIuMzQyLDIuMzQyIDAgMCAwIC00LjY3OCwwIHYgMjUuMjE1IiBpZD0iYmFyX2dyYW5kZSIgY2xhc3M9ImJhciIvPjxwYXRoIGQ9Im0gMjQuMDI2LDU2LjI3NyB2IC01LjAwMiBsIC0wLjA5OCwwLjA0MyBhIDkuMjUzLDkuMjUzIDAgMCAxIC0zLjYwNSwwLjkxNSA1LjMwMyw1LjMwMyAwIDAgMSAtMy42M
ywtMS4wNyA0LjY0NCw0LjY0NCAwIDAgMSAtMS41OCwtMi4yNDQgNS4zOTUsNS4zOTUgMCAwIDEgLTAuMTA2LC0zIDQuNiw0LjYgMCAwIDEgMS42MDksLTIuNTY2IDQuODIzLDQuODIzIDAgMCAxIDIuNTI4LC0xLjA5IDguMzMyLDguMzMyIDAgMCAxIDQuNzc0LDAuODk1IGwgMC4xMDgsMC4wNTYgdiAtNS4wMyBsIC0wLjIyOCwtMC4wNjEgYSAxMi43OCwxMi43OCAwIDAgMCAtNC41NTIsLTAuNTk2IDEwLjUzNCwxMC41MzQgMCAwIDAgLTQuMDY1LDAuOTMgOS4yOSw5LjI
5IDAgMCAwIC0zLjMyOSwyLjU3MiAxMC4wMTQsMTAuMDE0IDAgMCAwIC0wLjE4MiwxMi4xOCA5LjU0Niw5LjU0NiAwIDAgMCA1LjI5MiwzLjQwMyBjIDIuMjExLDAuNTM4IDQuNTI4LDAuNDU2IDYuNjk3LC0wLjIzNCBsIDAuMzY3LC0wLjEwMSIgaWQ9ImNpc2NvX2MiLz48L2RlZnM+PGcgZmlsbD0iIzAyYzhmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBpZD0iZzExIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMC4wOTQ2Njg1OSwyNi4yMzQ5NDYpIj48dXNlIGhyZWY9IiNja
XNjb19jIiBpZD0idXNlMSIvPjx1c2UgaHJlZj0iI2Npc2NvX2MiIHg9IjQ5LjUiIGlkPSJ1c2UyIi8+PHBhdGggZD0ibSA0MS42OSw1Mi4xMjUgLTAuMDE5LDQuMzA4IDAuMzE1LDAuMDU0IGMgMC45ODksMC4xODUgMS45ODcsMC4zMTMgMi45OSwwLjM4NSBhIDE4LjEsMTguMSAwIDAgMCAyLjY0MiwwLjA1MSBjIDAuODQyLC0wLjA2IDEuNjc1LC0wLjIxOCAyLjQ4LC0wLjQ2OCBhIDYuODYyLDYuODYyIDAgMCAwIDIuNTczLC0xLjM3MSA1LjE5LDUuMTkgMCAwIDAgMS4
0OTgsLTIuMTQ4IDUuOTEyLDUuOTEyIDAgMCAwIC0wLjAzLC00LjMyNCA0Ljg1Miw0Ljg1MiAwIDAgMCAtMS4zNDMsLTEuODYyIDUuNTY4LDUuNTY4IDAgMCAwIC0xLjk3LC0xLjE0NyBsIC0zLjI1LC0xLjIwNiBhIDEuNzQyLDEuNzQyIDAgMCAxIC0wLjg4NywtMC44NDUgMS4xMDcsMS4xMDcgMCAwIDEgMC4wMzYsLTAuOTg2IDEuMjksMS4yOSAwIDAgMSAwLjIxNywtMC4yOTEgMS43NSwxLjc1IDAgMCAxIDAuNDgsLTAuMzQ3IGMgMC4zNjMsLTAuMTggMC43NTUsLTAuM
jkzIDEuMTU4LC0wLjMzNyBhIDYuNzYsNi43NiAwIDAgMSAyLjA3MiwwLjAyMiBjIDAuODEsMC4wODggMS42MTMsMC4yMzEgMi40MDIsMC40MyBsIDAuMTY4LDAuMDM3IHYgLTMuOTc0IGwgLTAuMzEsLTAuMDY3IGEgMjEuMTQsMjEuMTQgMCAwIDAgLTIuNDQ0LC0wLjQzNSAxMi41NDUsMTIuNTQ1IDAgMCAwIC0zLjIxMywtMC4wMTQgNi45NDUsNi45NDUgMCAwIDAgLTMuNjk5LDEuNDg4IDQuOTA4LDQuOTA4IDAgMCAwIC0xLjU4LDIuMTc4IDUuOTg0LDUuOTg0IDAgMCA
wIC0wLjAwMyw0LjA1IGMgMC4yNDIsMC42NSAwLjYzLDEuMjM3IDEuMTM2LDEuNzE0IDAuNDM3LDAuNDIyIDAuOTMyLDAuNzggMS40NywxLjA2NSAwLjcwOCwwLjM4IDEuNDU4LDAuNjc1IDIuMjM1LDAuODc4IDAuMjU3LDAuMDc3IDAuNTEyLDAuMTU4IDAuNzY2LDAuMjQzIGwgMC4zODUsMC4xNDEgMC4xMSwwLjA0NSBjIDAuMzgsMC4xMzYgMC43MjYsMC4zNSAxLjAxOCwwLjYyOCAwLjIwMiwwLjE5IDAuMzU4LDAuNDIzIDAuNDU1LDAuNjgzIDAuMDYsMC4yMzcgMC4wN
jEsMC40ODUgMC4wMDMsMC43MjMgYSAxLjUzNiwxLjUzNiAwIDAgMSAtMC43NDQsMC44OTIgMy42OTEsMy42OTEgMCAwIDEgLTEuMjM5LDAuMzg3IDksOSAwIDAgMSAtMS45MiwwLjA5NyAyMS45NzMsMjEuOTczIDAgMCAxIC0yLjUwNywtMC4zMzQgYyAtMC40MzMsLTAuMDkgLTAuODY0LC0wLjE5IC0xLjI5MSwtMC4zMDMgeiBtIC0xMS4xNDQsNC40ODIgaCA0LjczIFYgMzcuODQ2IGggLTQuNzMgeiBNIDg1LjMwNSw0My4zODYgYSA0LjkzNCw0LjkzNCAwIDEgMSA2LjE
1Nyw3LjcxMSA0LjkzNCw0LjkzNCAwIDAgMSAtNi4xNTcsLTcuNzEgbSAtNi44NjcsMy44NDggYSA5Ljg3LDkuODcgMCAwIDAgMTIuMDAyLDkuNDg1IDkuNjI5LDkuNjI5IDAgMCAwIDMuMTU3LC0xNy43MjkgOS45MzQsOS45MzQgMCAwIDAgLTE1LjE2LDguMjQ0IiBpZD0icGF0aDIiLz48dXNlIGhyZWY9IiNiYXJfc2hvcnQiIHg9IjAiIGlkPSJ1c2UzIi8+PHVzZSBocmVmPSIjYmFyX3RhbGwiIHg9IjAiIGlkPSJ1c2U0Ii8+PHVzZSBocmVmPSIjYmFyX2dyYW5kZSIge
D0iMCIgaWQ9InVzZTUiLz48dXNlIGhyZWY9IiNiYXJfdGFsbCIgeD0iMjUuODc1IiBpZD0idXNlNiIvPjx1c2UgaHJlZj0iI2Jhcl9zaG9ydCIgeD0iNTEuNzUiIGlkPSJ1c2U3Ii8+PHVzZSBocmVmPSIjYmFyX3RhbGwiIHg9IjUxLjc1IiBpZD0idXNlOCIvPjx1c2UgaHJlZj0iI2Jhcl9ncmFuZGUiIHg9IjUxLjc1IiBpZD0idXNlOSIvPjx1c2UgaHJlZj0iI2Jhcl90YWxsIiB4PSI3Ny42MjUiIGlkPSJ1c2UxMCIvPjx1c2UgaHJlZj0iI2Jhcl9zaG9ydCIgeD0iMTAzLjM3NSIgaWQ9InVzZTExIi8+PC9nPjxtZXRhZGF0YSBpZD0ibWV0YWRhdGExMSI+PHJkZjpSREY+PGNjOldvcmsgcmRmOmFib3V0PSIiPjxkYzp0aXRsZT5DaXNjby5jb20gRnJhbmNlPC9kYzp0aXRsZT48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PC9zdmc+Cg==</data><downloads_count>0</downloads_count></logo>
<finalclass>Brand</finalclass>
<friendlyname>Cisco</friendlyname>
</Brand>
<Brand alias="Brand" id="5">
<name>Dell</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-dell.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiMwMjg4ZDEiIGQ9Ik0yNCw0QzEyLjk1Niw0LDQsMTIuOTU2LDQsMjRzOC45NTYsMjAsMjAsMjBzMjAtOC45NTYsMjAtMjBTMzUuMDQ0LDQsMjQs
NHogTTI0LDQxYy05LjM5MSwwLTE3LTcuNjA5LTE3LTE3UzE0LjYwOSw3LDI0LDdzMTcsNy42MDksMTcsMTdTMzMuMzkxLDQxLDI0LDQxeiIvPjxwYXRoIGZpbGw9IiMwMjg4ZDEiIGQ9Ik0zNS42NDEsMjUuNTYzbDIuODQsMC4wMDRsLTAuMDA0LDIuMzk1bC01LjY5MS0wLjAxMmwwLjAxMi04LjE3MmwyLjg1NSwwLjAwNEwzNS42NDEsMjUuNTYzeiBNMjYuMzQsMjUuMTAybC00LjY5OSwzLjY4NGwtNC4yODUtMy4zNzljLTAuNjIxLDEuNDg0LTIuMTA5LDIuNTItMy44Mz
YsMi41MTZsLTMuNjY0LTAuMDA0bDAuMDA4LTguMTcybDMuNjY4LDAuMDA0YzEuOTI2LDAuMDA0LDMuMzA5LDEuMjIzLDMuODI4LDIuNTMxbDQuMjk3LTMuMzY3bDEuNTg2LDEuMjVsLTMuOTM0LDMuMDg2bDAuNzU0LDAuNTk0bDMuOTM0LTMuMDg2bDEuNTksMS4yNTRsLTMuOTM0LDMuMDgybDAuNzUsMC41OTRsMy45NDEtMy4wODJsMC4wMDQtMi44MzZsMi44NTIsMC4wMDRsLTAuMDA4LDUuNzgxbDIuODQsMC4wMDRsLTAuMDA0LDIuMzkxbC01LjY5MS0wLjAwOEwyNi4zNCwyNS4xMDJ6IE0xNS4wMTIsMjMuODRjMC0xLjExMy0wLjczLTEuNzQyLTEuNzctMS43NDJoLTAuNjM3bC0wLjAwNCwzLjQ3N2gwLjYyMUMxNC4xODQsMjUuNTc0LDE1LjAxMiwyNS4wNTEsMTUuMDEyLDIzLjg0Ii8+PC9zdmc+Cg==</data><downloads_count>0</downloads_count></logo>
<finalclass>Brand</finalclass>
<friendlyname>Dell</friendlyname>
</Brand>
<Brand alias="Brand" id="6">
<name>HP Inc</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-hp.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0yNCA0QTIwIDIwIDAgMSAwIDI0IDQ0QTIwIDIwIDAgMSAwIDI0IDRaIi8+PHBhdGggZmlsbD0iIzE5NzZkMiIgZD0iTTI0LDQzLj
k5N2MtMC4xOTksMC0wLjY1MiwwLjAwNi0wLjg1LDBsNC0xMC45OTloNS42MjVjMC45ODcsMCwyLjA3MS0wLjc1OSwyLjQwOS0xLjY4Nmw0Ljc0OC0xMi42ODdjMC43MjUtMS45OTUtMC40MTctMy42MjYtMi41MzktMy42MjZoLTcuODA0bC02LjUxOCwxOC4yNTdoLTAuMDAybC0zLjcxMiwxMC4xOThDMTAuNTUsNDEuMzYxLDQsMzMuNDQ1LDQsMjMuOTk5YzAtOS4xNzQsNi4xNzgtMTYuOTA1LDE0LjYtMTkuMjYxbC0zLjgzLDEwLjUyNmgtMC4wMDFMOC4xNSwzMi45OTho
NC4yMzlsNS41NzYtMTQuOTk5aDMuMTg1bC01LjU3NiwxNC45OTlsMy45MTksMC4wMDFsNS40MzgtMTQuMzc0YzAuNzI2LTEuOTk1LTAuNDE2LTMuNjI2LTIuNTM2LTMuNjI2SDE5LjE1bDMuOTUxLTEwLjk3OEMyMy4zOTksNC4wMDgsMjMuNjk5LDQsMjQsNGMxMS4wNDYsMCwyMCw4Ljk1MywyMCwxOS45OTlTMzUuMDQ2LDQzLjk5NywyNCw0My45OTd6IE0zNi4xNSwxNy45OTloLTMuMTg1bC00LjUwOSwxMS45OTloMy4xODVMMzYuMTUsMTcuOTk5eiIvPjwvc3ZnPgo=</data><downloads_count>0</downloads_count></logo>
<finalclass>Brand</finalclass>
<friendlyname>HP Inc</friendlyname>
</Brand>
<Brand alias="Brand" id="7">
<name>HPE</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icon-hpe.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIgeG1sbnM9Imh0dHA6Ly93d3c
udzMub3JnLzIwMDAvc3ZnIiB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjAiIGlkPSJrYXRtYW5fMSIgeD0iMHB4IiB5PSIwcHgiIHZpZXdCb3g9IjAgMCA2MzAgNjMwIiB4bWw6c3BhY2U9InByZXNlcnZlIiBzb2RpcG9kaTpkb2NuYW1lPSJpY29uLWhwZS5zdmciIHdpZHRoPSI2MzAiIGhlaWdodD0iNjMwIiBpbmtzY2FwZTp2ZXJzaW9uPSIxLjQuMyAoMGQxNWY3NTA0MiwgMjAyNS0xMi0yNSkiPjxkZWZzIGlkPSJkZWZzM
iIvPjxzb2RpcG9kaTpuYW1lZHZpZXcgaWQ9Im5hbWVkdmlldzIiIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIgYm9yZGVyY29sb3I9IiMwMDAwMDAiIGJvcmRlcm9wYWNpdHk9IjAuMjUiIGlua3NjYXBlOnNob3dwYWdlc2hhZG93PSIyIiBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIiBpbmtzY2FwZTpwYWdlY2hlY2tlcmJvYXJkPSIwIiBpbmtzY2FwZTpkZXNrY29sb3I9IiNkMWQxZDEiIHNob3dndWlkZXM9InRydWUiIGlua3NjYXBlOnpvb209IjAuNjkwNTUxNSIgaW5rc2N
hcGU6Y3g9Ii0yMTcuOTQxNzUiIGlua3NjYXBlOmN5PSIyNzUuMTQyNCIgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIyNTYwIiBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMzYwIiBpbmtzY2FwZTp3aW5kb3cteD0iMCIgaW5rc2NhcGU6d2luZG93LXk9IjAiIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjEiIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9ImthdG1hbl8xIj48c29kaXBvZGk6Z3VpZGUgcG9zaXRpb249IjE5Ny42NjI0NCwzMTQuNDg4NDgiIG9yaWVudGF0a
W9uPSIwLC0xIiBpZD0iZ3VpZGUyIiBpbmtzY2FwZTpsb2NrZWQ9ImZhbHNlIi8+PC9zb2RpcG9kaTpuYW1lZHZpZXc+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyIgaWQ9InN0eWxlMSI+Cgkuc3Qwe2ZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MzY7fQoJLnN0MXtmaWxsOm5vbmU7c3Ryb2tlOiMwM0E4ODM7c3Ryb2tlLXdpZHRoOjM2O30KPC9zdHlsZT4KPHBhdGggY2xhc3M9InN0MCIgZD0ibSAxOC41MTE5ODcsNDA1LjkzMDEyIHYgLTE4MCBtIDE
1NC4wMDAwMDMsMTgwIHYgLTE4MCBtIC0xNTQuMDAwMDAzLDg5IEggMTU1LjUxMTk5IG0gOTUsOTEgdiAtMTgwIG0gMCwxOCBoIDEwMiBjIDI3LjYsMCA1MCwyMi40IDUwLDUwIDAsMjcuNiAtMjIuNCw1MCAtNTAsNTAgaCAtMTAyIG0gMjIyLC02NyB2IC0zMyBoIDE1OCIgaWQ9InBhdGgxIi8+CjxwYXRoIGNsYXNzPSJzdDEiIGQ9Im0gNjMwLjUxMTk5LDM4Ny45MzAxMiBoIC0xNTggdiAtNzYgaCAxNTgiIGlkPSJwYXRoMiIvPgo8L3N2Zz4K</data><downloads_count>0</downloads_count></logo>
<finalclass>Brand</finalclass>
<friendlyname>HPE</friendlyname>
</Brand>
<Brand alias="Brand" id="8">
<name>IBM</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-ibm.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiMzZjUxYjUiIGQ9Ik00OCAxNkw0OCAxNCA0MC40NjkgMTQgMzkuODA5IDE2ek0zMyAzMUwzMyAyOSAyOSAyOSAyNyAyOSAyNyAzMXpNMzkuNzY2I
DI4TDQwLjUxIDI2IDM0LjUxIDI2IDM1LjI2NiAyOHpNMjkgMjZIMzNWMjhIMjl6TTI5IDIzSDMzVjI1SDI5ek00MiAyOUw0MiAzMSA0OCAzMSA0OCAyOSA0NiAyOXpNMzYuMTgxIDE5TDM1LjUyMSAxNyAyNyAxNyAyNyAxOSAyOSAxOXpNMzcuMTcgMjJMMzYuNTEgMjAgMjkgMjAgMjkgMjJ6TTMzLjM3OCAyM0wzNC4xMzMgMjUgNDAuODgzIDI1IDQxLjYyOCAyM3pNNDIgMjNINDZWMjVINDJ6TTI3IDMySDMzVjM0SDI3ek0zNS4xOTEgMTZMMzQuNTMxIDE0IDI3IDE0IDI
3IDE2ek0zNi43NzYgMzJMMzcuNTMxIDM0IDM4LjI3NiAzMnpNNDIgMzJINDhWMzRINDJ6TTM5LjQ3OSAxN0wzOC44MTkgMTkgNDYgMTkgNDggMTkgNDggMTd6TTM5LjM5MyAyOUwzNS42NDMgMjkgMzYuMzk4IDMxIDM4LjY0OCAzMXpNNDIgMjZINDZWMjhINDJ6TTM4LjQ5IDIwTDM3LjgzIDIyIDQ2IDIyIDQ2IDIwek0wIDE0SDhWMTZIMHpNMCAxN0g4VjE5SDB6TTIgMjBINlYyMkgyek0yIDIzSDZWMjVIMnpNMiAyNkg2VjI4SDJ6TTAgMjlIOFYzMUgwek0wIDMySDhWM
zRIMHpNMTAgMTdIMThWMTlIMTB6TTI0Ljk3NyAxNmMtLjkxMy0xLjIwOC0yLjM0Ny0yLTMuOTc3LTJIMTB2Mmg3LjAyM0gyNC45Nzd6Ii8+PHBhdGggZmlsbD0iIzNmNTFiNSIgZD0iTTI1LjU3OCAxN2gtOS4xMzFDMTYuMTcxIDE3LjYxMyAxNiAxOC4yODMgMTYgMTloMTBDMjYgMTguMjg4IDI1Ljg0NiAxNy42MTMgMjUuNTc4IDE3ek0yMy45NzUgMjNIMTJ2MmgxMS45NzNjLS44MzMtLjYyLTEuODU0LTEtMi45NzMtMUMyMi4xMTkgMjQgMjMuMTQyIDIzLjYyMSAyMy4
5NzUgMjN6TTE3LjAyMyAzMkgxMHYyaDExYzEuNjMgMCAzLjA2NS0uNzkyIDMuOTc3LTJIMTcuMDIzek0xOCAyOWgtMi02djJoNi40NDdIMThoNy41NzhDMjUuODQ2IDMwLjM4NyAyNiAyOS43MTIgMjYgMjlIMTh6TTIxIDIwYzAgMCAwIC4wODMgMCAxcy0xIDEtMSAxaDQuOTc5Yy40NDEtLjU4NC43Ny0xLjI1Ny45MjEtMkgyMXpNMTIgMjBIMTdWMjJIMTJ6Ii8+PGc+PHBhdGggZmlsbD0iIzNmNTFiNSIgZD0iTTIxIDI4aDQuODg1Yy0uMTU2LS43MzgtLjQ2Ny0xLjQxOC0uOTA3LTJIMjBjMCAwIDEgLjE2NyAxIDFTMjEgMjggMjEgMjh6TTEyIDI2SDE3VjI4SDEyeiIvPjwvZz48L3N2Zz4K</data><downloads_count>0</downloads_count></logo>
<finalclass>Brand</finalclass>
<friendlyname>IBM</friendlyname>
</Brand>
<Brand alias="Brand" id="9">
<name>Lenovo</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-lenovo.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxyZWN0IHdpZHRoPSI0OCIgaGVpZ2h0PSIxNiIgeT0iMTYiIGZpbGw9IiNmZjE3NDQiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTEuOTM1LDI2LjA2MmMwLjM3LD
AuMzQxLDAuNzgyLDAuNDUsMS4yMjcsMC40NzhjMC42NDUsMC4wMzksMS4yMTctMC4xNTYsMS43MjQtMC41NDYgYzAuMTI0LTAuMDk2LDAuMTYzLTAuMTI2LDAuMTYzLTAuMTI2czAuNzc3LDAuNjA5LDEuMDk3LDAuODU1Yy0wLjI2MSwwLjMwOS0wLjU2NywwLjUyMS0wLjkwMSwwLjY5MiBjLTEuMTAzLDAuNTY1LTIuMjQ5LDAuNjg3LTMuNDI0LDAuMjY0Yy0xLjM0Ni0wLjQ4NS0yLjEzMS0xLjY5My0yLjA1OS0zLjExN2MwLjA3My0xLjQ0MSwxLjAwNS0yLjYwOCwyLjM5
Mi0yLjk0NCBjMS4wNjQtMC4yNTcsMi4wODQtMC4xNDMsMi45OCwwLjUzOWMwLjY1MSwwLjQ5NiwxLjAwMSwxLjI4LDEuMTQ3LDIuMDY4YzAsMC0yLjU5NSwxLjExLTMuNzk1LDEuNjA2IEMxMi4zMTMsMjUuOTAzLDEyLjE0MSwyNS45NzUsMTEuOTM1LDI2LjA2MnogTTExLjQ3MiwyNC45MjhjMC45NjktMC40MDEsMS45MzUtMC44MDEsMi45MDEtMS4yMDEgYy0wLjMzNC0wLjcyMi0xLjA5Ni0xLjAxNy0xLjg2Ni0wLjc0OEMxMS43NTgsMjMuMjQsMTEuMjgsMjQuMTI0LD
ExLjQ3MiwyNC45Mjh6Ii8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTI3LjA3NiwyNy45MzRjLTEuNjMyLTAuMDE3LTIuOTU2LTEuMDY4LTMuMjYxLTIuNmMtMC4zNy0xLjg1MiwwLjk2OS0zLjYzMywyLjg2LTMuODA4IGMxLjI4MS0wLjExOCwyLjM3MywwLjI1NCwzLjE1NSwxLjNjMS40NTEsMS45NDEsMC4yMiw0Ljc1Ny0yLjE5Myw1LjA1NUMyNy40NSwyNy45MDUsMjcuMjYzLDI3LjkxNywyNy4wNzYsMjcuOTM0eiBNMjcuMTEsMjYuNDM0IGMwLjkyOSwwLjAwMiwxLjYy
Mi0wLjcyMywxLjYyOC0xLjcwNGMwLjAwNi0wLjk3LTAuNzI0LTEuNzQyLTEuNjQ5LTEuNzQ1Yy0wLjkzNi0wLjAwMy0xLjYzMSwwLjczNC0xLjYyOCwxLjcyNiBDMjUuNDY1LDI1LjY3NSwyNi4xODksMjYuNDMyLDI3LjExLDI2LjQzNHoiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNDAuNjI0LDI3LjkzNGMtMS42MzItMC4wMTctMi45NTYtMS4wNjgtMy4yNjEtMi42Yy0wLjM3LTEuODUyLDAuOTY5LTMuNjMzLDIuODYtMy44MDggYzEuMjgxLTAuMTE4LDIuMzczLDAuMj
U0LDMuMTU1LDEuM2MxLjQ1MSwxLjk0MSwwLjIyLDQuNzU3LTIuMTkzLDUuMDU1QzQwLjk5OCwyNy45MDUsNDAuODExLDI3LjkxNyw0MC42MjQsMjcuOTM0eiBNNDAuNjU4LDI2LjQzNGMwLjkyOSwwLjAwMiwxLjYyMy0wLjcyMywxLjYyOC0xLjcwNGMwLjAwNi0wLjk3LTAuNzI0LTEuNzQyLTEuNjQ5LTEuNzQ1Yy0wLjkzNi0wLjAwMy0xLjYzMSwwLjczNC0xLjYyOCwxLjcyNiBDMzkuMDEzLDI1LjY3NSwzOS43MzcsMjYuNDMyLDQwLjY1OCwyNi40MzR6Ii8+PHBhdGgg
ZmlsbD0iI2ZmZiIgZD0iTTIxLjM3NywyNy45ODdjMC0wLjA5NSwwLjAwMS0yLjU4NywwLTMuNjYzYy0wLjAwMS0wLjc1OS0wLjYwNS0xLjMyNy0xLjQxNC0xLjMzMSBjLTAuNzk0LTAuMDA1LTEuMzgzLDAuNTYtMS4zODQsMS4zMzJjLTAuMDAxLDEuMDc2LDAsMy42NzksMCwzLjY3OWwtMS43NDEsMC4wMDJsMC4wMDctNi4zNzhjMCwwLDEuMTY4LTAuMDE4LDEuNzIyLTAuMDE4IGMwLDAuMjY0LTAuMDA2LDAuODMyLTAuMDA2LDAuODMyczAuMTM4LTAuMTI4LDAuMTgxLT
AuMTcxYzEuMTU3LTEuMTc0LDMuMjI2LTAuOTczLDQuMDMxLDAuMzkxIGMwLjIyMywwLjM3OCwwLjMzOCwwLjc4OSwwLjM0LDEuMjIzYzAuMDA4LDEuMjY0LDAuMDAzLDQuMTA0LDAuMDAzLDQuMTA0UzIxLjk2NCwyNy45ODcsMjEuMzc3LDI3Ljk4N3oiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMzAuMTY0LDIxLjYwOGMwLjY0MiwwLDEuOTY0LDAuMDE1LDEuOTY0LDAuMDE1czEuNDc4LDQuMDI0LDEuNTI2LDQuMTQ5IGMwLjExNS0wLjMxMSwxLjIwOC0zLjI5LDEuNTIx
LTQuMTc0YzAuNjQ2LDAuMDE4LDEuMjg4LDAuMDEsMS45NywwLjAxYy0wLjAyOSwwLjA4NC0yLjU2Miw2LjM5OC0yLjU2Miw2LjM5OGwtMS44NzYtMC4wMDMgQzMxLjkwNSwyNi4wNDIsMzAuMTkxLDIxLjY5MiwzMC4xNjQsMjEuNjA4eiIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik01Ljc2NywyNi4zOTNjMC4xMDQsMCwzLjg2OCwwLjAxMSwzLjg2OCwwLjAxMWwtMC4wMDIsMS41ODFMNCwyNy45ODl2LTguMDczaDEuNzcgQzUuNzcsMTkuOTE2LDUuNzY3LDI2LjI4NCw1Ljc2NywyNi4zOTN6Ii8+PC9zdmc+Cg==</data><downloads_count>0</downloads_count></logo>
<finalclass>Brand</finalclass>
<friendlyname>Lenovo</friendlyname>
</Brand>
<Brand alias="Brand" id="10">
<name>Razer</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-razer.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiM2NGRkMTciIGQ9Ik0yNiwyMGMwLjEsMCwwLjIsMCwwLjMsMGMwLjMtMC4xLDAuNS0wLjMsMC42LTAuNWMwLjEtMC4yLDAuMS0wLjUsMC4xLTA
uOGMtMC4yLTAuNi0xLTEuOC0yLTIuNCBjMCwwLjcsMCwxLjMsMCwxLjljMCwwLjUsMCwxLDAsMS4yYzAuMSwwLjIsMC4zLDAuNCwwLjUsMC42QzI1LjcsMjAsMjUuOCwyMCwyNiwyMCBNMjkuNCwyM2MwLjcsMCwxLjYtMC4xLDIuMy0wLjUgYy0wLjUtMC4zLTEtMC42LTEuNS0wLjljLTAuNC0wLjMtMC45LTAuNi0xLjEtMC42YzAsMC0wLjEsMC0wLjEsMGMtMC4yLDAtMC40LDAuMS0wLjYsMC4yYy0wLjIsMC4xLTAuNCwwLjQtMC40LDAuNiBjLTAuMSwwLjUsMC4zLDEuM
SwwLjgsMS4yQzI5LDIzLDI5LjIsMjMsMjkuNCwyMyBNMjMsMjQuOWMwLjUtMC4yLDEtMC40LDEuNS0wLjZjMC41LTAuMiwxLTAuNCwxLjItMC41YzAuMi0wLjIsMC4zLTAuNCwwLjMtMC43IGMwLTAuMy0wLjEtMC41LTAuMy0wLjdDMjUuNiwyMi4xLDI1LjMsMjIsMjUsMjJjLTAuMiwwLTAuNSwwLjEtMC43LDAuM0MyMy45LDIyLjYsMjMuMiwyMy44LDIzLDI0LjkgTTQxLjUsMSBjLTAuNywwLTEuNCwwLjUtMS41LDEuM2MwLDAsMCwwLDAsMEMzOS43LDIuMSwzOS40LDI
sMzksMmMtMS43LDAtMi45LDAuMi0zLjYsMC43QzM0LjUsMy4yLDM0LDQuMSwzNCw1YzAsMC4xLDAsMC4xLDAsMC4yIEMzMy42LDUuMSwzMy4xLDUsMzIuNyw1Yy0xLjMsMC0yLjQsMC43LTMuMSwyQzI5LjIsNy43LDI5LDguNCwyOSw5LjJjMCwyLjYsMi4yLDQuNiw0LjUsNi42YzIuNywyLjQsMy45LDMuOCwzLjMsNC44IGMtMC4yLDAuNC0wLjQsMC40LTAuNCwwLjRjLTAuMSwwLTAuMiwwLTAuMywwYy0wLjEsMC0wLjMsMC0wLjUtMC4xYzAtMC42LTAuMS0xLjEtMC4yL
TEuNmMtMC4yLTAuNi0wLjUtMS4yLTEtMS43IGMtMC40LTAuNS0xLTAuNy0xLjUtMC45Yy0wLjEsMC0wLjItMC4xLTAuNC0wLjFjLTAuMywwLTAuNywwLjItMC44LDAuNWMtMC4zLDAuNC0wLjIsMC45LDAuMiwxLjNjMC4yLDAuMiwwLjQsMC40LDAuNSwwLjYgYzAuMSwwLjIsMC4xLDAuMywwLjEsMC41Yy0wLjItMC4xLTAuNS0wLjMtMC43LTAuNGMtMC44LTAuNS0xLjQtMC45LTEuOS0xYzAtMC4xLTAuMS0wLjItMC4xLTAuM2MtMC41LTEuNS0yLjEtMy43LTQuMy00LjU
gYy0xLTMuNS0zLjMtNC4zLTUuMS00LjNjLTQuNiwwLTUuNCw0LjktNiw4LjVjLTAuMiwxLjQtMC43LDQtMS4zLDQuNWMwLTAuMi0wLjEtMC41LTAuMS0xQzEzLDE5LjksMTIuNywxNywxMCwxNyBjLTEuNiwwLTIuNCwxLjEtMi44LDEuN2MtMC4yLTAuMi0wLjMtMC40LTAuNC0wLjZjLTAuMy0wLjYtMS0xLjEtMS43LTEuMUM1LjEsMTYuOSw1LDE2LjcsNSwxNi41QzUsMTUuNyw0LjMsMTUsMy41LDE1IGMtMC43LDAtMS4yLDAuNC0xLjQsMS4xQzEuNCwxNi4zLDEsMTYuO
CwxLDE3LjVDMSwxOC4zLDEuNywxOSwyLjUsMTljMC4xLDAsMC4yLDAsMC4zLDBDMi45LDE5LjEsMywxOS4xLDMsMTkuMiBjMCwwLjIsMC4xLDAuNSwwLjIsMC43YzEsMS45LDIuNSwyLjcsMywyLjlDNi42LDIyLjksNywyMyw3LjQsMjNjMC43LDAsMS4zLTAuMiwxLjgtMC41QzkuNSwyNCwxMC4zLDI2LDEzLDI2IGM0LjEsMCw0LjktNC42LDUuNC03LjljMC42LTMuNCwxLTUuMSwyLTUuMWMwLjMsMCwwLjcsMCwxLjEsMC45Yy0wLjQsMC4yLTAuNywwLjUtMSwwLjhjLTA
uNSwwLjUtMC44LDEuMS0xLDEuNyBjLTAuMiwwLjYtMC4xLDEuMi0wLjEsMS43YzAuMSwwLjUsMC41LDAuOCwwLjksMC45YzAsMCwwLDAsMC4xLDBjMC41LDAsMC45LTAuMywxLTAuOGMwLjEtMC4zLDAuMi0wLjYsMC4zLTAuOCBjMC4xLTAuMSwwLjItMC4yLDAuMy0wLjNjMCwwLjMsMCwwLjYsMCwwLjljMCwwLjksMCwxLjUsMC4xLDJjLTEsMS0yLjIsMy0yLjIsNS4yYy0xLjMsMC45LTIuMiwxLjktMi43LDNjLTAuNSwxLjItMC41LDIuNSwwLjEsMy43IGMwLjgsMS45L
DIuNSwyLjksNC45LDIuOWMxLjgsMCwzLjgtMC41LDUuNy0xLjFjMS43LTAuNSwzLjMtMC45LDQuMy0wLjljMC4yLDAsMC40LDAsMC42LDAuMWMtMC4yLDAuMS0wLjUsMC4zLTEsMC42IGMtMS4xLDAuNi0zLjYsMi0yLjUsNC41YzAuNywxLjYsMi41LDEuOCwzLjYsMS44bDAuMSwwYzAsMCwwLDAsMCwwYy0wLjEsMC4yLTAuMiwwLjMtMC40LDAuNmMtMC40LDAuNC0wLjYsMC44LTAuNiwxLjMgYy0wLjEsMC0wLjMsMC4xLTAuNSwwLjFjLTAuOCwwLTEuNSwwLjctMS41LDE
uNWMwLDAuNywwLjQsMS4yLDEuMSwxLjRjMC4yLDAuNiwwLjgsMS4xLDEuNCwxLjFjMC44LDAsMS41LTAuNywxLjUtMS41bDAtMC4zIGMwLjEtMC4xLDAuMS0wLjEsMC4yLTAuMmMwLjUsMCwwLjktMC4yLDEuMi0wLjZjMS43LTEuNywxLjYtMy4zLDEuNi00LjFjMC0xLjMtMC45LTIuNC0yLTIuOWMxLjMtMC45LDIuNy0yLjUsMS42LTQuOCBjLTAuOC0xLjctMi4yLTIuNi00LjQtMi42Yy0xLjcsMC0zLjUsMC41LTUuMywxYy0xLjgsMC41LTMuNCwwLjktNC42LDAuOWMtM
SwwLTEuMS0wLjMtMS4yLTAuNUMyMSwzMC4xLDIxLDI5LjksMjEsMjkuOCBjMC4xLTAuMiwwLjMtMC41LDAuOC0wLjljMC40LDAuNCwwLjksMC42LDEuMywwLjhjMC40LDAuMiwwLjksMC4zLDEuNCwwLjNjMC4yLDAsMC40LDAsMC42LTAuMWMwLjYtMC4xLDEuMi0wLjQsMS42LTAuNyBjMC40LTAuMywwLjYtMC44LDAuNC0xLjJjLTAuMS0wLjQtMC41LTAuNi0wLjktMC42Yy0wLjEsMC0wLjEsMC0wLjIsMGMtMC4yLDAtMC40LDAuMS0wLjYsMC4xYy0wLjEsMC0wLjIsMC0
wLjIsMCBjLTAuMiwwLTAuMy0wLjEtMC40LTAuMmMwLjItMC4xLDAuNS0wLjIsMC43LTAuM2MxLTAuNCwxLjYtMC42LDItMWMwLjEtMC4xLDAuMS0wLjEsMC4yLTAuMmMwLjEsMCwwLjMsMC4xLDAuNCwwLjEgQzI4LjYsMjYsMjksMjYsMjkuNCwyNmMwLjksMCwyLjktMC4xLDQuNS0xLjRjMC44LDAuMiwxLjUsMC40LDIuMiwwLjRjMC40LDAsMC45LTAuMSwxLjMtMC4yYzEuMi0wLjMsMi4zLTEuMSwyLjktMi4zIGMyLjItMy45LTEuMy03LjEtNC4yLTkuN0MzNC45LDExL
jcsMzMsMTAsMzMsOS4xYzAsMCwwLDAsMC0wLjFjMC4yLDAuMSwwLjQsMC4xLDAuOCwwLjNjMC42LDAuMiwxLjUsMC42LDIuNCwwLjYgYzEuMSwwLDEuOS0wLjUsMi41LTEuNWMwLjYtMSwwLjQtMS45LDAuMS0yLjVjMC4xLDAsMC4yLDAsMC4zLDBjMC42LDAsMS4yLTAuMywxLjYtMC44QzQxLjMsNS4xLDQyLDUsNDIuNSw1QzQzLjMsNSw0NCw0LjMsNDQsMy41IGMwLTAuNy0wLjQtMS4yLTEuMS0xLjRDNDIuNywxLjQsNDIuMiwxLDQxLjUsMUw0MS41LDF6Ii8+PHBhdGg
gZD0iTTEzLDI1Yy0yLjYsMC0yLjgtMi41LTIuOS0zLjhjMC0wLjMtMC4xLTAuOS0wLjItMS4xYy0wLjEsMC4xLTAuMSwwLjItMC4yLDAuM2MtMC4xLDAuMS0wLjIsMC4zLTAuMywwLjUgYy0wLjYsMS0xLjksMS40LTIuOSwxYy0wLjgtMC4zLTEuOC0xLjEtMi41LTIuNGMtMC4yLTAuNSwwLTEuMSwwLjQtMS4zYzAuNS0wLjIsMS4xLDAsMS4zLDAuNGMwLjQsMC43LDEsMS4zLDEuNCwxLjQgYzAuMiwwLjEsMC40LDAsMC41LTAuMmMwLjEtMC4xLDAuMi0wLjMsMC4yLTAuN
GMwLjMtMC42LDAuOC0xLjQsMi0xLjRjMS44LDAsMiwxLjksMi4xLDNjMC4yLDEuOCwwLjQsMiwwLjksMiBjMS41LDAsMS45LTIuMSwyLjUtNS40YzAuNi0zLjQsMS4zLTcuNiw1LTcuNmMyLjcsMCwzLjgsMS45LDQuMiw0LjFjMi4xLDAuNCwzLjcsMi43LDQuMSw0YzAuMiwwLjgsMC4yLDEuNi0wLjIsMi4zcy0xLDEuMi0xLjcsMS41IGMtMC44LDAuMi0xLjYsMC4yLTIuMy0wLjJjLTAuNy0wLjQtMS4yLTEtMS41LTEuN0MyMywxOS42LDIzLDE5LjEsMjMsMTguMmMwLTM
uNy0wLjQtNi4yLTIuNi02LjJjLTEuOSwwLTIuNCwyLjQtMyw2IEMxNi45LDIxLjMsMTYuMywyNSwxMywyNXogTTI1LDE2LjNjMCwwLjcsMCwxLjMsMCwxLjljMCwwLjUsMCwxLDAsMS4yYzAuMSwwLjIsMC4zLDAuNCwwLjUsMC42YzAuMiwwLjEsMC41LDAuMSwwLjgsMC4xIGMwLjMtMC4xLDAuNS0wLjMsMC42LTAuNWMwLjEtMC4yLDAuMS0wLjUsMC4xLTAuOEMyNi44LDE4LjEsMjYsMTYuOSwyNSwxNi4zeiIvPjxwYXRoIGQ9Ik0yNCAxNmMtLjMtLjEtLjctLjEtMS4xL
S4xLS40IDAtLjcuMS0xLjEuMi0uMy4xLS42LjQtLjkuNy0uMy4zLS40LjctLjUgMS4xLS4xLS40LS4xLS45IDAtMS40LjEtLjUuNC0uOS43LTEuMy40LS40LjgtLjcgMS4yLS45LjUtLjIuOS0uNCAxLjUtLjVsLjEgMGMuNi0uMSAxLjEuMyAxLjIuOFMyNSAxNS45IDI0LjUgMTZDMjQuMyAxNiAyNC4xIDE2IDI0IDE2ek0zLjUgMTguMWMtLjEgMC0uMSAwLS4yIDBDMy4xIDE4IDIuOCAxOCAyLjUgMTggMi4yIDE4IDIgMTcuOCAyIDE3LjVTMi4yIDE3IDIuNSAxN2MuNSA
wIC44IDAgMS4yLjJDNCAxNy4zIDQuMSAxNy42IDQgMTcuOCAzLjkgMTggMy43IDE4LjEgMy41IDE4LjF6Ii8+PHBhdGggZD0iTTQuNSAxOWMtLjEgMC0uMSAwLS4yLS4xLS4xIDAtMS4zLS43LTEuMy0yLjRDMyAxNi4yIDMuMiAxNiAzLjUgMTZTNCAxNi4yIDQgMTYuNWMwIDEuMS43IDEuNS43IDEuNi4yLjEuMy40LjIuN0M0LjkgMTguOSA0LjcgMTkgNC41IDE5ek0zNCA0M2MtLjMgMC0uNS0uMS0uNy0uMy0uNC0uNC0uNC0xIDAtMS40LjctLjcuNy0xLjQuNy0xLjggM
C0uMi0uMi0uMy0uNC0uNC0uMiAwLS40IDAtLjctLjEtMS0uMS0yLjMtLjItMi44LTEuMi0uNy0xLjcgMS0yLjYgMi4xLTMuMiAxLjctLjkgMS45LTEuMiAxLjYtMS44LS42LTEuMy0yLjgtLjgtNi4yLjEtMy43IDEtNy44IDIuMi05LjMtMS4zLS40LTEtLjUtMi0uMS0yLjkuNS0xLjIgMS42LTIuMSAyLjgtMi44LS4zLTIuMSAxLTQuMyAyLTUuMSAxLjItMS4xIDMuMS0xIDQuMi4yIDEuMSAxLjIgMSAzLjEtLjIgNC4yLS4zLjMtLjguNS0xLjcuOS0uNi4zLTEuNS42LTI
uMyAxLS4xLjEtLjIuMi0uMy4yLS4xIDAtLjEuMS0uMi4xLTEgLjYtMiAxLjMtMi4zIDIuMS0uMi40LS4yLjkuMSAxLjQuOCAxLjcgMy4zIDEuMiA3IC4yIDMuNC0uOSA3LjItMiA4LjYgMSAxIDIuMy0xLjIgMy42LTIuNSA0LjMtLjMuMi0uOC40LTEgLjYuMyAwIC42LjEuOS4xLjMgMCAuNiAwIC44LjEgMS4yLjIgMi4xIDEuMiAyLjEgMi4zIDAgLjYuMSAyLTEuMyAzLjNDMzQuNSA0Mi45IDM0LjMgNDMgMzQgNDN6TTI1IDIyYy0uMiAwLS41LjEtLjcuMy0uNC40LTEuM
iAxLjUtMS4zIDIuNi41LS4yIDEtLjQgMS41LS42LjUtLjIgMS0uNCAxLjItLjUuNC0uMy40LTEgMC0xLjRDMjUuNSAyMi4xIDI1LjMgMjIgMjUgMjJ6Ii8+PHBhdGggZD0iTTIzLjIgMjYuMWMwIC4zLjIuNy4zIDEgLjIuMy4zLjYuNi45LjIuMy42LjQgMSAuNS40LjEuOC4xIDEuMiAwLS40LjItLjguNS0xLjIuNS0uNS4xLTEgMC0xLjUtLjItLjUtLjItLjktLjUtMS4zLS44LS40LS40LS43LS43LTEtMS4ybC0uMS0uMWMtLjMtLjUtLjEtMS4xLjQtMS40LjUtLjMgMS4
xLS4xIDEuNC40QzIzLjIgMjUuOCAyMy4yIDI1LjkgMjMuMiAyNi4xek0zMi41IDQ1Yy0uMyAwLS41LS4yLS41LS41bDAtLjVjMC0uMiAwLS40IDAtLjYgMC0uMy4zLS41LjUtLjUuMyAwIC41LjIuNS41IDAgLjIgMCAuMyAwIC41bDAgLjVDMzMgNDQuOCAzMi44IDQ1IDMyLjUgNDV6Ii8+PHBhdGggZD0iTTMxLjUgNDRjLS4zIDAtLjUtLjItLjUtLjVzLjItLjUuNS0uNWMxLjEgMCAxLjUtLjcgMS42LS43LjEtLjIuNC0uMy43LS4yLjIuMS4zLjQuMi43QzMzLjkgNDIuO
CAzMy4zIDQ0IDMxLjUgNDR6TTI5LjQgMjVjLS40IDAtLjcgMC0xLS4xLTEuNi0uMy0yLjctMS45LTIuNC0zLjUuMi0uOC42LTEuNSAxLjMtMS45LjctLjQgMS41LS42IDIuMy0uNS40LjEuOC4zIDEuNy45LjYuNCAxLjQuOCAyLjIgMS4yIDAgMCAwIDAgMCAwIC4yIDAgLjMgMCAuNS4xLjEuMS4yLjEuMy4yLjkuNCAxLjguNiAyLjQuNC40LS4xLjgtLjQgMS4xLS45IDEtMS43LS44LTMuNS0zLjUtNkMzMiAxMy4xIDMwIDExLjMgMzAgOS4yYzAtLjYuMS0xLjIuNC0xLjd
DMzEuMSA2LjMgMzIgNiAzMi43IDZjLjggMCAxLjYuMyAyLjMuNi4yLjEuNi4yLjguMyAwIDAgMCAwIDAgMC0uMi0uMi0uMy0uNC0uNS0uN0MzNS4xIDUuOCAzNSA1LjQgMzUgNWMwLS42LjMtMS4xLjktMS41QzM2LjYgMy4xIDM4LjEgMyAzOSAzYy42IDAgMSAuNCAxIDFzLS40IDEtMSAxYy0uOCAwLTEuNi4xLTIgLjIgMCAwIDAgMCAwIDAgLjEuMi4yLjQuMy41QzM3LjggNi4yIDM4LjMgNyAzNy44IDhjLS45IDEuNi0yLjUuOS0zLjYuNEMzMy43IDguMiAzMy4xIDggM
zIuNyA4Yy0uMSAwLS4zIDAtLjYuNEMzMiA4LjYgMzIgOC45IDMyIDkuMWMwIDEuMyAxLjggMi45IDMuNSA0LjQgMi43IDIuNCA1LjcgNS4yIDQgOC40LS41IDEtMS4zIDEuNi0yLjMgMS45LTEuMS4zLTIuMy4xLTMuNC0uNEMzMi42IDI0LjYgMzAuOCAyNSAyOS40IDI1ek0yOSAyMWMtLjIgMC0uNC4xLS42LjItLjIuMS0uNC40LS40LjYtLjEuNS4zIDEuMS44IDEuMi41LjEgMS45LjEgMi45LS41LS41LS4zLTEtLjYtMS41LS45LS40LS4zLS45LS42LTEuMS0uNkMyOS4
xIDIxIDI5LjEgMjEgMjkgMjF6Ii8+PHBhdGggZD0iTTMyLjYgMjEuN2MuMi0uMi40LS42LjYtLjkuMi0uMy4zLS43LjMtMS4xIDAtLjQgMC0uNy0uMi0xLjEtLjEtLjQtLjQtLjctLjgtMSAuNC4yLjguMyAxLjIuNy4zLjMuNi44LjggMS4zLjEuNS4yIDEgLjIgMS41IDAgLjUtLjEgMS0uMyAxLjVsMCAuMWMtLjIuNS0uOC44LTEuMy42cy0uOC0uOC0uNi0xLjNDMzIuNCAyMS45IDMyLjUgMjEuOCAzMi42IDIxLjd6TTQwLjggNC4xYy0uMiAwLS40LS4yLS41LS40IDAtL
jMuMS0uNS40LS42LjEgMCAuMi0uMi4yLS42QzQxIDIuMiA0MS4yIDIgNDEuNSAyUzQyIDIuMiA0MiAyLjVDNDIgMy4zIDQxLjUgNCA0MC44IDQuMSA0MC45IDQuMSA0MC45IDQuMSA0MC44IDQuMXoiLz48cGF0aCBkPSJNMzksNC41Yy0wLjIsMC0wLjQtMC4yLTAuNS0wLjRjLTAuMS0wLjMsMC4xLTAuNSwwLjQtMC42QzM5LDMuNSw0MSwzLDQyLjUsM0M0Mi44LDMsNDMsMy4yLDQzLDMuNVM0Mi44LDQsNDIuNSw0IGMtMS40LDAtMy40LDAuNS0zLjQsMC41QzM5LjEsNC41LDM5LDQuNSwzOSw0LjV6Ii8+PC9zdmc+Cg==</data><downloads_count>0</downloads_count></logo>
<finalclass>Brand</finalclass>
<friendlyname>Razer</friendlyname>
</Brand>
<Brand alias="Brand" id="11">
<name>Samsung</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-samsung.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiMwZDQ3YTEiIGQ9Ik00Ny45NywxOS44MjZjMC42NTQsMy43NDctOS41NDcsOC42NTUtMjIuNzg4LDEwLjk2Yy0xMy4yMzgsMi4zMDYtMjQuN
SwxLjEzNi0yNS4xNTMtMi42MTMgYy0wLjY1My0zLjc0Nyw5LjU1MS04LjY1NCwyMi43OS0xMC45NThDMzYuMDU5LDE0LjkwNyw0Ny4zMTgsMTYuMDc4LDQ3Ljk3LDE5LjgyNnoiLz48cG9seWdvbiBmaWxsPSIjZmFmYWZhIiBwb2ludHM9IjM1LjUxLDI1LjM4OCAzNS40NDIsMjEuNDkyIDM2LjY3MSwyMS40OTIgMzYuNjcxLDI2LjQwMyAzNC45MDUsMjYuNDAzIDMzLjY3OCwyMi4zNzMgMzMuNjUyLDIyLjM3MyAzMy43MiwyNi40MDMgMzIuNDk5LDI2LjQwMyAzMi40OTk
sMjEuNDkyIDM0LjM0MiwyMS40OTIgMzUuNDgzLDI1LjM4OCIvPjxwb2x5Z29uIGZpbGw9IiNmYWZhZmEiIHBvaW50cz0iMTMuMTc3LDIxLjk1MiAxMi40OTcsMjYuNDU1IDExLjE1NywyNi40NTUgMTIuMDc2LDIxLjQ5MiAxNC4yODQsMjEuNDkyIDE1LjIwMSwyNi40NTUgMTMuODY1LDI2LjQ1NSAxMy4yMDQsMjEuOTUyIi8+PHBvbHlnb24gZmlsbD0iI2ZhZmFmYSIgcG9pbnRzPSIxOC45NjQsMjUuMjg2IDE5LjU3NywyMS40OTIgMjEuNjAxLDIxLjQ5MiAyMS43MDgsM
jYuNDU1IDIwLjQ2OCwyNi40NTUgMjAuNDM1LDIxLjk5MyAyMC40MDksMjEuOTkzIDE5LjU4LDI2LjQ1NSAxOC4zMjEsMjYuNDU1IDE3LjQ5LDIxLjk5MyAxNy40NjQsMjEuOTkzIDE3LjQzMywyNi40NTUgMTYuMTksMjYuNDU1IDE2LjMsMjEuNDkyIDE4LjMyNSwyMS40OTIgMTguOTM3LDI1LjI4NiIvPjxwYXRoIGZpbGw9IiNmYWZhZmEiIGQ9Ik05LjA2NywyNS4wNTVjMC4wNDksMC4xMiwwLjAzNCwwLjI3NSwwLjAxMSwwLjM2OGMtMC4wNDIsMC4xNjUtMC4xNTQsMC4
zMzMtMC40ODMsMC4zMzMgYy0wLjMxMiwwLTAuNS0wLjE3OS0wLjUtMC40NXYtMC40OEg2Ljc2M0w2Ljc2MiwyNS4yMWMwLDEuMTA2LDAuODcxLDEuNDQxLDEuODA1LDEuNDQxYzAuODk4LDAsMS42MzctMC4zMDcsMS43NTQtMS4xMzQgYzAuMDYxLTAuNDI5LDAuMDE1LTAuNzA5LTAuMDA1LTAuODE2Yy0wLjIwOS0xLjAzOS0yLjA5My0xLjM0OS0yLjIzMy0xLjkzYy0wLjAyNC0wLjA5OS0wLjAxNy0wLjIwNS0wLjAwNS0wLjI2MiBjMC4wMzUtMC4xNTgsMC4xNDMtMC4zM
zIsMC40NTMtMC4zMzJjMC4yOSwwLDAuNDYxLDAuMTgsMC40NjEsMC40NWMwLDAuMDkxLDAsMC4zMDcsMCwwLjMwN2gxLjIzN3YtMC4zNDggYzAtMS4wODEtMC45Ny0xLjI1LTEuNjczLTEuMjVjLTAuODgzLDAtMS42MDQsMC4yOTItMS43MzYsMS4wOTljLTAuMDM2LDAuMjIzLTAuMDQxLDAuNDIyLDAuMDExLDAuNjcxIEM3LjA0OSwyNC4xMTgsOC44MTEsMjQuNDEyLDkuMDY3LDI1LjA1NXoiLz48cGF0aCBmaWxsPSIjZmFmYWZhIiBkPSJNMjUuMjA0LDI1LjA0NmMwLjA
0OSwwLjExOSwwLjAzMywwLjI3LDAuMDExLDAuMzYzYy0wLjA0MSwwLjE2NS0wLjE1MiwwLjMzLTAuNDc5LDAuMzMgYy0wLjMwNywwLTAuNDk0LTAuMTc5LTAuNDk0LTAuNDQ0bC0wLjAwMS0wLjQ3NmgtMS4zMThsLTAuMDAyLDAuMzc5YzAsMS4wOTUsMC44NjMsMS40MjYsMS43ODcsMS40MjYgYzAuODg4LDAsMS42Mi0wLjMwMywxLjczNi0xLjEyMmMwLjA2MS0wLjQyNiwwLjAxOC0wLjcwMi0wLjAwNC0wLjgwN2MtMC4yMDgtMS4wMjktMi4wNzMtMS4zMzYtMi4yMTEtM
S45MTIgYy0wLjAyNC0wLjA5OS0wLjAxNy0wLjIwMy0wLjAwNS0wLjI1N2MwLjAzNi0wLjE2LDAuMTQyLTAuMzI5LDAuNDQ5LTAuMzI5YzAuMjg4LDAsMC40NTUsMC4xNzUsMC40NTUsMC40NDQgYzAsMC4wOSwwLDAuMzA0LDAsMC4zMDRoMS4yMjh2LTAuMzQ1YzAtMS4wNy0wLjk2Mi0xLjIzNy0xLjY1OS0xLjIzN2MtMC44NzMsMC0xLjU4OCwwLjI4OC0xLjcxNywxLjA5IGMtMC4wMzYsMC4yMi0wLjA0LDAuNDE1LDAuMDEyLDAuNjYzQzIzLjIwNiwyNC4xMTgsMjQuOTU
xLDI0LjQxLDI1LjIwNCwyNS4wNDZ6Ii8+PHBhdGggZmlsbD0iI2ZhZmFmYSIgZD0iTTI5LjM3MiwyNS43MTNjMC4zNDQsMCwwLjQ1MS0wLjIzOCwwLjQ3NS0wLjM2YzAuMDEtMC4wNTQsMC4wMTMtMC4xMjUsMC4wMTItMC4xOVYyMS40OWgxLjI1NSB2My41NmMwLjAwMywwLjA5MS0wLjAwNiwwLjI3OS0wLjAxMSwwLjMyNWMtMC4wODgsMC45MjctMC44MjEsMS4yMjctMS43MzIsMS4yMjdjLTAuOTEzLDAtMS42NDYtMC4zMDEtMS43MzMtMS4yMjcgYy0wLjAwNC0wLjA0N
y0wLjAxMy0wLjIzNS0wLjAxMS0wLjMyNXYtMy41NmgxLjI1NHYzLjY3MmMwLDAuMDY0LDAuMDAyLDAuMTM3LDAuMDEyLDAuMTkgQzI4LjkyMSwyNS40NzMsMjkuMDI1LDI1LjcxMywyOS4zNzIsMjUuNzEzeiIvPjxwYXRoIGZpbGw9IiNmYWZhZmEiIGQ9Ik0zOS43MjUsMjUuNjZjMC4zNTksMCwwLjQ4NS0wLjIyNywwLjUwOC0wLjM1OWMwLjAwOS0wLjA1NywwLjAxMi0wLjEyNiwwLjAxMS0wLjE4OXYtMC43MiBoLTAuNTA5di0wLjcyNGgxLjc2VjI1Yy0wLjAwMSwwLjA
5My0wLjAwMywwLjE2Mi0wLjAxOCwwLjMyN2MtMC4wODIsMC45MDMtMC44NjYsMS4yMjUtMS43NDUsMS4yMjUgYy0wLjg4MSwwLTEuNjYzLTAuMzIyLTEuNzQ3LTEuMjI1Yy0wLjAxNC0wLjE2Ni0wLjAxNi0wLjIzNC0wLjAxOC0wLjMyN2wwLjAwMS0yLjA4OWMwLTAuMDg4LDAuMDExLTAuMjQ0LDAuMDIxLTAuMzI3IGMwLjExLTAuOTI4LDAuODYyLTEuMjI2LDEuNzQzLTEuMjI2YzAuODgsMCwxLjY1MSwwLjI5NywxLjc0MiwxLjIyNmMwLjAxNiwwLjE1OCwwLjAxMSwwL
jMyNywwLjAxMSwwLjMyN3YwLjE2NmgtMS4yNTF2LTAuMjc4IGMwLjAwMSwwLjAwMS0wLjAwMi0wLjExOC0wLjAxNi0wLjE4OWMtMC4wMjEtMC4xMS0wLjExNi0wLjM2Mi0wLjQ5NS0wLjM2MmMtMC4zNjIsMC0wLjQ2NywwLjIzOC0wLjQ5NCwwLjM2MiBjLTAuMDE1LDAuMDY1LTAuMDIxLDAuMTU0LTAuMDIxLDAuMjM0djIuMjdjLTAuMDAxLDAuMDYzLDAuMDAzLDAuMTMyLDAuMDEzLDAuMTg5QzM5LjI0MSwyNS40MzMsMzkuMzY2LDI1LjY2LDM5LjcyNSwyNS42NnoiLz48L3N2Zz4K</data><downloads_count>0</downloads_count></logo>
<finalclass>Brand</finalclass>
<friendlyname>Samsung</friendlyname>
</Brand>
<Brand alias="Brand" id="12">
<name>Sony</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icon-sony.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmc6c3ZnIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIiB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiIHhtbG5zOnN2Zz0iaH
R0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjgwIDEyODAiIHZlcnNpb249IjEuMSIgaWQ9InN2ZzEiIHNvZGlwb2RpOmRvY25hbWU9Imljb24tc29ueS5zdmciIGlua3NjYXBlOnZlcnNpb249IjEuNC4zICgwZDE1Zjc1MDQyLCAyMDI1LTEyLTI1KSIgd2lkdGg9IjEyODAiIGhlaWdodD0iMTI4MCI+PHNvZGlwb2RpOm5hbWVkdmlldyBpZD0ibmFtZWR2aWV3MSIgcGFnZWNvbG9yPSIjZmZmZmZmIiBib3JkZXJjb2xvcj0iIzAwMDAwMCIgYm9y
ZGVyb3BhY2l0eT0iMC4yNSIgaW5rc2NhcGU6c2hvd3BhZ2VzaGFkb3c9IjIiIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiIGlua3NjYXBlOnBhZ2VjaGVja2VyYm9hcmQ9IjAiIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIgaW5rc2NhcGU6em9vbT0iMC42MzQxODYzOSIgaW5rc2NhcGU6Y3g9Ii00NjIuNzk3NjkiIGlua3NjYXBlOmN5PSIyMjUuNDg1NzYiIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMjU2MCIgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTM2MC
IgaW5rc2NhcGU6d2luZG93LXg9IjAiIGlua3NjYXBlOndpbmRvdy15PSIwIiBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIiBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJnNCIgc2hvd2d1aWRlcz0idHJ1ZSI+PHNvZGlwb2RpOmd1aWRlIHBvc2l0aW9uPSIxNzMuMzcxMDgsNjQxLjg5MDI0IiBvcmllbnRhdGlvbj0iMCwtMSIgaWQ9Imd1aWRlMSIgaW5rc2NhcGU6bG9ja2VkPSJmYWxzZSIvPjwvc29kaXBvZGk6bmFtZWR2aWV3Pjxzdmc6ZGVmcyBpZD0iZGVmczEi
Lz48c3ZnOmcgaWQ9IkxheWVyXzIiPjxzdmc6ZyBpZD0iZzQiIGlua3NjYXBlOmxhYmVsPSJsb2dvIj48c3ZnOnBhdGggZD0ibSA0NTYuMDEyOSw3NTEuNzQ2ODcgYyAtNDYuNDEyNzksMCAtODkuNDIwNzksLTEzLjgyMzk5IC0xMTguMDkyNzksLTM5LjYwMzE5IGEgOTYuODk1OTg5LDk2Ljg5NTk4OSAwIDAgMSAtMzIuMTI3OTksLTczLjE5MDM5IDk4LjMyOTU4OCw5OC4zMjk1ODggMCAwIDEgMzIuMTI3OTksLTcyLjkzNDQgYyAyNi42MjQsLTI0LjE5MTk5IDczLjcyOC
wtMzkuNTUxOTkgMTE4LjA5Mjc5LC0zOS41NTE5OSA0OS4wNzUxOSwwIDg4LjM3MTE5LDEyLjM2NDggMTE4LjM5OTk5LDM5LjYwMzE5IGEgOTcuNDA3OTg5LDk3LjQwNzk4OSAwIDAgMSAzMS42OTI3OSw3Mi44ODMyIDEwMS41Mjk1OSwxMDEuNTI5NTkgMCAwIDEgLTMxLjY5Mjc5LDczLjE5MDM5IGMgLTI3Ljk4MDgsMjUuOTMyOCAtNzEuOTEwNCwzOS42MDMxOSAtMTE4LjM5OTk5LDM5LjYwMzE5IHYgLTI5LjY0NDc5IGMgMjQuNjAxNiwwIDQ3LjQzNjc5LC04LjQ5OTIg
NjMuMzg1NTksLTI0LjM3MTIgMTUuOTQ4OCwtMTUuODcyIDIzLjE5MzYsLTM1LjEyMzIgMjMuMTkzNiwtNTguODc5OTkgMCwtMjIuNjgxNiAtNy45NjE2LC00My44Nzg0IC0yMy4xOTM2LC01OC44OCAtMTUuNzQ0LC0xNS40ODc5OSAtMzkuMTE2NzksLTI0LjIxNzU5IC02My4zODU1OSwtMjQuMjE3NTkgLTI0LjI2ODgsMCAtNDcuNzQzOTksOC42NTI4IC02My40ODc5OSwyNC4yMTc1OSAtMTUuMTgwOCwxNS4wMjcyIC0yMy4wNCwzNi4zMDA4IC0yMy4wNCw1OC44OCBhID
gyLjM1NTE5LDgyLjM1NTE5IDAgMCAwIDIzLjA0LDU4Ljg3OTk5IGMgMTUuNzQ0LDE1LjY5MjggMzkuMDY1NTksMjQuMzcxMiA2My40ODc5OSwyNC4zNzEyIHogTSAxMTcuMTIwMTQsNTI2LjQ2NjkgYyAtMjQuODA2MzkyLDAgLTUyLjk5MTk4OSw0LjY1OTIgLTc2Ljc5OTk4NiwxNS4zNiBDIDE4LjA5OTM1NSw1NTEuNzU5NyAxLjU3NWUtNCw1NjcuNzA4NDkgMS41NzVlLTQsNTk0LjE1MzI5IGEgNTQuMjIwNzk0LDU0LjIyMDc5NCAwIDAgMCAxNC43MTk5OTg1LDM3LjI3
MzYgYyA2LjQyNTU5OSw1LjkzOTIgMTYuNzkzNTk5LDE2LjAyNTYgNDMuODc4Mzk2LDIxLjk2NDggMTIuMTA4Nzk4LDIuNTU5OTkgMzcuOTkwMzk1LDYuNjgxNTkgNjMuNzY5NTg4LDkuMzk1MTkgMjUuNzc5MiwyLjcxMzYgNTAuNzY0OCw1LjEyIDYxLjAwNDgsNy44NTkyIDguMTQwOCwyLjA3MzYgMjEuODM2NzksNC44ODk2IDIxLjgzNjc5LDIwLjI0OTYgMCwxNS4zNiAtMTQuNDEyNzksMTkuOTY4IC0xNi45MjE1OSwyMC45NjY0IC0yLjUwODgsMC45OTg0IC0xOS44MT
Q0LDguOTM0NCAtNTAuODkyOCw4LjkzNDQgYSAyMTYuNDIyMzcsMjE2LjQyMjM3IDAgMCAxIC02MC41OTUxOSwtMTAuNDE5MiBjIC0xMS41OTY3OTksLTQuMTQ3MiAtMjMuNzU2Nzk3LC05LjYgLTM1LjA5NzU5NiwtMjMuNDQ5NiBhIDQwLjI2ODc5NSw0MC4yNjg3OTUgMCAwIDEgLTcuMjk1OTk5LC0yMi4yMjA4IEggNi4yNDY1NTY3IHYgNzguODQ3OTkgSCAzNy41Mjk3NTQgdiAtMTAuNjc1MTkgYSA0LjQ1NDM5OTUsNC40NTQzOTk1IDAgMCAxIDYuNzU4NCwtMy44NCAy
NDYuNDI1NTcsMjQ2LjQyNTU3IDAgMCAwIDQ1Ljc3Mjc5NCwxNC43OTY3OSBjIDE2LjQzNTIwMiwzLjQzMDQgMjcuMDU5MTkyLDUuOTEzNiA0Ny40ODc5OTIsNS45MTM2IGEgMjAyLjYyMzk4LDIwMi42MjM5OCAwIDAgMCA2My42NDE2LC04Ljk4NTYgMTExLjA3ODM5LDExMS4wNzgzOSAwIDAgMCAzNy44MTExOSwtMTguNjYyMzkgNTEuODE0Mzk0LDUxLjgxNDM5NCAwIDAgMCAyMC4yNDk1OSwtNDEuNDk3NiA1OC4wNjA3OTMsNTguMDYwNzkzIDAgMCAwIC0xNi4zNTgzOS
wtNDAuODA2MzkgNzIuMDEyNzkyLDcyLjAxMjc5MiAwIDAgMCAtMjAuMTcyOCwtMTMuNzk4NCAxNDguNjA3OTgsMTQ4LjYwNzk4IDAgMCAwIC0yNC44ODMxOSwtOC42Nzg0IGMgLTE2LjIzMDQsLTMuOTY4IC01Mi42ODQ4LC04LjkzNDQgLTcwLjExODQsLTEwLjY3NTIgLTE4LjI3ODM5LC0xLjg5NDQgLTQ5Ljk5Njc5LC00LjUzMTIgLTYyLjY2ODc4OSwtOC40NDggLTMuODM5OTk5LC0xLjIwMzIgLTExLjY3MzU5OSwtNC45MTUyIC0xMS42NzM1OTksLTE0LjAwMzIgMCwt
Ni40NzY4IDMuNTg0LC0xMS45NTUyIDEwLjY0OTU5OSwtMTYuMzg0IEMgNzUuMjY0MTUsNTYwLjc3MDkgOTcuOTQ1NzQ3LDU1Ni4zOTMzIDEyMS42MDAxNCw1NTYuMzkzMyBhIDE2Ni45ODg3OCwxNjYuOTg4NzggMCAwIDEgNjYuNzEzNiwxMy4wMzAzOSA3Mi44NTc1OTEsNzIuODU3NTkxIDAgMCAxIDE1Ljg3MTk5LDkuNDcyIDQ3LjcxODM5NCw0Ny43MTgzOTQgMCAwIDEgMTUuNjQxNiwyNi4xNjMyIGggMjUuMjY3MiBWIDUzNi40MjUzIGggLTI4LjE2IHYgNy45NjE2IG
MgMCwyLjU2IC0yLjU2LDUuOTM5MiAtNy42OCwzLjE0ODggLTEyLjY5NzU5LC02LjYwNDggLTQ4LjM4Mzk5LC0yMC44ODk2IC05Mi4xMzQzOSwtMjEuMDY4OCB6IG0gNjE4LjIxNDMzLDEyLjU5NTIgMTM3LjYyNTU4LDEyNC4xODU1OCAtMS40MDgsLTgzLjYwOTU5IGMgLTAuMTUzNiwtMTAuOTgyNCAtMi4xNTA0LC0xNS41NjQ3OSAtMTQuMDI4OCwtMTUuNTY0NzkgaCAtMjUuODU1OTkgdiAtMjUuMDExMiBoIDExNy43NTk5OCB2IDI1LjAxMTIgaCAtMjUuMjY3MTkgYyAt
MTIuMDgzMiwwIC0xMi44LDMuODkxMTkgLTEzLjAwNDgsMTUuNTY0NzkgbCAyLjEyNDgsMTU5Ljc2OTU5IGggLTQwLjMyIEwgNzE0LjQxOTI3LDU5Ny45NDIwOSB2IDEwMC4zNzc1OSBjIDAuMTI4LDEwLjkzMTIgMC42NCwxNi4wNzY4IDExLjg3ODQsMTYuMDc2OCBoIDI4LjE2IHYgMjUuMDExMiBIIDYzOS4wMjcyOCB2IC0yNS4wMTEyIGggMjcuMDMzNiBjIDEwLjA4NjM5LDAgOS42NzY3OSwtOS42MjU2IDkuNjc2NzksLTE2LjY0IFYgNTgwLjU4NTI5IGMgMCwtNy42OC
AtMS4wNzUyLC0xNi40ODYzOSAtMTYuODk1OTksLTE2LjQ4NjM5IGggLTIxLjkxMzYgViA1MzkuMDYyMSBaIE0gMTA4My43NzYsNzE0LjM0NTI4IGEgNTUuODU5MTkzLDU1Ljg1OTE5MyAwIDAgMCA2Ljk2MzIsLTAuNDM1MiA4LjYyNzE5OSw4LjYyNzE5OSAwIDAgMCA1LjQyNzIsLTQuODEyOCAyOC4wMDYzOTcsMjguMDA2Mzk3IDAgMCAwIDAuNTM3NiwtNS40MDE2IHYgLTM5LjU1MiBjIDAsLTEuMzMxMiAwLC0xLjM1NjggLTEuNjg5NiwtMy40NTYgLTEuNjg5NiwtMi4w
OTkyIC03Mi4wODk2LC04MS45MTk5OSAtNzUuMjg5NiwtODUuNTAzOTkgLTMuOTkzNiwtNC4zNTIgLTExLjAwOCwtMTEuMDg0NzkgLTIxLjY4MzIsLTExLjA4NDc5IGggLTI0LjQ0Nzk2IHYgLTI1LjAzNjggaCAxMzcuOTgzOTYgdiAyNC45ODU2IGggLTE2LjY0IGMgLTMuODQsMCAtNi40LDMuNjYwNzkgLTMuMTIzMiw3LjY3OTk5IDAsMCA0Ni40Mzg0LDU1LjU1MiA0Ni44NzM2LDU2LjE0MDggMC40MzUyLDAuNTg4OCAwLjgxOTIsMC43MTY4IDEuNDA4LDAuMTc5MiAwLj
U4ODgsLTAuNTM3NiA0Ny41OTA0LC01NS44MDggNDcuOTQ4OCwtNTYuMzIgYSA0Ljc4NzE5OTQsNC43ODcxOTk0IDAgMCAwIC00LjA5NiwtNy42Nzk5OSBoIC0xNy4wNzUyIFYgNTM5LjA2MjEgSCAxMjgwIHYgMjUuMDM2OCBoIC0yNS4yNjcyIGMgLTkuMTY0OCwwIC0xMi44LDEuNjg5NTkgLTE5Ljc4ODgsOS40NzE5OSBsIC03Ni4xNiw4Ni44ODYzOSBhIDUuMzc1OTk5NCw1LjM3NTk5OTQgMCAwIDAgLTAuOTIxNiwzLjY4NjQgdiAzOS41MjY0IGEgMjguMTU5OTk3LDI4
LjE1OTk5NyAwIDAgMCAwLjU2MzIsNS40MDE2IDguNTI0Nzk5LDguNTI0Nzk5IDAgMCAwIDUuNDAxNiw0LjgxMjggNTAuNjExMTk0LDUwLjYxMTE5NCAwIDAgMCA2LjkxMiwwLjQzNTIgaCAyNS44MzA0IHYgMjUuMDM2OCBoIC0xMzcuMjY3MiB2IC0yNS4wMzY4IHoiIGlkPSJwYXRoMSIgZmlsbD0iIzAwMDAwMCIgc3R5bGU9InN0cm9rZS13aWR0aDoyLjU2Ii8+PC9zdmc6Zz48L3N2ZzpnPjwvc3ZnOnN2Zz4K</data><downloads_count>0</downloads_count></logo>
<finalclass>Brand</finalclass>
<friendlyname>Sony</friendlyname>
</Brand>
<Brand alias="Brand" id="13">
<name>Toshiba</name>
<logo><mimetype>image/svg+xml</mimetype><filename>logo-toshiba.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmc6c3ZnIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIiB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiIHhtbG5zOnN2Zz0
iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSI4MDAiIGhlaWdodD0iODAwIiB2ZXJzaW9uPSIxLjAiIHZpZXdCb3g9IjAgMCA4MDAgODAwLjAxOTY5IiBpZD0ic3ZnMSIgc29kaXBvZGk6ZG9jbmFtZT0ibG9nby10b3NoaWJhLnN2ZyIgaW5rc2NhcGU6dmVyc2lvbj0iMS40LjMgKDBkMTVmNzUwNDIsIDIwMjUtMTItMjUpIj48c3ZnOmRlZnMgaWQ9ImRlZnMxIi8+PHNvZGlwb2RpOm5hbWVkdmlldyBpZD0ibmFtZWR2a
WV3MSIgcGFnZWNvbG9yPSIjZmZmZmZmIiBib3JkZXJjb2xvcj0iIzAwMDAwMCIgYm9yZGVyb3BhY2l0eT0iMC4yNSIgaW5rc2NhcGU6c2hvd3BhZ2VzaGFkb3c9IjIiIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiIGlua3NjYXBlOnBhZ2VjaGVja2VyYm9hcmQ9IjAiIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIgc2hvd2d1aWRlcz0idHJ1ZSIgaW5rc2NhcGU6em9vbT0iMS41MzgxMjUiIGlua3NjYXBlOmN4PSIzNzguNzA3ODQiIGlua3NjYXBlOmN5PSIyODg
uMDEzIiBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjI1NjAiIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjEzNjAiIGlua3NjYXBlOndpbmRvdy14PSIwIiBpbmtzY2FwZTp3aW5kb3cteT0iMCIgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnMSI+PHNvZGlwb2RpOmd1aWRlIHBvc2l0aW9uPSIyMjcuMzE3MTYsMzk5LjM4NTQyIiBvcmllbnRhdGlvbj0iMCwtMSIgaWQ9Imd1aWRlMSIgaW5rc2NhcGU6bG9ja2VkPSJmY
WxzZSIvPjwvc29kaXBvZGk6bmFtZWR2aWV3PgogIDxzdmc6cGF0aCBmaWxsPSIjZTYxZTFlIiBkPSJtIDc2Ni4xMDAxNiw0NTkuNzEzNiBoIDM0LjU1IGwgLTM1LjY1LC0xMTcuNjYgLTQ5LjEsLTAuMDAyIC0zNS42NSwxMTcuNjYgaCAzNC41NiBsIDYuMywtMjEuNzggaCAzOC42NiBsIDYuMzMsMjEuNzggbSAtMzcuNTEsLTQ3Ljk0MyAxMS43NiwtNDAuNjUxIGggMC4yIGwgMTEuNzYsNDAuNjUxIHogbSAtNTU1LjQ2LDUwLjA1NSBjIDM1LjQ4LDAgNTIuNjMsLTYuMjU
gNTUuMDYsLTM4LjI2NSAwLjU4LC03LjYxOCAwLjY5LC0xNS40MzkgMC42OSwtMjIuNjg5IDAuMDEsLTcuMjI1IC0wLjExLC0xNS4wNTQgLTAuNjksLTIyLjY3MSAtMi40MywtMzIuMDI1IC0xOS41OCwtMzguMjY1IC01NS4wNiwtMzguMjY1IC0zNS40OCwwIC01Mi42Miw2LjI0IC01NS4wNCwzOC4yNjUgLTAuNTksNy42MTcgLTAuNzEsMTUuNDQ2IC0wLjcxLDIyLjY3MSAwLjAxLDcuMjUgMC4xMiwxNS4wNzEgMC43MSwyMi42ODkgMi40MiwzMi4wMTUgMTkuNTYsMzguMjY1IDU1LjA0LDM4LjI2NSBtIC0yMi4zMSwtNjAuOTU0IGMgMCwtNi40NjEgMC4xNiwtMTAuMjggMC4zLC0xMy4xMTQgMC45LC0xOC4xNjEgOC4wNywtMjAuMjc4IDIyLjAxLC0yMC4yNzggMTMuOTUsMCAyMS4xMiwyLjExNyAyMi4wMSwyMC4yNzggMC4xNCwyLjgzMyAwLjMxLDYuNjUyIDAuMzEsMTMuMTE0IDAsNi40ODIgLTAuMTcsMTAuMzA4IC0wLjMxLDEzLjEzNSAtMC44OSwxOC4xNjQgLTguMDYsMjAuMjg1IC0yMi4wMSwyMC4yODUgLTEzLjk0LDAgLTIxLjExLC0yLjEyMSAtMjIuMDEsLTIwLjI4NSAtMC4xNCwtMi44MjcgLTAuMywtNi42NTMgLTAuMywtMTMuMTM1IHogTSAwLjY1MDE1ODIyLDM0Mi4xMDU2IHYgMjkuMzMxIEggMzUuODIyMTU4IHYgODguMzI3IGggMzUuMTg1IHYgLTg4LjMyNyBoIDM1LjE3MzAwMiB2IC0yOS4zMzEgSCAwLjY1MDE1ODIyIE0gNTQwLjUwMDE2LDQ1OS43MTM2IHYgLTExNy42NjIgaCAtMzMuMzkgdiAxMTcuNjYyIGggMzMuMzkgbSAtMTM0LjM1LC03NC43MDMgdiAtNDIuOTU5IGggLTMzLjIgdiAxMTcuNjYyIGggMzMuMiB2IC00NS4zNzIgaCAzOC41OCB2IDQ1LjM3MiBoIDMzLjE5IHYgLTExNy42NjIgaCAtMzMuMTkgdiA0Mi45NTkgaCAtMzguNTggbSAyNDQuMTcsMTMuMjA2IGMgMTQuNzksLTMuNzgxIDE5LjEzLC0xMi42MTYgMTkuMTMsLTI1LjM4NiAwLC0yNS44NTkgLTE2LjI3LC0zMC43OCAtMzkuNCwtMzAuNzggaCAtNTkuOTUgdiAxMTcuNjYgaCA2Mi45MiBjIDI4Ljk3LDAgMzguNzEsLTEyLjQ4IDM4LjcxLC0zMS42NzUgMCwtMTMuMzgzIC0zLjA2LC0yNS4xOTEgLTIxLjQxLC0yOS44MjIgbSAtNDcuMDMsMTMuMTY5IGggMjMuMDIgYyA5LjMsMCAxMS4yNCw0LjA3NCAxMS4yNCwxMC43IDAsNi42MzIgLTMuNjQsMTAuNzE3IC0xMS4yNCwxMC43MTcgaCAtMjMuMDIgeiBtIDAsLTQyLjQyNSBoIDIzLjAyIGMgNi4wMSwwIDkuNzMsMi44NTEgOS43Myw5LjcwOCAwLDUuODc4IC0zLjY4LDkuNDk2IC05LjczLDkuNDk2IGggLTIzLjAyIHogbSAtMzU1LjA2LDUyLjE0MyBoIDMxLjY1IGMgMC4wMyw1LjcwOCAwLjc2LDkuNTIzIDMuNTMsMTEuNjMgMy4xNSwyLjM3NCA1Ljk3LDMuMTU4IDE1LjMyLDMuMTU4IDksMCAxOC44NiwwIDE4Ljg2LC0xMS4wODUgMCwtOC43NDIgLTUuNTEsLTEwLjczNyAtMTUuNjgsLTExLjI3OSAtMjUuMjIsLTEuMzM2IC0zNC4zNCwtMi4wNDkgLTQzLjczLC05LjAyNSAtNi40LC00Ljc1NyAtOS43MiwtMTQuMDE4IC05LjcyLC0yNi41NDIgMCwtMjEuMjk3IDcuNDMsLTI4Ljc2OCAxOC4xNSwtMzMuOTgxIDExLjA2LC01LjM4MSA1NC40NywtNS4zODEgNjYuMTUsMCAxNC42OSw2Ljc2OCAxNS4xMiwyMS40MiAxNS4xMiwzNS4wMTEgaCAtMzEuNTcgYyAtMC4wNiwtNi45MjkgLTEuNjIsLTguODg2IC0yLjg5LC0xMC4xNzUgLTMuMjgsLTIuOTA4IC03Ljk1LC0zLjUyMiAtMTQuNjksLTMuNTIyIC04LjE2LDAgLTE3LjYsMC4zNjggLTE3LjYsMTAuMjc3IDAsNy41NiAzLjI3LDEwLjcyIDExLjg1LDExLjI3NiAxMS43OSwwLjc1NCAzNS4wMiwxLjQ5NyA0My4zLDYuMzgzIDExLjYxLDYuODY3IDE0LjYyLDE2LjE1OSAxNC42MiwzMS4zMTkgMCwyMS45MDggLTcuODQsMjguMzM4IC0xOC43NSwzMy4xNTggLTEyLjU5LDUuNTYgLTU0LjY0LDUuNTYgLTY4LjMxLC0wLjQzIC0xNS4zLC02LjY3IC0xNS42MSwtMTkuOTY0IC0xNS42MSwtMzYuMTczIiBpZD0icGF0aDEiLz4KPC9zdmc6c3ZnPgo=</data><downloads_count>0</downloads_count></logo>
<finalclass>Brand</finalclass>
<friendlyname>Toshiba</friendlyname>
</Brand>
<Brand alias="Brand" id="1">
<name>Acer</name>
</Brand>
<Brand alias="Brand" id="2">
<name>Apple</name>
</Brand>
<Brand alias="Brand" id="3">
<name>Asus</name>
</Brand>
<Brand alias="Brand" id="4">
<name>Cisco</name>
</Brand>
<Brand alias="Brand" id="5">
<name>Dell</name>
</Brand>
<Brand alias="Brand" id="6">
<name>HP Inc</name>
</Brand>
<Brand alias="Brand" id="7">
<name>HPE</name>
</Brand>
<Brand alias="Brand" id="8">
<name>IBM</name>
</Brand>
<Brand alias="Brand" id="9">
<name>Lenovo</name>
</Brand>
<Brand alias="Brand" id="10">
<name>Razer</name>
</Brand>
<Brand alias="Brand" id="11">
<name>Samsung</name>
</Brand>
<Brand alias="Brand" id="12">
<name>Sony</name>
</Brand>
<Brand alias="Brand" id="13">
<name>Toshiba</name>
</Brand>
</Set>

View File

@@ -1,116 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<Set>
<OSFamily alias="OSFamily" id="1">
<name>Arch</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-arch-linux.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmlld0JveD0iMCwwLDI1NiwyNTYiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiIGZpbGwtcnVsZT0ibm9uemVybyI+PGcgZmlsbD0iIzAwODhjYyIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgc3Ryb2tlLWxpbmVjYXA9ImJ1dHQiIHN0cm9rZS1saW5lam9pbj0ibWl0ZXIiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLWRhc2hhcnJheT0iIiBzdHJva2UtZGFzaG9mZnNldD0iMCIgZm9udC1mYW1pbHk9Im5vbmUiIGZvbnQtd2VpZ2h0PSJub25lIiBmb250LXNpemU9Im5vbmUiIHRleHQtYW5jaG9yPSJub25lIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6IG5vcm1hbCI+PGcgdHJhbnNmb3JtPSJzY2FsZSg1LjMzMzMzLDUuMzMzMzMpIj48cGF0aCBkPSJNMjguNDY1LDM4LjYxMWMwLjQxOSwtMS4xMDUgMC42NjQsLTIuMzY1IDAuNjY0LC0zLjcxNGMwLC00LjEzMyAtMi4yMTEsLTcuNDk0IC00LjkyOSwtNy40OTRjLTIuNzQxLDAgLTQuOTUxLDMuMzYxIC00Ljk1MSw3LjQ5NGMwLDEuMzI2IDAuMjIxLDIuNTg2IDAuNjQxLDMuNjY5Yy05LjA0MSwwLjk1MSAtMTUuNDA3LDQuNzMxIC0xNy45OTMsNi40MzJjNC4zNTUsLTYuMjc4IDguOTA5LC0xMy42MzggMTMuMjYyLC0yMi4xMDVjMS4wODMsLTIuMTAxIDIuMTAxLC00LjE3OCAzLjA1LC02LjIxMWMwLjM3NSwwLjI0MyAwLjc1MSwwLjUwOSAxLjE3MSwwLjc3NWMxLjk0NSwxLjIxNSAzLjc1OSwxLjg3OSA1LjA4NCwyLjIzM2MtMC45NzMsLTAuNzMgLTIuMDMzLC0xLjYxMyAtMy4xMTYsLTIuNjk3Yy0wLjgxNywtMC44MTcgLTEuNTQ3LC0xLjYzNyAtMi4xNjcsLTIuNDMzYzEuODM1LC00LjAyMiAzLjQyNywtNy44OTEgNC44MTksLTExLjU2YzIuMzIsNi4xNDQgNS4yMTcsMTIuODQyIDguODQxLDE5Ljg5M2MyLjM0Myw0LjUzMSA0LjczMSw4Ljc1NCA3LjExNywxMi42NDRjLTAuNjg1LC0wLjM3NSAtMS40MzcsLTAuNzMgLTIuMjMzLC0xLjAzOWMtMS4zNzEsLTAuNTMgLTIuNjUyLC0wLjg2MiAtMy43NTksLTEuMDZjMS41MDMsMC43NTEgMy4yNSwxLjc0NyA1LjA4NCwzLjA3M2MxLjE5NCwwLjg4NSAyLjI1NCwxLjc2OSAzLjE2MSwyLjYzMWMwLjAyMSwwLjAyMSAwLjAyMSwwLjAyMSAwLjA0NSwwLjA0NWMxLjI2LDIuMDU2IDIuNTY1LDMuOTU3IDMuODQ2LDUuODEzYy0yLjU0MSwtMS42ODEgLTguNzk2LC01LjM5NSAtMTcuNjM3LC02LjM4OXoiLz48L2c+PC9nPjwvc3ZnPgo=</data><downloads_count>0</downloads_count></logo>
<finalclass>OS Family</finalclass>
<friendlyname>Arch</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="2">
<name>Debian</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-debian.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiNFOTFFNjMiIGQ9Ik0yNi43NjMsMjQuNTQ4Yy0wLjYxNCwwLjAxLDAuMTE3LDAuMzE3LDAuOTE4LDAuNDRjMC4yMi0wLjE3MiwwLjQxOS0wLj
M0OCwwLjYtMC41MTVDMjcuNzgxLDI0LjU5MiwyNy4yNzQsMjQuNTk0LDI2Ljc2MywyNC41NDggTTMwLjA1NCwyMy43MjdjMC4zNjQtMC41LDAuNjMxLTEuMDU1LDAuNzIzLTEuNjI0Yy0wLjA4MiwwLjQwNS0wLjMwMywwLjc1NS0wLjUxLDEuMTI4Yy0xLjE0NiwwLjcyMS0wLjEwOC0wLjQzLDAtMC44NjVDMjkuMDM1LDIzLjkxMywzMC4wOTgsMjMuMjkzLDMwLjA1NCwyMy43MjcgTTMxLjI2OSwyMC41NjhjMC4wNzMtMS4xMDUtMC4yMTktMC43NTYtMC4zMTctMC4zMzZD
MzEuMDY4LDIwLjI5NCwzMS4xNTYsMjEuMDEzLDMxLjI2OSwyMC41NjggTTI0LjQzOSw1LjQ3OGMwLjMyNywwLjA1OCwwLjcwNiwwLjEwNCwwLjY1MywwLjE4M0MyNS40NDksNS41ODIsMjUuNTMxLDUuNTEsMjQuNDM5LDUuNDc4IE0yNS4wOTMsNS42NmwtMC4yMzIsMC4wNDdsMC4yMTUtMC4wMTdMMjUuMDkzLDUuNjYgTTM1LjI5NCwyMC45ODZjMC4wMzgsMC45OTEtMC4yOSwxLjQ3Mi0wLjU4NSwyLjMyMmwtMC41MjksMC4yNjZjLTAuNDM1LDAuODQxLDAuMDQxLDAuNT
M1LTAuMjY4LDEuMjAyYy0wLjY3OSwwLjYwMy0yLjA1NSwxLjg4My0yLjQ5NiwyLjAwNGMtMC4zMjEtMC4wMDksMC4yMTgtMC4zODIsMC4yODktMC41MjZjLTAuOTA2LDAuNjItMC43MjgsMC45MzQtMi4xMTMsMS4zMTNsLTAuMDQxLTAuMDljLTMuNDE5LDEuNjA3LTguMTY2LTEuNTc2LTguMTAzLTUuOTI4Yy0wLjAzNywwLjI3NS0wLjEwNCwwLjIwOS0wLjE4LDAuMzJjLTAuMTc1LTIuMjM3LDEuMDMzLTQuNDg2LDMuMDczLTUuNDAzYzEuOTk1LTAuOTg3LDQuMzM1LTAu
NTgsNS43NjMsMC43NWMtMC43ODUtMS4wMjgtMi4zNDgtMi4xMTktNC4xOTktMi4wMTdjLTEuODE0LDAuMDI5LTMuNTEsMS4xODItNC4wNzcsMi40MzRjLTAuOTI5LDAuNTg1LTEuMDM4LDIuMjU2LTEuNDQxLDIuNTYzYy0wLjU0NSw0LjAwMywxLjAyNCw1LjczMywzLjY4LDcuNzY4YzAuNDE3LDAuMjgyLDAuMTE4LDAuMzI2LDAuMTc1LDAuNTQxYy0wLjg4My0wLjQxMi0xLjY5LTEuMDM3LTIuMzU0LTEuODAxYzAuMzUzLDAuNTE3LDAuNzMzLDEuMDE3LDEuMjIzLDEuND
FjLTAuODMxLTAuMjc5LTEuOTQyLTIuMDEzLTIuMjY3LTIuMDg0YzEuNDM1LDIuNTY3LDUuODE4LDQuNTAyLDguMTEzLDMuNTQxYy0xLjA2MiwwLjA0LTIuNDEyLDAuMDIxLTMuNjA0LTAuNDJjLTAuNTAxLTAuMjU3LTEuMTgzLTAuNzkxLTEuMDYyLTAuODkzYzMuMTMzLDEuMTcxLDYuMzY5LDAuODg3LDkuMDc4LTEuMjg2YzAuNjg5LTAuNTM3LDEuNDQzLTEuNDQ5LDEuNjYyLTEuNDY0Yy0wLjMyNywwLjQ5MywwLjA1NywwLjIzOS0wLjE5NywwLjY3NGMwLjY4OC0xLjEw
OS0wLjI5OS0wLjQ0OSwwLjcxMS0xLjkxM2wwLjM3MywwLjUxMmMtMC4xMzktMC45MTcsMS4xNDMtMi4wMzMsMS4wMTItMy40ODljMC4yOTEtMC40NDUsMC4zMjYsMC40NzgsMC4wMTUsMS41MDJjMC40MzQtMS4xMzYsMC4xMTMtMS4zMTcsMC4yMjQtMi4yNTRjMC4xMjEsMC4zMTUsMC4yNzksMC42NDgsMC4zNTksMC45ODFjLTAuMjgxLTEuMDk3LDAuMjg5LTEuODQ4LDAuNDMzLTIuNDg1Yy0wLjE0Mi0wLjA2My0wLjQzNSwwLjQ4NS0wLjUwMy0wLjgxMmMwLjAxLTAuNT
YyLDAuMTU2LTAuMjk1LDAuMjE0LTAuNDM1Yy0wLjExMS0wLjA2NC0wLjQtMC40OTYtMC41NzctMS4zMjNjMC4xMjctMC4xOTMsMC4zNDIsMC41MDYsMC41MTYsMC41MzNjLTAuMTEyLTAuNjU1LTAuMzA0LTEuMTU5LTAuMzEzLTEuNjY1Yy0wLjUxLTEuMDYxLTAuMTgxLDAuMTQzLTAuNTkyLTAuNDU4Yy0wLjU0My0xLjY4NywwLjQ0OS0wLjM5LDAuNTE0LTEuMTU2YzAuODIsMS4xODgsMS4yODksMy4wMjksMS41MDQsMy43OTJjLTAuMTY0LTAuOTMtMC40MjgtMS44MzIt
MC43NTItMi43MDRjMC4yNDksMC4xMDgtMC40MDEtMS45MTEsMC4zMjQtMC41NzVjLTAuNzcyLTIuODQ4LTMuMzE0LTUuNTExLTUuNjUtNi43NmMwLjI4NiwwLjI2MiwwLjY0NiwwLjU5MSwwLjUxNywwLjY0MmMtMS4xNjMtMC42OS0wLjk1OS0wLjc0NS0xLjEyNC0xLjA0MWMtMC45NDYtMC4zODMtMS4wMSwwLjAzNC0xLjYzNiwwYy0xLjc4Ni0wLjk0My0yLjEyOS0wLjg0NS0zLjc3Mi0xLjQzN2wwLjA3OCwwLjM0OWMtMS4xODQtMC4zOTQtMS4zNzksMC4xNDYtMi42NT
csMC4wMDJjLTAuMDc4LTAuMDYyLDAuNDEtMC4yMTksMC44MTEtMC4yNzhjLTEuMTQzLDAuMTUtMS4wOS0wLjIyOC0yLjIwOCwwLjA0MmMwLjI3Ny0wLjE5NywwLjU2Ni0wLjMyMiwwLjg2MS0wLjQ4NmMtMC45MzIsMC4wNTktMi4yMjYsMC41NDItMS44MjUsMC4xMDNjLTEuNTIxLDAuNjc2LTQuMjIsMS42My01LjczNSwzLjA1MWwtMC4wNDctMC4zMjJjLTAuNjk0LDAuODM1LTMuMDI4LDIuNDkyLTMuMjE1LDMuNTdsLTAuMTg1LDAuMDQzYy0wLjM2MSwwLjYxMy0wLjU5
NSwxLjMwNS0wLjg4MSwxLjkzNWMtMC40NzQsMC44MDYtMC42OTIsMC4zMTEtMC42MjYsMC40MzZjLTAuOTI5LDEuODgzLTEuMzksMy40NjctMS43OSw0Ljc2OGMwLjI4NCwwLjQyNCwwLjAwNywyLjU1OCwwLjExMyw0LjI2NGMtMC40NjcsOC40MjksNS45MTYsMTYuNjA5LDEyLjg5MSwxOC41YzEuMDIzLDAuMzY1LDIuNTQyLDAuMzU0LDMuODM2LDAuMzljLTEuNTI1LTAuNDM4LTEuNzIyLTAuMjMyLTMuMjA5LTAuNzQ5Yy0xLjA3NC0wLjUwNi0xLjMwOC0xLjA4Mi0yLj
A2Ni0xLjc0bDAuMywwLjUzYy0xLjQ5LTAuNTI2LTAuODY3LTAuNjUyLTIuMDc4LTEuMDM0bDAuMzIxLTAuNDI0Yy0wLjQ4Mi0wLjAzMi0xLjI3OS0wLjgxMS0xLjQ5Ny0xLjI0MWwtMC41MjgsMC4wMjFjLTAuNjM0LTAuNzgzLTAuOTcyLTEuMzQ4LTAuOTQ4LTEuNzg1bC0wLjE3LDAuMzA1Yy0wLjE5NC0wLjMzMi0yLjMzNS0yLjkzNy0xLjIyNC0yLjMzYy0wLjIwNy0wLjE4OC0wLjQ4MS0wLjMwNy0wLjc3OS0wLjg1bDAuMjI3LTAuMjU4Yy0wLjUzNS0wLjY4Ni0wLjk4
My0xLjU2OC0wLjk0OS0xLjg2YzAuMjg0LDAuMzg0LDAuNDgyLDAuNDU0LDAuNjc5LDAuNTIyYy0xLjM1MS0zLjM0OS0xLjQyNi0wLjE4Ny0yLjQ0OC0zLjQwOWwwLjIxNi0wLjAxOWMtMC4xNjYtMC4yNDYtMC4yNjUtMC41MjEtMC4zOTktMC43ODVsMC4wOTQtMC45MzhjLTAuOTcyLTEuMTI1LTAuMjcyLTQuNzgxLTAuMTMyLTYuNzgzYzAuMDk3LTAuODE2LDAuODExLTEuNjg0LDEuMzU0LTMuMDQ1bC0wLjMzMi0wLjA1NWMwLjYzMi0xLjEwNCwzLjYxMi00LjQzMyw0Lj
k5LTQuMjZjMC42NjktMC44NDEtMC4xMzItMC4wMDItMC4yNjMtMC4yMTVjMS40NjktMS41MiwxLjkzLTEuMDczLDIuOTItMS4zNDljMS4wNjgtMC42MzMtMC45MTcsMC4yNTEtMC40MS0wLjIzOWMxLjg0OC0wLjQ3MywxLjMxLTEuMDczLDMuNzE4LTEuMzExYzAuMjU0LDAuMTQ1LTAuNTksMC4yMjMtMC44LDAuNDFjMS41MzgtMC43NTMsNC44Ny0wLjU4NCw3LjAzNCwwLjQxN2MyLjUxMSwxLjE3Myw1LjMzLDQuNjQyLDUuNDQzLDcuOTA0bDAuMTI2LDAuMDM1Yy0wLjA2
MywxLjI5OCwwLjE5OCwyLjc5OC0wLjI1Nyw0LjE3NUwzNS4yOTQsMjAuOTg2IE0yMC4wNzIsMjUuMzg5bC0wLjA4NiwwLjQzMWMwLjQwMywwLjU0NywwLjcyNCwxLjE0MiwxLjIzNywxLjU2N0MyMC44NTMsMjYuNjY0LDIwLjU3NywyNi4zNjQsMjAuMDcyLDI1LjM4OSBNMjEuMDIzLDI1LjM1M2MtMC4yMTMtMC4yMzctMC4zNC0wLjUxOC0wLjQ4LTAuODAyYzAuMTM1LDAuNDk1LDAuNDExLDAuOTIyLDAuNjY5LDEuMzU3TDIxLjAyMywyNS4zNTMgTTM3Ljg3NywyMS42OD
hsLTAuMDg4LDAuMjI2Yy0wLjE2NiwxLjE3NC0wLjUyMywyLjMzMi0xLjA2OCwzLjQxMkMzNy4zMjQsMjQuMTg5LDM3LjcxNCwyMi45NDcsMzcuODc3LDIxLjY4OCBNMjQuNTYsNS4xODVDMjQuOTc0LDUuMDMxLDI1LjU3OSw1LjEwMSwyNi4wMTksNWMtMC41NzMsMC4wNDgtMS4xNDQsMC4wNzktMS43MDYsMC4xNTFMMjQuNTYsNS4xODUgTTEwLjAwNywxMi45MjNjMC4wOTUsMC44ODItMC42NjcsMS4yMjksMC4xNjcsMC42NDRDMTAuNjIzLDEyLjU2MiwxMCwxMy4yODYsMTAuMDA3LDEyLjkyMyBNOS4wMjgsMTcuMDE2YzAuMTkxLTAuNTkyLDAuMjI2LTAuOTQzLDAuMy0xLjI4NUM4Ljc5NywxNi40MSw5LjA4NCwxNi41NTMsOS4wMjgsMTcuMDE2Ii8+PC9zdmc+Cg==</data><downloads_count>0</downloads_count></logo>
<finalclass>OS Family</finalclass>
<friendlyname>Debian</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="3">
<name>Oracle Linux</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icon-oracle-linux.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIgeG1sbnM9Imh0dH
A6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMjMxIDIzMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pbllNaWQiIHZlcnNpb249IjEuMSIgaWQ9InN2ZzQiIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSIyMzEiIGhlaWdodD0iMjMxIiBzb2RpcG9kaTpkb2NuYW1lPSJpY29uLW9yYWNsZS1saW51eC5zdmciIGlua3NjYXBlOnZlcnNpb249IjEuNC4zICgwZDE1Zjc1MDQyLCAy
MDI1LTEyLTI1KSI+PHNvZGlwb2RpOm5hbWVkdmlldyBpZD0ibmFtZWR2aWV3MSIgcGFnZWNvbG9yPSIjZmZmZmZmIiBib3JkZXJjb2xvcj0iIzAwMDAwMCIgYm9yZGVyb3BhY2l0eT0iMC4yNSIgaW5rc2NhcGU6c2hvd3BhZ2VzaGFkb3c9IjIiIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiIGlua3NjYXBlOnBhZ2VjaGVja2VyYm9hcmQ9IjAiIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIgc2hvd2d1aWRlcz0idHJ1ZSIgaW5rc2NhcGU6em9vbT0iNy41MzMyOD
kxIiBpbmtzY2FwZTpjeD0iMTA3LjA1ODE1IiBpbmtzY2FwZTpjeT0iOTAuNzk2OTkzIiBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjI1NjAiIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjEzNjAiIGlua3NjYXBlOndpbmRvdy14PSIwIiBpbmtzY2FwZTp3aW5kb3cteT0iMCIgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnNCI+PHNvZGlwb2RpOmd1aWRlIHBvc2l0aW9uPSI0MS43NjM1ODksMTE1LjQ2OCIgb3JpZW50
YXRpb249IjAsLTEiIGlkPSJndWlkZTEiIGlua3NjYXBlOmxvY2tlZD0iZmFsc2UiLz48L3NvZGlwb2RpOm5hbWVkdmlldz48ZGVmcyBpZD0iZGVmczgiPjxyZWN0IHg9Ii0xMzguMjQ2NDEiIHk9IjgxLjU2NzIzOCIgd2lkdGg9IjM4Mi4zNjc0IiBoZWlnaHQ9IjI0MC4yMTE1NiIgaWQ9InJlY3Q0OTUiLz48cmVjdCB4PSItMTcuNjEyNjIzIiB5PSIxMDQuMzU4OTgiIHdpZHRoPSIxNzEuMDUyMzgiIGhlaWdodD0iMTk3LjMxNDQ0IiBpZD0icmVjdDI5MSIvPjwvZGVmcz
48ZyBpZD0iZzE0MDciIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuNjEzMjE1NDIsNzYuODM1NTMzKSI+PHBhdGggZD0ibSA5OS41OSwxOS41MiBoIDE1LjI0IEwgMTA2Ljc4LDYuNTIgOTEuOTgsMzAgaCAtNi43MyBsIDE4LC0yOC4xNyBhIDQuMjksNC4yOSAwIDAgMSA3LC0wLjA1IEwgMTI4LjMsMzAgaCAtNi43MyBsIC0zLjE3LC01LjI1IGggLTE1LjQyIGwgLTMuMzYsLTUuMjMgbSA2OS45Myw1LjIzIFYgMC4yOCBoIC01LjcyIHYgMjYuODggYSAyLjc2LDIuNzYgMCAw
IDAgMC44NSwyIDIuODksMi44OSAwIDAgMCAyLjA4LDAuODcgaCAyNiBsIDMuMzksLTUuMjUgaCAtMjYuNjMgbSAtOTQuNTQsLTQuNCBhIDEwLjA1LDEwLjA1IDAgMCAwIDAsLTIwLjEgaCAtMjUgViAzMCBoIDUuNzEgViA1LjU0IGggMTguOTQgYSA0LjgxLDQuODEgMCAwIDEgMCw5LjYyIEggNTguNTIgTCA3NS41OCwzMCBoIDguMjkgTCA3Mi40MSwyMC4zOCBoIDIuNTcgTSAxNC44NiwzMCBoIDE3LjI3IGEgMTQuODYsMTQuODYgMCAwIDAgMCwtMjkuNzEgSCAxNC44Ni
BhIDE0Ljg2LDE0Ljg2IDAgMSAwIDAsMjkuNzEgbSAxNi44OCwtNS4yMyBoIC0xNi41IGEgOS42Miw5LjYyIDAgMCAxIDAsLTE5LjIzIGggMTYuNSBhIDkuNjIsOS42MiAwIDEgMSAwLDE5LjIzIE0gMTQwLjIzLDMwIGggMTcuNjMgbCAzLjM0LC01LjIzIGggLTIwLjU4IGEgOS42Miw5LjYyIDAgMSAxIDAsLTE5LjIzIGggMTYuNzUgbCAzLjM4LC01LjI1IGggLTIwLjUyIGEgMTQuODYsMTQuODYgMCAxIDAgMCwyOS43MSBtIDY5Ljg3LC01LjIzIGEgOS42Miw5LjYyIDAg
MCAxIC05LjI2LC03IGggMjQuNDIgbCAzLjM2LC01LjI0IGggLTI3Ljc4IGEgOS42MSw5LjYxIDAgMCAxIDkuMjYsLTcgaCAxNi43NiBsIDMuMzUsLTUuMjUgaCAtMjAuNSBhIDE0Ljg2LDE0Ljg2IDAgMCAwIDAsMjkuNzEgaCAxNy42MyBsIDMuMzUsLTUuMjMgaCAtMjAuNiIgc3R5bGU9ImZpbGw6I2M3NDYzNCIgaWQ9InBhdGgyIi8+PHBhdGggaWQ9InJlY3Q3NDkiIHN0eWxlPSJmaWxsOiNjNzQ2MzQ7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlLXdpZHRoOjUuMjUxOTtzdH
Jva2UtbGluZWNhcDpzcXVhcmU7cGFpbnQtb3JkZXI6c3Ryb2tlIGZpbGwgbWFya2VycyIgZD0ibSAyNS4zOTE1MTcsNDcuOTMwODcxIGggNC45ODA4NjMgdiAzLjgzMDAyNSBoIC00Ljk4MDg2MyB6IG0gNjEuNTAwOCwxOC41NTcwNjEgLTcuMjQsLTEwLjI0MDAwMiBoIDQuOTYgbCA0LjY0LDcuMDgwMDAyIGggMC4yNCBsIDQuNzYsLTcuMDgwMDAyIGggNC43MiBsIC03LjI4LDEwLjE2MDAwMiA3LjY0LDEwLjg4IGggLTQuOTYgbCAtNS4wOCwtNy43MiBoIC0wLjI0IGwg
LTUuMTIsNy43MiBoIC00LjcyIHogbSAtMjEuNTk5OTk4LDExLjI4IHEgLTYuNjgsMCAtNi42OCwtNy4yNCBWIDU2LjI0NzkzIGggNC4xMiB2IDEzLjc2MDAwMiBxIDAsMi4zNiAxLjA0LDMuMzIgMS4wOCwwLjkyIDMuMTIsMC45MiAxLjQsMCAyLjYsLTAuNjggMS4yLC0wLjcyIDEuODgsLTIgMC43MiwtMS4yOCAwLjcyLC0yLjk2IFYgNTYuMjQ3OTMgaCA0LjEyIHYgMjEuMDQwMDAyIGggLTMuMzIgbCAtMC40LC0yLjggaCAtMC4yOCBxIC0xLjE2LDEuNiAtMi45MiwyLj
Q0IC0xLjc2LDAuODQgLTQsMC44NCB6IE0gMzUuODEyMzIsNTYuMjQ3OTMgaCAzLjMyMDAwMSBsIDAuNCwyLjgwMDAwMiBoIDAuMjggcSAxLjEyLC0xLjYwMDAwMiAyLjg4LC0yLjQ0MDAwMiAxLjgsLTAuODQgNC4wNCwtMC44NCA2LjY4LDAgNi42OCw3LjI0MDAwMiB2IDE0LjI4IGggLTQuMTIgdiAtMTMuNzYgcSAwLC0yLjM2IC0xLjA4LC0zLjI4IC0xLjA0LC0wLjk2IC0zLjA4LC0wLjk2IC0xLjQsMCAtMi42LDAuNzIgLTEuMiwwLjY4IC0xLjkyLDEuOTYgLTAuNjgsMS4yOCAtMC42OCwyLjk2IHYgMTIuMzYgSCAzNS44MTIzMiBaIE0gMy42MTIzOSw0Ny45MzA4NzEgaCA0LjQ0IHYgMjUuNTE3MDYxIGggMTQuMjggdiAzLjg0IGggLTE4LjcyIHogTSAyNi4yNTY3NjgsNTkuNDYzOTUgViA3Ny4yODc5MzIgSCAzMC4zNzIzOCBWIDU2LjI3NTQ2NCBoIC02LjM4NzYxMiB2IDEuODI1NjgxIHoiLz48L2c+PC9zdmc+Cg==</data><downloads_count>0</downloads_count></logo>
<finalclass>OS Family</finalclass>
<friendlyname>Oracle Linux</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="4">
<name>Red Hat</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-red-hat.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiMzNzQ3NEYiIGQ9Ik0yNy44MTMsMzQuODg4Yy0wLjMxOSwwLjM1My0wLjc1NSwwLjM3OC0xLjI4NCwwLjA3NWMtMC40OTctMC4yODItMS4yN
zYtMC4yODItMS41MS0wLjAwMSBjLTAuMjY5LDAuMzIzLTAuMDc1LDAuNjgxLDAuMzY3LDAuNjgxYzAuMjA3LDAsMC42MDQsMC4xMzcsMC44NzQsMC4zMDNjMC4yNzEsMC4xNjcsMC42NTMsMC4zMDMsMC44NDcsMC4zMDMgYzAuNDc1LDAuMDAyLDEuMjM0LTAuNzgyLDEuMjM0LTEuMjczQzI4LjM0MiwzNC41MDYsMjguMTgyLDM0LjQ4LDI3LjgxMywzNC44ODh6IE00MywyNS45NzFjMCwzLjIyMS0wLjUzNSw1LjYwMi0xLjg4Nyw4LjM4NiBjLTAuOTgsMi4wMjItMC44NDY
sMS45Mi0yLjI3MiwxLjc0Yy0xLjI5My0wLjE2My0yLjk4OS0wLjAyNi00LjA3NSwwLjMzMWMtMC40ODQsMC4xNi0wLjYxNywwLjI4My0wLjY4MSwwLjY0MyBjLTAuMDY1LDAuMzc5LTAuMjA2LDAuNDkzLTAuOTc5LDAuNzkzYy0wLjY1MiwwLjI1My0wLjk3NywxLjAzMy0xLjY4NiwxLjcyOGMtMC41NDMsMC41My0xLjEyMSwxLjAzLTEuMjg4LDEuMTEzIEMyOS43NDIsNDAuODk0LDI5LDQxLjA2OSwyOSw0MC45NzRjMC0wLjA3MSwwLjk4Ni0yLjI1MSwyLjM1Ny00LjIxO
GMwLjQ5LTAuNzA0LDAuNjYxLTEuMTA0LDAuNzA5LTEuNjYxIGMwLjAzNi0wLjQwNSwwLjIxMS0xLjIyOSwwLjM4OS0xLjgzYzAuMTc5LTAuNjAzLDAuMjg5LTEuMTMsMC4yNDYtMS4xNzNzLTAuMzcsMC4wMjUtMC43MjYsMC4xNDggYy0wLjc3OSwwLjI3NS0xLjI5MywwLjI4Ni0yLjE3NywwLjA1MWMtMC41NjMtMC4xNTEtMC43NzEtMC4xMzktMS4zNDQsMC4wOGMtMC42MzksMC4yNDYtMC43MTksMC4yNDYtMS4zMTMsMC4wMDYgYy0wLjU5NS0wLjIzNy0wLjcyNS0wLjI
zNy0yLjE0NiwwLjAxNGMtMi42NTUsMC40NjYtMy41MTMsMC4yMDYtNS4yODQtMS42MDljLTEuMDk4LTEuMTI1LTEuNTA4LTEuMzcyLTEuNzE3LTEuMDMgYy0wLjA1MSwwLjA4My0wLjI1MywwLjE1MS0wLjQ0NiwwLjE1MWMtMC4yNTgsMC0wLjQ4NC0wLjIwMy0wLjg0MS0wLjc2Yy0wLjI2Ny0wLjQxNS0wLjY3Ny0wLjg5My0wLjkwOS0xLjA1OCBjLTEuMjc0LTAuOTA2LTIuNzE1LTAuMjUtMi43MTUsMS4yNGMwLDAuNjE0LDEuMjI4LDIuNDY1LDEuODQ3LDIuNzg2YzAuO
DE4LDAuNDIzLDAuMzU3LDEuMDU3LTAuNjcxLDAuOTE5IGMtMC41MzItMC4wNzItMC43NjctMC4yMzEtMS40ODItMS4wMDNjLTAuNzY0LTAuODI2LTAuOTA3LTAuOTE2LTEuNDQzLTAuOTE0Yy0wLjMyOCwwLjAwMi0wLjg3NSwwLjEzMS0xLjIxNSwwLjI4NCBjLTAuODc3LDAuMzk5LTIuMDIyLDAuNjItMy4yMjEsMC42Mkg1Ljg2NUw1LjYzLDMxLjE0NkM1LjA5MywyOS4xNzEsNSwyOC40LDUsMjUuOTcxYzAtMS44NTcsMC4wNzEtMi44MjEsMC4yNzktMy43NzkgYzEuMTI
0LTUuMTcyLDMuOTg0LTkuMzUzLDguMzIzLTEyLjE2OWMyLjEyOC0xLjM4Miw0LjA4NC0yLjE5Miw2LjU3OS0yLjcyOWMxLjc4My0wLjM4Myw1Ljc3Ni0wLjM5NCw3LjUxLTAuMDIxIGM1LjkwMiwxLjI3MywxMC43OTQsNC45OTYsMTMuMzM5LDEwLjE2QzQyLjQ1NywyMC4zMjUsNDMsMjIuNjc3LDQzLDI1Ljk3MXoiLz48cGF0aCBmaWxsPSIjRkYzRDAwIiBkPSJNMzguMDgyLDIyLjMxMmMtMC43MTEtMC41MzktMi41MjktMS4zMjctMy4wNjMtMS4zMjdjLTAuMTQ2LDAtM
C40LDAuMjczLTAuNTk4LDAuNjQzIGMtMC4yOTcsMC41NTktMS4xMzEsMS4zMjItMS40NDQsMS4zMjJjLTAuMDU4LDAsMC4xMDEtMC4zODQsMC4zNS0wLjg1MmMwLjExLTAuMjA1LDAuMjA0LTAuNDIzLDAuMjgtMC42MzEgYzAuMTA5LTAuMzAzLDAuMTc4LTAuNTg0LDAuMTc4LTAuNzc2YzAtMC45NTgtMC42NzYtMy4xMTctMS40OTctNS4wNDljLTAuNDM1LTEuMDI0LTAuOTExLTEuOTg1LTEuMzUxLTIuNjY5IGMtMC4yMTUtMC4zMzUtMC4zOTMtMC41ODktMC41NTktMC4
3OTJjLTAuMjktMC4zNTEtMC41NTktMC41NTQtMC45NzUtMC43NzljLTAuMjgzLTAuMTUyLTAuNTgtMC4yNy0wLjg5MS0wLjM1NiBjLTAuMDA2LDAtMC4wMTItMC4wMDMtMC4wMTYtMC4wMDNjLTAuMDczLTAuMDIxLTAuMTQ1LTAuMDM5LTAuMjE5LTAuMDU1Yy0wLjAxMi0wLjAwMy0wLjAyMy0wLjAwNS0wLjAzNS0wLjAwOSBjLTAuMDctMC4wMTQtMC4xMzktMC4wMjYtMC4yMTItMC4wMzljLTAuMDE1LTAuMDAzLTAuMDI4LTAuMDAzLTAuMDQ0LTAuMDA2Yy0wLjA3Mi0wL
jAxMi0wLjE0Mi0wLjAyMS0wLjIxNS0wLjAzIGMtMC4wMTItMC4wMDEtMC4wMjUtMC4wMDEtMC4wMzctMC4wMDNjLTAuMTYtMC4wMTYtMC4zMjUtMC4wMjctMC40OTUtMC4wMjdjLTAuMDE3LDAtMC4wMzYsMC0wLjA1MywwIGMtMC4xMiwwLTAuMjM5LDAuMDA2LTAuMzYyLDAuMDE0Yy0wLjAzOSwwLjAwMS0wLjA3NiwwLjAwNC0wLjExNCwwLjAwNWMtMC4xMzIsMC4wMTEtMC4yNjcsMC4wMjQtMC40MDIsMC4wNDIgYy0wLjA0OCwwLjAwNi0wLjA5NSwwLjAxMy0wLjE0NCw
wLjAxOWMtMC4xMzQsMC4wMTktMC4yNywwLjA0MS0wLjQwOCwwLjA2OGMtMC4wNTUsMC4wMS0wLjExMiwwLjAyNC0wLjE3LDAuMDM2IGMtMC4xMDYsMC4wMjItMC4yMTgsMC4wNDgtMC4zMjksMC4wNzVjLTAuMDQzLDAuMDEtMC4wODMsMC4wMTctMC4xMjYsMC4wMjdjLTEuMDYzLDAuMjcyLTEuNTU4LDAuMzE5LTIuNTY2LDAuMjQ1IGMtNC4xMTctMC4yOTctMy45NTEtMC4zLTQuNjMsMC4wNDdjLTAuMzY4LDAuMTg4LTAuNjI4LDAuMzktMC44NDgsMC43M2MtMC4yOTgsM
C40NjItMC41MiwxLjE3Mi0wLjgyOSwyLjQ1MSBjLTAuNTgyLDIuNDAzLTAuNTk1LDMuMTQ2LTAuMDY4LDMuNzczYzAuNTM2LDAuNjM5LDIuMjY1LDEuNDMxLDMuOTksMS44MjdjMi4wNjMsMC40NzQsMy40NjksMC44OTIsMy44NTksMS4xNDkgYzAuMDk3LDAuMDYzLDAuMjA1LDAuMTc4LDAuMzAxLDAuMzEzYzAuMDkxLDAuMTI1LDAuMTcsMC4yNjYsMC4yMTgsMC4zOTRjMC40MzQsMS4xNTcsMC42NjcsMS42MjcsMC45MzEsMS44NjQgYzAuMDY1LDAuMDYsMC4xMjUsMC4
xMzEsMC4xNzQsMC4xOTdjMC4wNjYsMC4wOTIsMC4xMDcsMC4xODMsMC4xMDcsMC4yMzljMCwxLjA5Ny03LjgwMy0xLjMzMy05LjgxNC0zLjA1NCBjLTAuNTM4LTAuNDYyLTAuODM3LTAuODQ2LTAuOTY1LTEuMjg4Yy0wLjA3Ny0wLjI3NC0wLjA4Ny0wLjU3MS0wLjA0Ni0wLjkyMmwwLjAyNC0wLjIxMWwwLjA2My0wLjUzNGgtMC4wMDF2LTAuMDAybC0xLjk5OCwwLjA2NiBjLTEuNjg4LDAuMDU2LTIuMTA5LDAuMTE4LTIuNzE1LDAuNDEyYy0yLjA4NiwxLjAwNS0xLjk4N
SwyLjY5LDAuMjg4LDQuODVjMS42MzQsMS41NTQsNC4xMTMsMy4wODUsNi44NTMsNC4zMzQgYzIuODI4LDEuMjkzLDUuOTM2LDIuMjgzLDguNjgxLDIuNjg2YzEuMTg4LDAuMTc1LDIuNzEyLDAuMjM3LDQuMTEzLDAuMTkzYzAuNzkxLTAuMDI0LDEuNTQzLTAuMDg0LDIuMTctMC4xOCBjMy4zNjUtMC41MTMsNS42OTUtMS44ODYsNi42NjgtMy45MzRjMC4xMDUtMC4yMjIsMC4xODktMC40MzgsMC4yNTgtMC42NTJDMzkuODE4LDI0LjY1NCwzOS4zNzcsMjMuMjk3LDM4LjA
4MiwyMi4zMTJ6IE0yMi4xMjIsMTYuMDE1Yy0xLjQzOCwwLjE1OS0yLjE0MSwwLjM1NS0yLjQwMywwLjY3MWMtMC4xMDEsMC4xMjEtMC4yMDQsMC4xODgtMC4zMjcsMC4xOTdjLTAuMjQ5LDAuMDE3LTAuNTctMC4yMTUtMS4wODktMC43NDIgYy0wLjM2MS0wLjM3MS0wLjU2Ny0wLjYzNy0wLjY2Ny0wLjg2N2MtMC4wNS0wLjExNS0wLjA3My0wLjIyNi0wLjA3My0wLjMzMmMwLTAuMDYxLDAuMDA0LTAuMTIsMC4wMS0wLjE3OCBjMC4wMDEtMC4wMDgsMC4wMDMtMC4wMTYsM
C4wMDMtMC4wMjNjMC4wMDctMC4wNDcsMC4wMTYtMC4wOTMsMC4wMjgtMC4xMzljMC4wMDEtMC4wMDksMC4wMDMtMC4wMTUsMC4wMDUtMC4wMjMgYzAuMDE0LTAuMDQ4LDAuMDI5LTAuMDk0LDAuMDQ5LTAuMTM3YzAuMDAzLTAuMDA3LDAuMDA1LTAuMDEzLDAuMDEtMC4wMTljMC4wMTctMC4wMzIsMC4wMzItMC4wNjMsMC4wNTEtMC4wOTMgYzAuMDA3LTAuMDEzLDAuMDE1LTAuMDI0LDAuMDIzLTAuMDM1YzAuMDE0LTAuMDI0LDAuMDMyLTAuMDQ2LDAuMDUtMC4wNjdjMC4
wMDctMC4wMDksMC4wMTUtMC4wMTcsMC4wMjMtMC4wMjcgYzAuMDI1LTAuMDI0LDAuMDUxLTAuMDQ3LDAuMDc3LTAuMDY1YzAuMDEyLTAuMDA3LDAuMDIzLTAuMDE1LDAuMDM2LTAuMDJjMC4wMTktMC4wMTQsMC4wMzgtMC4wMjEsMC4wNTgtMC4wMyBjMC4wMTItMC4wMDYsMC4wMjUtMC4wMTEsMC4wMzctMC4wMTVjMC4wMjktMC4wMDgsMC4wNTctMC4wMTYsMC4wODgtMC4wMTljMC4wMDMsMCwwLjAwNi0wLjAwMSwwLjAxLTAuMDAxIGMwLjAzMi0wLjAwMywwLjA2NC0wL
jAwMiwwLjA5NywwLjAwM2MwLjAwNywwLjAwMSwwLjAxNSwwLjAwMywwLjAyMywwLjAwNGMwLjAyNiwwLjAwNiwwLjA1MywwLjAxMiwwLjA3OSwwLjAyMSBjMC4wMDcsMC4wMDMsMC4wMTQsMC4wMDYsMC4wMjEsMC4wMDljMC4wMzEsMC4wMTQsMC4wNjIsMC4wMzEsMC4wOTEsMC4wNTFjMC4wMDIsMC4wMDIsMC4wMDQsMC4wMDMsMC4wMDcsMC4wMDYgYzAuMDM2LDAuMDIzLDAuMDcxLDAuMDUsMC4xMDQsMC4wODNjMC42NjgsMC42NTMsMC45LDAuNzE0LDIuMzk0LDAuNjE
1YzAuNTQ2LTAuMDM3LDEuMDEyLTAuMDM4LDEuMzk0LTAuMDA3IGMwLjcwOSwwLjA2LDEuMTMxLDAuMjMsMS4yNCwwLjUxNWMwLjAzOCwwLjA5OSwwLjA1LDAuMTgsMC4wMjUsMC4yNUMyMy41MjIsMTUuODA5LDIzLjEyMiwxNS45MDQsMjIuMTIyLDE2LjAxNXogTTI5LjAwMSwxMy4zOCBjLTAuMDEsMC4wMDctMC4wMTksMC4wMTMtMC4wMzEsMC4wMTljMCwwLjAwMS0wLjAwMywwLjAwMy0wLjAwNiwwLjAwNGMtMC41MTEsMC4yNjktMS4zMjUsMC40MjktMi4xNzEsMC4zN
TggYy0wLjM0NC0wLjAyNy0wLjUyNi0wLjA4Ny0wLjU0My0wLjE2NGMtMC4wMTItMC4wNTIsMC4wNTUtMC4xMTEsMC4xOTctMC4xNzFjMC4wNy0wLjAzLDAuMTUzLTAuMDYxLDAuMjU2LTAuMDg5IGMwLjU4NC0wLjE2OSwwLjkxLTAuNjA4LDAuNTk0LTAuODA1Yy0wLjMzNy0wLjIxMi0xLjQ5Mi0wLjM0NS0yLjAwNC0wLjIzMWMtMC4yMTcsMC4wNDktMC4zNDIsMC4wNTEtMC4zNzQsMC4wMTMgYy0wLjAwMS0wLjAwMS0wLjAwNC0wLjAwMy0wLjAwNS0wLjAwNmMtMC4wMDk
tMC4wMTUtMC4wMDQtMC4wMzUsMC4wMTEtMC4wNjJjMC4wMTItMC4wMTYsMC4wMjYtMC4wMzEsMC4wNDctMC4wNDggYzAsMCwwLjAwMSwwLDAuMDAyLDBjMC4wMTktMC4wMTgsMC4wNDUtMC4wMzIsMC4wNzItMC4wNDZjMC4wMDItMC4wMDMsMC4wMDYtMC4wMDMsMC4wMS0wLjAwNmMwLjAyNy0wLjAxNCwwLjA1Ny0wLjAyNywwLjA5Mi0wLjA0MSBjMC4wMDUtMC4wMDIsMC4wMDgtMC4wMDMsMC4wMTEtMC4wMDVjMC4wMzctMC4wMTQsMC4wNzctMC4wMjgsMC4xMjEtMC4wN
DJjMC4wMDEsMCwwLjAwMy0wLjAwMiwwLjAwNS0wLjAwMiBjMC4yNzYtMC4wOSwwLjY4Mi0wLjE2NCwxLjEtMC4yMTNjMC4wMDgtMC4wMDMsMC4wMTQtMC4wMDMsMC4wMjEtMC4wMDNjMC4wNjEtMC4wMDksMC4xMjMtMC4wMTQsMC4xODYtMC4wMiBjMC4wMDktMC4wMDIsMC4wMTktMC4wMDIsMC4wMjctMC4wMDNjMC4wNjEtMC4wMDQsMC4xMTktMC4wMDksMC4xOC0wLjAxM2MwLjAwOSwwLDAuMDE5LTAuMDAyLDAuMDI3LTAuMDAyIGMwLjA2NS0wLjAwMywwLjEzMS0wLjAwNywwLjE5My0wLjAwOWMwLjAwNCwwLDAuMDA2LDAsMC4wMDYsMGMwLjIwNi0wLjAwOCwwLjQtMC4wMDIsMC41NzEsMC4wMTIgYzAuOTQ4LDAuMDg2LDEuNzE2LDAuNDIxLDEuODY4LDAuODEzYzAuMDE1LDAuMDQsMC4wMjQsMC4wOCwwLjAyNywwLjEyM0MyOS41MSwxMi45NzksMjkuMzIsMTMuMjAxLDI5LjAwMSwxMy4zOHoiLz48L3N2Zz4K</data><downloads_count>0</downloads_count></logo>
<finalclass>OS Family</finalclass>
<friendlyname>Red Hat</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="5">
<name>Ubuntu</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-ubuntu.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00MCwyNGMwLDguOC03LjIsMTYtMTYsMTZTOCwzMi44LDgsMjRTMTUuMiw4LDI0LDhTNDAsMTUuMiw0MCwyNHoiLz48cGF0aC
BmaWxsPSIjZTY0YTE5IiBkPSJNMzAgMjRjMCAzLjMtMi43IDYtNiA2LTMuMyAwLTYtMi43LTYtNnMyLjctNiA2LTZDMjcuMyAxOCAzMCAyMC43IDMwIDI0ek0xMiAyMS41YzEuNCAwIDIuNSAxLjEgMi41IDIuNXMtMS4xIDIuNS0yLjUgMi41UzkuNSAyNS40IDkuNSAyNCAxMC42IDIxLjUgMTIgMjEuNU0xMiAyMC41Yy0xLjkgMC0zLjUgMS42LTMuNSAzLjVzMS42IDMuNSAzLjUgMy41IDMuNS0xLjYgMy41LTMuNVMxMy45IDIwLjUgMTIgMjAuNXpNMzAgMzEuNWMxLjQg
MCAyLjUgMS4xIDIuNSAyLjVzLTEuMSAyLjUtMi41IDIuNS0yLjUtMS4xLTIuNS0yLjVTMjguNiAzMS41IDMwIDMxLjVNMzAgMzAuNWMtMS45IDAtMy41IDEuNi0zLjUgMy41czEuNiAzLjUgMy41IDMuNSAzLjUtMS42IDMuNS0zLjVTMzEuOSAzMC41IDMwIDMwLjV6Ii8+PHBhdGggZmlsbD0iI2U2NGExOSIgZD0iTTI0LDRDMTMsNCw0LDEzLDQsMjRzOSwyMCwyMCwyMHMyMC05LDIwLTIwUzM1LDQsMjQsNHogTTMyLjUsMzRjMCwxLjQtMS4xLDIuNS0yLjUsMi41IHMtMi
41LTEuMS0yLjUtMi41YzAtMC4yLDAtMC41LDAuMS0wLjdDMjYuNSwzMy44LDI1LjMsMzQsMjQsMzRjLTUuMSwwLTkuMi0zLjgtOS45LTguN2MtMC40LDAuNy0xLjIsMS4yLTIuMSwxLjIgYy0xLjQsMC0yLjUtMS4xLTIuNS0yLjVzMS4xLTIuNSwyLjUtMi41YzAuOSwwLDEuNywwLjUsMi4xLDEuMmMwLjctNC45LDQuOC04LjcsOS45LTguN2MxLjMsMCwyLjUsMC4yLDMuNiwwLjcgYy0wLjEtMC4yLTAuMS0wLjQtMC4xLTAuN2MwLTEuNCwxLjEtMi41LDIuNS0yLjVzMi41
LDEuMSwyLjUsMi41YzAsMS4yLTAuOCwyLjItMiwyLjRDMzIuNywxOC4zLDM0LDIxLDM0LDI0cy0xLjMsNS43LTMuNSw3LjYgQzMxLjcsMzEuOCwzMi41LDMyLjgsMzIuNSwzNHoiLz48cGF0aCBmaWxsPSIjZTY0YTE5IiBkPSJNMzAgMTEuNWMxLjQgMCAyLjUgMS4xIDIuNSAyLjUgMCAxLjQtMS4xIDIuNS0yLjUgMi41cy0yLjUtMS4xLTIuNS0yLjVDMjcuNSAxMi42IDI4LjYgMTEuNSAzMCAxMS41TTMwIDEwLjVjLTEuOSAwLTMuNSAxLjYtMy41IDMuNXMxLjYgMy41ID
MuNSAzLjUgMy41LTEuNiAzLjUtMy41UzMxLjkgMTAuNSAzMCAxMC41ek0yNCAyNGMtMi42LTQuMS01LjItOC4xLTcuOC0xMi4yIi8+PHBhdGggZmlsbD0iI2U2NGExOSIgZD0iTTE5LjEgMTAuN0gyMS4xVjI1LjFIMTkuMXoiIHRyYW5zZm9ybT0icm90YXRlKC0zMi40NjcgMjAuMTI3IDE3LjkxMSkiLz48cGF0aCBmaWxsPSIjZTY0YTE5IiBkPSJNMjQgMjNIMzguNFYyNUgyNHoiLz48Zz48cGF0aCBmaWxsPSIjZTY0YTE5IiBkPSJNMjQsMjRjLTIuNyw0LTUuMyw4LTgsMTIiLz48cGF0aCBmaWxsPSIjZTY0YTE5IiBkPSJNMTIuOCAyOUgyNy4yMDAwMDAwMDAwMDAwMDNWMzFIMTIuOHoiIHRyYW5zZm9ybT0icm90YXRlKC01Ni4zMTIgMTkuOTk4IDMwLjAwNikiLz48L2c+PC9zdmc+Cg==</data><downloads_count>0</downloads_count></logo>
<finalclass>OS Family</finalclass>
<friendlyname>Ubuntu</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="6">
<name>Ubuntu server</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-ubuntu.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik00MCwyNGMwLDguOC03LjIsMTYtMTYsMTZTOCwzMi44LDgsMjRTMTUuMiw4LDI0LDhTNDAsMTUuMiw0MCwyNHoiLz48cGF0aC
BmaWxsPSIjZTY0YTE5IiBkPSJNMzAgMjRjMCAzLjMtMi43IDYtNiA2LTMuMyAwLTYtMi43LTYtNnMyLjctNiA2LTZDMjcuMyAxOCAzMCAyMC43IDMwIDI0ek0xMiAyMS41YzEuNCAwIDIuNSAxLjEgMi41IDIuNXMtMS4xIDIuNS0yLjUgMi41UzkuNSAyNS40IDkuNSAyNCAxMC42IDIxLjUgMTIgMjEuNU0xMiAyMC41Yy0xLjkgMC0zLjUgMS42LTMuNSAzLjVzMS42IDMuNSAzLjUgMy41IDMuNS0xLjYgMy41LTMuNVMxMy45IDIwLjUgMTIgMjAuNXpNMzAgMzEuNWMxLjQg
MCAyLjUgMS4xIDIuNSAyLjVzLTEuMSAyLjUtMi41IDIuNS0yLjUtMS4xLTIuNS0yLjVTMjguNiAzMS41IDMwIDMxLjVNMzAgMzAuNWMtMS45IDAtMy41IDEuNi0zLjUgMy41czEuNiAzLjUgMy41IDMuNSAzLjUtMS42IDMuNS0zLjVTMzEuOSAzMC41IDMwIDMwLjV6Ii8+PHBhdGggZmlsbD0iI2U2NGExOSIgZD0iTTI0LDRDMTMsNCw0LDEzLDQsMjRzOSwyMCwyMCwyMHMyMC05LDIwLTIwUzM1LDQsMjQsNHogTTMyLjUsMzRjMCwxLjQtMS4xLDIuNS0yLjUsMi41IHMtMi
41LTEuMS0yLjUtMi41YzAtMC4yLDAtMC41LDAuMS0wLjdDMjYuNSwzMy44LDI1LjMsMzQsMjQsMzRjLTUuMSwwLTkuMi0zLjgtOS45LTguN2MtMC40LDAuNy0xLjIsMS4yLTIuMSwxLjIgYy0xLjQsMC0yLjUtMS4xLTIuNS0yLjVzMS4xLTIuNSwyLjUtMi41YzAuOSwwLDEuNywwLjUsMi4xLDEuMmMwLjctNC45LDQuOC04LjcsOS45LTguN2MxLjMsMCwyLjUsMC4yLDMuNiwwLjcgYy0wLjEtMC4yLTAuMS0wLjQtMC4xLTAuN2MwLTEuNCwxLjEtMi41LDIuNS0yLjVzMi41
LDEuMSwyLjUsMi41YzAsMS4yLTAuOCwyLjItMiwyLjRDMzIuNywxOC4zLDM0LDIxLDM0LDI0cy0xLjMsNS43LTMuNSw3LjYgQzMxLjcsMzEuOCwzMi41LDMyLjgsMzIuNSwzNHoiLz48cGF0aCBmaWxsPSIjZTY0YTE5IiBkPSJNMzAgMTEuNWMxLjQgMCAyLjUgMS4xIDIuNSAyLjUgMCAxLjQtMS4xIDIuNS0yLjUgMi41cy0yLjUtMS4xLTIuNS0yLjVDMjcuNSAxMi42IDI4LjYgMTEuNSAzMCAxMS41TTMwIDEwLjVjLTEuOSAwLTMuNSAxLjYtMy41IDMuNXMxLjYgMy41ID
MuNSAzLjUgMy41LTEuNiAzLjUtMy41UzMxLjkgMTAuNSAzMCAxMC41ek0yNCAyNGMtMi42LTQuMS01LjItOC4xLTcuOC0xMi4yIi8+PHBhdGggZmlsbD0iI2U2NGExOSIgZD0iTTE5LjEgMTAuN0gyMS4xVjI1LjFIMTkuMXoiIHRyYW5zZm9ybT0icm90YXRlKC0zMi40NjcgMjAuMTI3IDE3LjkxMSkiLz48cGF0aCBmaWxsPSIjZTY0YTE5IiBkPSJNMjQgMjNIMzguNFYyNUgyNHoiLz48Zz48cGF0aCBmaWxsPSIjZTY0YTE5IiBkPSJNMjQsMjRjLTIuNyw0LTUuMyw4LTgsMTIiLz48cGF0aCBmaWxsPSIjZTY0YTE5IiBkPSJNMTIuOCAyOUgyNy4yMDAwMDAwMDAwMDAwMDNWMzFIMTIuOHoiIHRyYW5zZm9ybT0icm90YXRlKC01Ni4zMTIgMTkuOTk4IDMwLjAwNikiLz48L2c+PC9zdmc+Cg==</data><downloads_count>0</downloads_count></logo>
<finalclass>OS Family</finalclass>
<friendlyname>Ubuntu server</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="7">
<name>vCenter Server</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-vmware.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNTAgNTAiIHdpZHRoPSI1MHB4IiBoZWlnaHQ9IjUwcHgiPjxwYXRoIGQ9Ik0gNDIuNDE0MDYzIDE1IEMgMzguODI0MjE5IDE1IDM2LjU3NDIxOSAxNy41IDM2LjU3NDIxOSAxNy41IEMgMzUuMzc4OTA2IDE1Ljk0MTQwNiAzMy43MzA0NjkgMTUuMDAzOTA2IDMwLjk0MTQwNiAxNS4wMDM5MDYgQyAyNy45OTYwOTQgMTUuMDAzOTA2IDI2LjA0Mjk2OSAxNy41IDI2LjA0Mjk2OSAxNy41IEMgMjQuODQ3NjU2IDE1Ljk0MTQwNiAyMi42ODc1IDE1IDIxIDE1IEMgMTguMzkwNjI1IDE1IDE2LjMyMDMxMyAxNi4xNTIzNDQgMTUuMDU0Njg4IDE5LjA1ODU5NCBMIDEwLjgyMDMxMyAyOC4zMjAzMTMgTCA2LjAzMTI1IDE2LjU1ODU5NCBDIDUuNDI1NzgxIDE1LjIyNjU2MyAzLjkzMzU5NCAxNC42MjUgMi41NDI5NjkgMTUuMjQ2MDk0IEMgMS4xNDg0MzggMTUuODcxMDk0IDAuNjM2NzE5IDE3LjQyNTc4MSAxLjI2NTYyNSAxOC43NTc4MTMgTCA3LjExMzI4MSAzMS45NDUzMTMgQyA4LjAzMTI1IDMzLjk0OTIxOSA5LjAwMzkwNiAzNSAxMC44MjAzMTMgMzUgQyAxMi43NjU2MjUgMzUgMTMuNjA5Mzc1IDMzLjg1NTQ2OSAxNC41MzEyNSAzMS45NDUzMTMgQyAxNC41MzEyNSAzMS45NDUzMTMgMTguNTExNzE5IDIzLjA2MjUgMTkgMjIgQyAxOS40ODgyODEgMjAuOTM3NSAyMC4zMDA3ODEgMjAgMjEuNSAyMCBDIDIyLjg3NSAyMCAyNCAyMS4xMjUgMjQgMjIuNSBMIDI0IDMyLjM3NSBDIDI0IDMzLjgyMDMxMyAyNS4wODU5MzggMzUgMjYuNTIzNDM4IDM1IEMgMjcuOTU3MDMxIDM1IDI5IDMzLjgyMDMxMyAyOSAzMi4zNzUgTCAyOSAyMi41IEMgMjkgMjEuMTI1IDMwLjEyNSAyMCAzMS41IDIwIEMgMzIuODc1IDIwIDM0IDIxLjEyNSAzNCAyMi41IEwgMzQgMzIuNSBDIDM0IDMzLjg3NSAzNS4xMjUgMzUgMzYuNSAzNSBDIDM3Ljg3NSAzNSAzOSAzMy44NzUgMzkgMzIuNSBMIDM5IDIyLjUgQyAzOSAyMS4xMjUgNDAuMTI1IDIwIDQxLjUgMjAgQyA0Mi44NzUgMjAgNDQgMjEuMTI1IDQ0IDIyLjUgTCA0NCAzMi41IEMgNDQgMzMuODc1IDQ1LjEyNSAzNSA0Ni41IDM1IEMgNDcuODc1IDM1IDQ5IDMzLjg3NSA0OSAzMi41IEwgNDkgMjEuMzU1NDY5IEMgNDkgMTcuNjE3MTg4IDQ2LjAxMTcxOSAxNSA0Mi40MTQwNjMgMTUgWiIvPjwvc3ZnPgo=</data><downloads_count>0</downloads_count></logo>
<finalclass>OS Family</finalclass>
<friendlyname>vCenter Server</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="8">
<name>Windows</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-windows.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiMxOTc2ZDIiIGQ9Ik02LDZoMTd2MTdINlY2eiIvPjxwYXRoIGZpbGw9IiMxOTc2ZDIiIGQ9Ik0yNS4wNDIsMjIuOTU4VjZINDJ2MTYuOTU4SDI1LjA0MnoiLz48cGF0aCBmaWxsPSIjMTk3NmQyIiBkPSJNNiwyNWgxN3YxN0g2VjI1eiIvPjxwYXRoIGZpbGw9IiMxOTc2ZDIiIGQ9Ik0yNSw0MlYyNWgxN3YxN0gyNXoiLz48L3N2Zz4K</data><downloads_count>0</downloads_count></logo>
<finalclass>OS Family</finalclass>
<friendlyname>Windows</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="9">
<name>Windows server</name>
<logo><mimetype>image/svg+xml</mimetype><filename>icons8-windows-server.svg</filename><data>PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiMwMGIwZmYiIGQ9Ik0yMCAyNS4wMjZMNS4wMTEgMjUgNS4wMTIgMzcuNzQ0IDIwIDM5LjgxOHpNMjIgMjUuMDNMMjIgNDAuMDk1IDQyLjk5NSA0MyA0MyAyNS4wNjZ6TTIwIDguMjU2TDUgMTAuMzggNS4wMTQgMjMgMjAgMjN6TTIyIDcuOTczTDIyIDIzIDQyLjk5NSAyMyA0Mi45OTUgNXoiLz48L3N2Zz4K</data><downloads_count>0</downloads_count></logo>
<finalclass>OS Family</finalclass>
<friendlyname>Windows server</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="1">
<name>Arch</name>
<finalclass>OS Family</finalclass>
<friendlyname>Arch</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="2">
<name>Debian</name>
<finalclass>OS Family</finalclass>
<friendlyname>Debian</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="3">
<name>Oracle Linux</name>
<finalclass>OS Family</finalclass>
<friendlyname>Oracle Linux</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="4">
<name>Red Hat</name>
<finalclass>OS Family</finalclass>
<friendlyname>Red Hat</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="5">
<name>Ubuntu</name>
<finalclass>OS Family</finalclass>
<friendlyname>Ubuntu</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="6">
<name>Ubuntu server</name>
<finalclass>OS Family</finalclass>
<friendlyname>Ubuntu server</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="7">
<name>vCenter Server</name>
<finalclass>OS Family</finalclass>
<friendlyname>vCenter Server</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="8">
<name>Windows</name>
<finalclass>OS Family</finalclass>
<friendlyname>Windows</friendlyname>
</OSFamily>
<OSFamily alias="OSFamily" id="9">
<name>Windows server</name>
<finalclass>OS Family</finalclass>
<friendlyname>Windows server</friendlyname>
</OSFamily>
</Set>

View File

@@ -4461,9 +4461,6 @@
<attribute id="name"/>
</attributes>
</naming>
<fields_semantic>
<image_attribute>logo</image_attribute>
</fields_semantic>
<style>
<icon/>
</style>
@@ -4473,15 +4470,7 @@
</attributes>
</reconciliation>
</properties>
<fields>
<field id="logo" xsi:type="AttributeImage">
<display_max_width>96</display_max_width>
<display_max_height>96</display_max_height>
<storage_max_width>128</storage_max_width>
<storage_max_height>128</storage_max_height>
<is_null_allowed>true</is_null_allowed>
</field>
</fields>
<fields/>
<methods/>
<presentation>
<details>
@@ -4489,9 +4478,6 @@
<item id="name">
<rank>10</rank>
</item>
<item id="logo">
<rank>20</rank>
</item>
</items>
</details>
<search>
@@ -4506,9 +4492,6 @@
<item id="name">
<rank>10</rank>
</item>
<item id="logo">
<rank>20</rank>
</item>
</items>
</list>
</presentation>
@@ -4591,9 +4574,6 @@
<item id="name">
<rank>10</rank>
</item>
<item id="logo">
<rank>20</rank>
</item>
</items>
</list>
<summary>

View File

@@ -1104,7 +1104,7 @@ Dict::Add('EN US', 'English', 'English', [
//
Dict::Add('EN US', 'English', 'English', [
'Class:Model' => 'Device Model',
'Class:Model' => 'Model',
'Class:Model+' => '',
'Class:Model/ComplementaryName' => '%1$s - %2$s',
'Class:Model/Attribute:brand_id' => 'Brand',

View File

@@ -1240,7 +1240,7 @@ Dict::Add('FR FR', 'French', 'Français', [
//
Dict::Add('FR FR', 'French', 'Français', [
'Class:Model' => 'Modèle de matériel',
'Class:Model' => 'Modèle',
'Class:Model+' => '',
'Class:Model/ComplementaryName' => '%1$s - %2$s',
'Class:Model/Attribute:brand_id' => 'Marque',

View File

@@ -19,7 +19,7 @@ Dict::Add('FR FR', 'French', 'Français', [
'Class:FAQ/Attribute:summary+' => '',
'Class:FAQ/Attribute:description' => 'Description',
'Class:FAQ/Attribute:description+' => '',
'Class:FAQ/Attribute:category_id' => 'Catégorie',
'Class:FAQ/Attribute:category_id' => 'Categorie',
'Class:FAQ/Attribute:category_id+' => '',
'Class:FAQ/Attribute:category_name' => 'Nom catégorie',
'Class:FAQ/Attribute:category_name+' => '',

View File

@@ -238,11 +238,6 @@
<action id="action:bulk delete">allow</action>
</actions>
</group>
<group id="Ticketing">
<actions>
<action id="stimulus:ev_close">allow</action>
</actions>
</group>
<group id="UserRequest">
<actions>
<action id="stimulus:ev_approve">allow</action>

View File

@@ -307,9 +307,7 @@
</field>
<field id="servicesubcategory_id" xsi:type="AttributeExternalKey">
<filter><![CDATA[SELECT ServiceSubcategory WHERE service_id = :this->service_id AND status != 'obsolete']]></filter>
<dependencies>
<attribute id="service_id"/>
</dependencies>
<dependencies/>
<sql>servicesubcategory_id</sql>
<target_class>ServiceSubcategory</target_class>
<is_null_allowed>true</is_null_allowed>

View File

@@ -243,7 +243,7 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', [
'Class:Service/Attribute:description' => 'Popis',
'Class:Service/Attribute:description+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Balíček služeb',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Název rodiny služeb',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:documents_list' => 'Dokumenty',

View File

@@ -242,7 +242,7 @@ Dict::Add('DA DA', 'Danish', 'Dansk', [
'Class:Service/Attribute:description' => 'Beskrivelse',
'Class:Service/Attribute:description+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Service familie',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Ydelses familie navn',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:documents_list' => 'Dokument',
@@ -500,7 +500,7 @@ Dict::Add('DA DA', 'Danish', 'Dansk', [
'Class:DeliveryModel/Attribute:description' => 'Beskrivelse',
'Class:DeliveryModel/Attribute:description+' => '',
'Class:DeliveryModel/Attribute:contacts_list' => 'Kontakt',
'Class:DeliveryModel/Attribute:contacts_list+' => 'There must be at least one team to enable Ticket assignment~~',
'Class:DeliveryModel/Attribute:contacts_list+' => 'All the contacts (Teams and Persons) for this delivery model~~',
'Class:DeliveryModel/Attribute:customers_list' => 'Kunde',
'Class:DeliveryModel/Attribute:customers_list+' => 'All the customers having this delivering model~~',
]);

View File

@@ -242,7 +242,7 @@ Dict::Add('DE DE', 'German', 'Deutsch', [
'Class:Service/Attribute:description' => 'Beschreibung',
'Class:Service/Attribute:description+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Service-Familie',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Service-Familien-Name',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:documents_list' => 'Dokumente',

View File

@@ -268,7 +268,7 @@ Dict::Add('EN US', 'English', 'English', [
'Class:Service/Attribute:description' => 'Description',
'Class:Service/Attribute:description+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Service Family',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Service Family Name',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:documents_list' => 'Documents',
@@ -526,7 +526,7 @@ Dict::Add('EN US', 'English', 'English', [
'Class:DeliveryModel/Attribute:description' => 'Description',
'Class:DeliveryModel/Attribute:description+' => '',
'Class:DeliveryModel/Attribute:contacts_list' => 'Contacts',
'Class:DeliveryModel/Attribute:contacts_list+' => 'There must be at least one team to enable Ticket assignment',
'Class:DeliveryModel/Attribute:contacts_list+' => 'All the contacts (Teams and Persons) for this delivery model',
'Class:DeliveryModel/Attribute:customers_list' => 'Customers',
'Class:DeliveryModel/Attribute:customers_list+' => 'All the customers having this delivering model',
]);

View File

@@ -268,7 +268,7 @@ Dict::Add('EN GB', 'British English', 'British English', [
'Class:Service/Attribute:description' => 'Description',
'Class:Service/Attribute:description+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Service Family',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Service Family Name',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:documents_list' => 'Documents',
@@ -526,7 +526,7 @@ Dict::Add('EN GB', 'British English', 'British English', [
'Class:DeliveryModel/Attribute:description' => 'Description',
'Class:DeliveryModel/Attribute:description+' => '',
'Class:DeliveryModel/Attribute:contacts_list' => 'Contacts',
'Class:DeliveryModel/Attribute:contacts_list+' => 'There must be at least one team to enable Ticket assignment',
'Class:DeliveryModel/Attribute:contacts_list+' => 'All the contacts (Teams and Persons) for this delivery model',
'Class:DeliveryModel/Attribute:customers_list' => 'Customers',
'Class:DeliveryModel/Attribute:customers_list+' => 'All the customers having this delivering model',
]);

View File

@@ -239,7 +239,7 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', [
'Class:Service/Attribute:description' => 'Descripción',
'Class:Service/Attribute:description+' => 'Descripción',
'Class:Service/Attribute:servicefamily_id' => 'Familia de Servicios',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => 'Familia de Servicios',
'Class:Service/Attribute:servicefamily_name' => 'Familia de Servicios',
'Class:Service/Attribute:servicefamily_name+' => 'Familia de Servicios',
'Class:Service/Attribute:documents_list' => 'Documentos',

View File

@@ -247,7 +247,7 @@ Dict::Add('FR FR', 'French', 'Français', [
'Class:Service/Attribute:description' => 'Description',
'Class:Service/Attribute:description+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Famille de service',
'Class:Service/Attribute:servicefamily_id+' => 'Obligatoire pour que ce service soit visible dans le portal utilisateur',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Nom Famille de service',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:documents_list' => 'Documents',
@@ -511,7 +511,7 @@ Dict::Add('FR FR', 'French', 'Français', [
'Class:DeliveryModel/Attribute:description' => 'Description',
'Class:DeliveryModel/Attribute:description+' => '',
'Class:DeliveryModel/Attribute:contacts_list' => 'Contacts',
'Class:DeliveryModel/Attribute:contacts_list+' => 'Il doit y avoir au moins une équipe pour permettre l\'assignation des Tickets',
'Class:DeliveryModel/Attribute:contacts_list+' => 'Tous les contacts (Equipe ou Personne) pour ce modèle de support',
'Class:DeliveryModel/Attribute:customers_list' => 'Clients',
'Class:DeliveryModel/Attribute:customers_list+' => 'Tous les clients ayant ce modèle de support',
'Class:DeliveryModel/Attribute:customers_list/UI:Links:Create:Button+' => 'Créer un %4$s',

View File

@@ -241,7 +241,7 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', [
'Class:Service/Attribute:description' => 'Leírás',
'Class:Service/Attribute:description+' => '~~',
'Class:Service/Attribute:servicefamily_id' => 'Szolgáltatáscsalád',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '~~',
'Class:Service/Attribute:servicefamily_name' => 'Szolgáltatáscsalád név',
'Class:Service/Attribute:servicefamily_name+' => '~~',
'Class:Service/Attribute:documents_list' => 'Dokumentumok',

View File

@@ -241,7 +241,7 @@ Dict::Add('IT IT', 'Italian', 'Italiano', [
'Class:Service/Attribute:description' => 'Descrizione',
'Class:Service/Attribute:description+' => '~~',
'Class:Service/Attribute:servicefamily_id' => 'Famiglia di Servizi',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '~~',
'Class:Service/Attribute:servicefamily_name' => 'Nome della Famiglia di Servizi',
'Class:Service/Attribute:servicefamily_name+' => '~~',
'Class:Service/Attribute:documents_list' => 'Documenti',

View File

@@ -241,7 +241,7 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
'Class:Service/Attribute:description' => '説明',
'Class:Service/Attribute:description+' => '',
'Class:Service/Attribute:servicefamily_id' => 'サービスファミリ',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'サービスファミリ名',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:documents_list' => '文書',
@@ -499,7 +499,7 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
'Class:DeliveryModel/Attribute:description' => '説明',
'Class:DeliveryModel/Attribute:description+' => '',
'Class:DeliveryModel/Attribute:contacts_list' => '連絡先',
'Class:DeliveryModel/Attribute:contacts_list+' => 'There must be at least one team to enable Ticket assignment~~',
'Class:DeliveryModel/Attribute:contacts_list+' => 'All the contacts (Teams and Persons) for this delivery model~~',
'Class:DeliveryModel/Attribute:customers_list' => '顧客',
'Class:DeliveryModel/Attribute:customers_list+' => 'All the customers having this delivering model~~',
]);

View File

@@ -243,7 +243,7 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', [
'Class:Service/Attribute:description' => 'Omschrijving',
'Class:Service/Attribute:description+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Servicecategorie',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Naam servicecategorie',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:documents_list' => 'Documenten',

View File

@@ -241,7 +241,7 @@ Dict::Add('PL PL', 'Polish', 'Polski', [
'Class:Service/Attribute:description' => 'Opis',
'Class:Service/Attribute:description+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Rodzina usług',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Nazwa rodziny usług',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:documents_list' => 'Dokumenty',

View File

@@ -241,7 +241,7 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', [
'Class:Service/Attribute:description' => 'Descrição',
'Class:Service/Attribute:description+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Família de serviços',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Nome da família de serviços',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:documents_list' => 'Documentos',

View File

@@ -242,7 +242,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'Class:Service/Attribute:description' => 'Описание',
'Class:Service/Attribute:description+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Пакет услуг',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Пакет услуг',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:documents_list' => 'Документы',

View File

@@ -241,7 +241,7 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', [
'Class:Service/Attribute:description' => 'Popis',
'Class:Service/Attribute:description+' => '~~',
'Class:Service/Attribute:servicefamily_id' => 'Kategória služieb',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '~~',
'Class:Service/Attribute:servicefamily_name' => 'Názov rodiny služieb',
'Class:Service/Attribute:servicefamily_name+' => '~~',
'Class:Service/Attribute:documents_list' => 'Dokumenty',
@@ -499,7 +499,7 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', [
'Class:DeliveryModel/Attribute:description' => 'Popis',
'Class:DeliveryModel/Attribute:description+' => '~~',
'Class:DeliveryModel/Attribute:contacts_list' => 'Kontakty',
'Class:DeliveryModel/Attribute:contacts_list+' => 'There must be at least one team to enable Ticket assignment~~',
'Class:DeliveryModel/Attribute:contacts_list+' => 'All the contacts (Teams and Persons) for this delivery model~~',
'Class:DeliveryModel/Attribute:customers_list' => 'Zákazníci',
'Class:DeliveryModel/Attribute:customers_list+' => 'All the customers having this delivering model~~',
]);

View File

@@ -241,7 +241,7 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', [
'Class:Service/Attribute:description' => 'Tanımlama',
'Class:Service/Attribute:description+' => '~~',
'Class:Service/Attribute:servicefamily_id' => 'Service Family~~',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '~~',
'Class:Service/Attribute:servicefamily_name' => 'Service Family Name~~',
'Class:Service/Attribute:servicefamily_name+' => '~~',
'Class:Service/Attribute:documents_list' => 'Documents~~',
@@ -499,7 +499,7 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', [
'Class:DeliveryModel/Attribute:description' => 'Description~~',
'Class:DeliveryModel/Attribute:description+' => '~~',
'Class:DeliveryModel/Attribute:contacts_list' => 'Contacts~~',
'Class:DeliveryModel/Attribute:contacts_list+' => 'There must be at least one team to enable Ticket assignment~~',
'Class:DeliveryModel/Attribute:contacts_list+' => 'All the contacts (Teams and Persons) for this delivery model~~',
'Class:DeliveryModel/Attribute:customers_list' => 'Customers~~',
'Class:DeliveryModel/Attribute:customers_list+' => 'All the customers having this delivering model~~',
]);

View File

@@ -264,7 +264,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:Service/Attribute:description' => '描述',
'Class:Service/Attribute:description+' => '',
'Class:Service/Attribute:servicefamily_id' => '服务系列',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => '服务系列名称',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:documents_list' => '文档',

View File

@@ -218,7 +218,7 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', [
'Class:Service/Attribute:organization_name' => 'Název poskytovatele',
'Class:Service/Attribute:organization_name+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Balíček služeb',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Název rodiny služeb',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:description' => 'Popis',

View File

@@ -217,7 +217,7 @@ Dict::Add('DA DA', 'Danish', 'Dansk', [
'Class:Service/Attribute:organization_name' => 'Leverandør navn',
'Class:Service/Attribute:organization_name+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Service familie',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Ydelses familie navn',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:description' => 'Beskrivelse',
@@ -463,7 +463,7 @@ Dict::Add('DA DA', 'Danish', 'Dansk', [
'Class:DeliveryModel/Attribute:description' => 'Beskrivelse',
'Class:DeliveryModel/Attribute:description+' => '',
'Class:DeliveryModel/Attribute:contacts_list' => 'Kontakt',
'Class:DeliveryModel/Attribute:contacts_list+' => 'There must be at least one team to enable Ticket assignment~~',
'Class:DeliveryModel/Attribute:contacts_list+' => 'All the contacts (Teams and Persons) for this delivery model~~',
'Class:DeliveryModel/Attribute:customers_list' => 'Kunde',
'Class:DeliveryModel/Attribute:customers_list+' => 'All the customers having this delivering model~~',
]);

View File

@@ -217,7 +217,7 @@ Dict::Add('DE DE', 'German', 'Deutsch', [
'Class:Service/Attribute:organization_name' => 'Provider-Name',
'Class:Service/Attribute:organization_name+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Service-Familie',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Service-Familien-Name',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:description' => 'Beschreibung',

View File

@@ -240,7 +240,7 @@ Dict::Add('EN US', 'English', 'English', [
'Class:Service/Attribute:organization_name' => 'Provider Name',
'Class:Service/Attribute:organization_name+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Service Family',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Service Family Name',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:description' => 'Description',
@@ -486,7 +486,7 @@ Dict::Add('EN US', 'English', 'English', [
'Class:DeliveryModel/Attribute:description' => 'Description',
'Class:DeliveryModel/Attribute:description+' => '',
'Class:DeliveryModel/Attribute:contacts_list' => 'Contacts',
'Class:DeliveryModel/Attribute:contacts_list+' => 'There must be at least one team to enable Ticket assignment',
'Class:DeliveryModel/Attribute:contacts_list+' => 'All the contacts (Teams and Persons) for this delivery model',
'Class:DeliveryModel/Attribute:customers_list' => 'Customers',
'Class:DeliveryModel/Attribute:customers_list+' => 'All the customers having this delivering model',
]);

View File

@@ -240,7 +240,7 @@ Dict::Add('EN GB', 'British English', 'British English', [
'Class:Service/Attribute:organization_name' => 'Provider Name',
'Class:Service/Attribute:organization_name+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Service Family',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Service Family Name',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:description' => 'Description',
@@ -486,7 +486,7 @@ Dict::Add('EN GB', 'British English', 'British English', [
'Class:DeliveryModel/Attribute:description' => 'Description',
'Class:DeliveryModel/Attribute:description+' => '',
'Class:DeliveryModel/Attribute:contacts_list' => 'Contacts',
'Class:DeliveryModel/Attribute:contacts_list+' => 'There must be at least one team to enable Ticket assignment',
'Class:DeliveryModel/Attribute:contacts_list+' => 'All the contacts (Teams and Person) for this delivery model',
'Class:DeliveryModel/Attribute:customers_list' => 'Customers',
'Class:DeliveryModel/Attribute:customers_list+' => 'All the customers having this delivering model',
]);

View File

@@ -214,7 +214,7 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', [
'Class:Service/Attribute:organization_name' => 'Proveedor',
'Class:Service/Attribute:organization_name+' => 'Proveedor',
'Class:Service/Attribute:servicefamily_id' => 'Familia de Servicios',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => 'Familia de Servicios',
'Class:Service/Attribute:servicefamily_name' => 'Familia de Servicios',
'Class:Service/Attribute:servicefamily_name+' => 'Familia de Servicios',
'Class:Service/Attribute:description' => 'Descripción',

View File

@@ -214,7 +214,7 @@ Dict::Add('FR FR', 'French', 'Français', [
'Class:Service/Attribute:organization_name' => 'Nom fournisseur',
'Class:Service/Attribute:organization_name+' => 'Nom commun',
'Class:Service/Attribute:servicefamily_id' => 'Famille de service',
'Class:Service/Attribute:servicefamily_id+' => 'Obligatoire pour que ce service soit visible dans le portal utilisateur',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Nom Famille de service',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:services_list/UI:Links:Create:Button+' => 'Créer un %4$s',
@@ -478,7 +478,7 @@ Dict::Add('FR FR', 'French', 'Français', [
'Class:DeliveryModel/Attribute:description' => 'Description',
'Class:DeliveryModel/Attribute:description+' => '',
'Class:DeliveryModel/Attribute:contacts_list' => 'Contacts',
'Class:DeliveryModel/Attribute:contacts_list+' => 'Il doit y avoir au moins une équipe pour permettre l\'assignation des Tickets',
'Class:DeliveryModel/Attribute:contacts_list+' => 'Tous les contacts (Equipe ou Personne) pour ce modèle de support',
'Class:DeliveryModel/Attribute:customers_list' => 'Clients',
'Class:DeliveryModel/Attribute:customers_list+' => 'Tous les clients ayant ce modèle de support',
'Class:DeliveryModel/Attribute:customers_list/UI:Links:Create:Button+' => 'Créer un %4$s',

View File

@@ -216,7 +216,7 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', [
'Class:Service/Attribute:organization_name' => 'Szolgáltató név',
'Class:Service/Attribute:organization_name+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Szolgáltatáscsalád',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Szolgáltatáscsalád név',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:description' => 'Leírás',

View File

@@ -215,7 +215,7 @@ Dict::Add('IT IT', 'Italian', 'Italiano', [
'Class:Service/Attribute:organization_name' => 'Nome del Fornitore',
'Class:Service/Attribute:organization_name+' => '~~',
'Class:Service/Attribute:servicefamily_id' => 'Famiglia di Servizi',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '~~',
'Class:Service/Attribute:servicefamily_name' => 'Nome della Famiglia di Servizi',
'Class:Service/Attribute:servicefamily_name+' => '~~',
'Class:Service/Attribute:description' => 'Descrizione',

View File

@@ -215,7 +215,7 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
'Class:Service/Attribute:organization_name' => 'プロバイダー名',
'Class:Service/Attribute:organization_name+' => '',
'Class:Service/Attribute:servicefamily_id' => 'サービスファミリ',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'サービスファミリ名',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:description' => '説明',
@@ -461,7 +461,7 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
'Class:DeliveryModel/Attribute:description' => '説明',
'Class:DeliveryModel/Attribute:description+' => '',
'Class:DeliveryModel/Attribute:contacts_list' => '連絡先',
'Class:DeliveryModel/Attribute:contacts_list+' => 'There must be at least one team to enable Ticket assignment~~',
'Class:DeliveryModel/Attribute:contacts_list+' => 'All the contacts (Teams and Persons) for this delivery model~~',
'Class:DeliveryModel/Attribute:customers_list' => '顧客',
'Class:DeliveryModel/Attribute:customers_list+' => 'All the customers having this delivering model~~',
]);

View File

@@ -217,7 +217,7 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', [
'Class:Service/Attribute:organization_name' => 'Naam leverancier',
'Class:Service/Attribute:organization_name+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Servicecategorie',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Naam servicecategorie',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:description' => 'Omschrijving',

View File

@@ -215,7 +215,7 @@ Dict::Add('PL PL', 'Polish', 'Polski', [
'Class:Service/Attribute:organization_name' => 'Nazwa dostawcy',
'Class:Service/Attribute:organization_name+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Rodzina usług',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Nazwa rodziny usług',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:description' => 'Opis',

View File

@@ -215,7 +215,7 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', [
'Class:Service/Attribute:organization_name' => 'Nome do provedor',
'Class:Service/Attribute:organization_name+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Família de serviços',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Nome da família de serviços',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:description' => 'Descrição',

View File

@@ -216,7 +216,7 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
'Class:Service/Attribute:organization_name' => 'Поставщик',
'Class:Service/Attribute:organization_name+' => '',
'Class:Service/Attribute:servicefamily_id' => 'Пакет услуг',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => 'Пакет услуг',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:description' => 'Описание',

View File

@@ -215,7 +215,7 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', [
'Class:Service/Attribute:organization_name' => 'Meno poskytovateľa',
'Class:Service/Attribute:organization_name+' => '~~',
'Class:Service/Attribute:servicefamily_id' => 'Kategória služieb',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '~~',
'Class:Service/Attribute:servicefamily_name' => 'Názov rodiny služieb',
'Class:Service/Attribute:servicefamily_name+' => '~~',
'Class:Service/Attribute:description' => 'Popis',
@@ -461,7 +461,7 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', [
'Class:DeliveryModel/Attribute:description' => 'Popis',
'Class:DeliveryModel/Attribute:description+' => '~~',
'Class:DeliveryModel/Attribute:contacts_list' => 'Kontakty',
'Class:DeliveryModel/Attribute:contacts_list+' => 'There must be at least one team to enable Ticket assignment~~',
'Class:DeliveryModel/Attribute:contacts_list+' => 'All the contacts (Teams and Persons) for this delivery model~~',
'Class:DeliveryModel/Attribute:customers_list' => 'Zákazníci',
'Class:DeliveryModel/Attribute:customers_list+' => 'All the customers having this delivering model~~',
]);

View File

@@ -216,7 +216,7 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', [
'Class:Service/Attribute:organization_name' => 'Sağlayıcı Adı',
'Class:Service/Attribute:organization_name+' => '~~',
'Class:Service/Attribute:servicefamily_id' => 'Servis Ailesi',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '~~',
'Class:Service/Attribute:servicefamily_name' => 'Servis Aile Adı',
'Class:Service/Attribute:servicefamily_name+' => '~~',
'Class:Service/Attribute:description' => 'Tanımlama',

View File

@@ -236,7 +236,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:Service/Attribute:organization_name' => '供应商名称',
'Class:Service/Attribute:organization_name+' => '',
'Class:Service/Attribute:servicefamily_id' => '服务系列',
'Class:Service/Attribute:servicefamily_id+' => 'Required for this service to be visible on User Portal~~',
'Class:Service/Attribute:servicefamily_id+' => '',
'Class:Service/Attribute:servicefamily_name' => '服务系列名称',
'Class:Service/Attribute:servicefamily_name+' => '',
'Class:Service/Attribute:description' => '描述',

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Set>
<QueryOQL alias="QueryOQL" id="8">
<name>L'agent du Ticket</name>
<name>L'agent du ticket</name>
<description>Utilisable dans les notifications, pour informer l'agent en charge du ticket.</description>
<is_template>yes</is_template>
<oql><![CDATA[SELECT Person WHERE id=:this->agent_id]]></oql>
@@ -9,8 +9,9 @@
<finalclass>QueryOQL</finalclass>
</QueryOQL>
<QueryOQL alias="QueryOQL" id="4">
<name>L'agent du Ticket s'il n'a pas déclenché la notification</name>
<description>Utilisable dans les notifications, pour informer l'agent en charge du ticket, sauf si c'est l'agent lui-même qui déclenche la notification.</description>
<name>L'agent du ticket s'il n'a pas déclenché la notification</name>
<description>Utilisable dans les notifications, pour informer l'agent en charge du ticket, sauf si c'est l'agent lui-même qui déclenche la notification
</description>
<is_template>yes</is_template>
<oql><![CDATA[SELECT Person WHERE id=:this->agent_id AND id != :current_contact_id]]></oql>
<fields>id,email</fields>
@@ -18,7 +19,7 @@
</QueryOQL>
<QueryOQL alias="QueryOQL" id="3">
<name>Le demandeur et les contacts liés au Ticket</name>
<description>Utilisable dans les notifications, pour informer le demandeur ainsi que tous les contacts liés au ticket.</description>
<description>Utilisable dans les notifications, pour informer le demandeur ainsi que tout les contacts liés au ticket</description>
<is_template>yes</is_template>
<oql><![CDATA[SELECT Contact AS C JOIN lnkContactToTicket AS L ON L.contact_id=C.id WHERE L.ticket_id=:this->id
UNION SELECT Person WHERE id=:this->caller_id
@@ -28,7 +29,7 @@
</QueryOQL>
<QueryOQL alias="QueryOQL" id="1">
<name>Le demandeur du Ticket</name>
<description>Utilisable dans les notifications, pour informer le demandeur du ticket.</description>
<description>Utilisable dans les notifications, pour informer le demandeur du ticket</description>
<is_template>yes</is_template>
<oql><![CDATA[SELECT Person WHERE id=:this->caller_id]]></oql>
<fields>id,email</fields>
@@ -36,7 +37,7 @@
</QueryOQL>
<QueryOQL alias="QueryOQL" id="2">
<name>Les contacts liés au Ticket</name>
<description>Utilisable dans les notifications, pour informer tous les contacts liés au ticket.</description>
<description>Utilisable dans les notifications, pour informer tout les contacts liés au ticket</description>
<is_template>yes</is_template>
<oql><![CDATA[SELECT Contact AS C JOIN lnkContactToTicket AS L ON L.contact_id=C.id WHERE L.ticket_id=:this->id]]></oql>
<fields>id,email</fields>
@@ -44,7 +45,7 @@
</QueryOQL>
<QueryOQL alias="QueryOQL" id="7">
<name>Le manager du demandeur</name>
<description>Utilisable dans les notifications, pour informer le manager du demandeur d'un ticket.</description>
<description>Utilisable dans les notifications, pour informer le manager du demandeur d'un Ticket</description>
<is_template>yes</is_template>
<oql><![CDATA[SELECT Person AS manager JOIN Person AS employee ON employee.manager_id = manager.id WHERE employee.id=:this->caller_id]]></oql>
<fields>id,email</fields>
@@ -53,7 +54,7 @@
<QueryOQL alias="QueryOQL" id="9">
<name>La personne qui a déclenché la notification</name>
<description>Utilisable dans les notifications, pour informer la personne qui a effectué l'action qui a provoqué la notification.
Utilisée seule, cette requête retourne la personne liée à l'utilisateur iTop qui l'exécute.
Utilisée seule, cette requête retourne la personne liée à l'utilisateur iTop qui l'execute.
</description>
<is_template>yes</is_template>
<oql><![CDATA[SELECT Person WHERE id = :current_contact_id]]></oql>
@@ -73,7 +74,7 @@
<QueryOQL alias="QueryOQL" id="5">
<name>Les membres de l'équipe hors l'agent</name>
<description>Utilisable dans les notifications, pour informer les membres de l'équipe en charge du ticket.
L'agent lui-même est exclu de cette liste.
L'agent lui-même est exclus de cette liste.
</description>
<is_template>yes</is_template>
<oql><![CDATA[SELECT Person AS P JOIN lnkPersonToTeam AS L ON L.person_id=P.id
@@ -102,7 +103,7 @@
</QueryOQL>
<QueryOQL alias="QueryOQL" id="12">
<name>Les contacts d'un Service</name>
<description>Utilisable dans les notifications, pour informer les contacts liés au service courant.</description>
<description>Utilisable dans les notifications, pour informer les contacts liés au Service courant.</description>
<is_template>yes</is_template>
<oql><![CDATA[SELECT Contact AS C JOIN lnkContactToService AS L ON L.contact_id=C.id
WHERE L.service_id = :this->id
@@ -112,7 +113,7 @@
</QueryOQL>
<QueryOQL alias="QueryOQL" id="13">
<name>Les contacts d'un Contrat</name>
<description>Utilisable dans les notifications, pour informer les contacts liés au contrat courant.</description>
<description>Utilisable dans les notifications, pour informer les contacts liés au Contrat courant.</description>
<is_template>yes</is_template>
<oql><![CDATA[SELECT Contact AS C JOIN lnkContactToContract AS L ON L.contact_id=C.id
WHERE L.contract_id = :this->id

View File

@@ -12,7 +12,8 @@ SetupWebPage::AddModule(
// Setup
//
'dependencies' => [
'itop-structure/2.7.1 || itop-portal/3.0.0', // itop-portal : module_design_itop_design->module_designs->itop-portal
'itop-structure/2.7.1',
'itop-portal/3.0.0', // module_design_itop_design->module_designs->itop-portal
],
'mandatory' => false,
'visible' => true,

View File

@@ -708,7 +708,6 @@ Dict::Add('CS CZ', 'Czech', 'Čeština', [
Dict::Add('CS CZ', 'Czech', 'Čeština', [
'Class:TriggerOnObjectUpdate' => 'Triger \'aktualizace objektu\'',
'Class:TriggerOnObjectUpdate+' => 'Spustit při aktualizaci objektu [podřízené třídy] dané třídy',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Cílová pole',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '',
]);

View File

@@ -707,7 +707,6 @@ Dict::Add('DA DA', 'Danish', 'Dansk', [
Dict::Add('DA DA', 'Danish', 'Dansk', [
'Class:TriggerOnObjectUpdate' => 'Trigger (on object update)~~',
'Class:TriggerOnObjectUpdate+' => 'Trigger on object update of [a child class of] the given class~~',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Target fields~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '',
]);

View File

@@ -704,7 +704,6 @@ Dict::Add('DE DE', 'German', 'Deutsch', [
Dict::Add('DE DE', 'German', 'Deutsch', [
'Class:TriggerOnObjectUpdate' => 'Trigger (bei Objektanpassung)',
'Class:TriggerOnObjectUpdate+' => 'Trigger bei Objektanpassung einer gegebenen Klasse oder Kindklasse',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Ziel-Felder',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '',
]);

View File

@@ -801,7 +801,6 @@ Dict::Add('EN US', 'English', 'English', [
Dict::Add('EN US', 'English', 'English', [
'Class:TriggerOnObjectUpdate' => 'Trigger (on object update)',
'Class:TriggerOnObjectUpdate+' => 'Trigger on object update of [a child class of] the given class',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Target fields',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '',
]);

File diff suppressed because it is too large Load Diff

View File

@@ -784,7 +784,6 @@ Dict::Add('EN GB', 'British English', 'British English', [
Dict::Add('EN GB', 'British English', 'British English', [
'Class:TriggerOnObjectUpdate' => 'Trigger (on object update)',
'Class:TriggerOnObjectUpdate+' => 'Trigger on object update of [a child class of] the given class',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Target fields',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '',
]);

View File

@@ -695,7 +695,6 @@ Dict::Add('ES CR', 'Spanish', 'Español, Castellano', [
Dict::Add('ES CR', 'Spanish', 'Español, Castellano', [
'Class:TriggerOnObjectUpdate' => 'Disparador (actualizando un objecto)',
'Class:TriggerOnObjectUpdate+' => 'Disparador al actualizar un objeto de la clase dada [o una clase hija]',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Campos objetivo',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => 'Campos que serán monitorizados',
]);

View File

@@ -746,7 +746,6 @@ Dict::Add('FR FR', 'French', 'Français', [
Dict::Add('FR FR', 'French', 'Français', [
'Class:TriggerOnObjectUpdate' => 'Déclencheur sur la modification d\'un objet',
'Class:TriggerOnObjectUpdate+' => '',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'Ce filtre est appliqué après la sauvegarde en base de l\'objet modifié. Il restreint les objets qui vont déclencher les actions.',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Attributs cible',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '',
]);

View File

@@ -702,7 +702,6 @@ Dict::Add('HU HU', 'Hungarian', 'Magyar', [
Dict::Add('HU HU', 'Hungarian', 'Magyar', [
'Class:TriggerOnObjectUpdate' => 'Eseményindító (objektum frissítéskor)',
'Class:TriggerOnObjectUpdate+' => 'Az adott osztály [egy gyermekosztálya] objektumának frissítésekor elinduló eseményindító',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Célmezők',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '',
]);

View File

@@ -702,7 +702,6 @@ Dict::Add('IT IT', 'Italian', 'Italiano', [
Dict::Add('IT IT', 'Italian', 'Italiano', [
'Class:TriggerOnObjectUpdate' => 'Trigger (alla modifica dell\'oggetto)',
'Class:TriggerOnObjectUpdate+' => 'Trigger alla modifica dell\'oggetto di [una classe figlia della] classe specificata',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Campi di destinazione',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '',
]);

View File

@@ -706,7 +706,6 @@ Dict::Add('JA JP', 'Japanese', '日本語', [
Dict::Add('JA JP', 'Japanese', '日本語', [
'Class:TriggerOnObjectUpdate' => 'Trigger (on object update)~~',
'Class:TriggerOnObjectUpdate+' => 'Trigger on object update of [a child class of] the given class~~',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Target fields~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '~~',
]);

View File

@@ -704,7 +704,6 @@ Dict::Add('NL NL', 'Dutch', 'Nederlands', [
Dict::Add('NL NL', 'Dutch', 'Nederlands', [
'Class:TriggerOnObjectUpdate' => 'Trigger (bij het aanpassen van een object)',
'Class:TriggerOnObjectUpdate+' => 'Trigger bij het aanpassen van een object van de opgegeven klasse (of subklasse ervan)',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Doelvelden',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '',
]);

View File

@@ -704,7 +704,6 @@ Dict::Add('PL PL', 'Polish', 'Polski', [
Dict::Add('PL PL', 'Polish', 'Polski', [
'Class:TriggerOnObjectUpdate' => 'Wyzwalacz (przy aktualizacji obiektu)',
'Class:TriggerOnObjectUpdate+' => 'Wyzwalanie przy aktualizacji obiektu [klasy potomnej] danej klasy',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Pola docelowe',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '',
]);

View File

@@ -702,7 +702,6 @@ Dict::Add('PT BR', 'Brazilian', 'Brazilian', [
Dict::Add('PT BR', 'Brazilian', 'Brazilian', [
'Class:TriggerOnObjectUpdate' => 'Gatilho (na atualização do objeto)',
'Class:TriggerOnObjectUpdate+' => 'Gatilho na atualização de objeto de [uma classe filha] de uma determinada classe',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Campos de destino',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '',
]);

View File

@@ -707,7 +707,6 @@ Dict::Add('RU RU', 'Russian', 'Русский', [
Dict::Add('RU RU', 'Russian', 'Русский', [
'Class:TriggerOnObjectUpdate' => 'Триггер на обновление объекта',
'Class:TriggerOnObjectUpdate+' => 'Триггер на обновление объекта данного или дочернего класса',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Отслеживаемые поля',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => 'Поля объекта, при обновлении которых сработает триггер',
]);

View File

@@ -720,7 +720,6 @@ Dict::Add('SK SK', 'Slovak', 'Slovenčina', [
Dict::Add('SK SK', 'Slovak', 'Slovenčina', [
'Class:TriggerOnObjectUpdate' => 'Trigger (on object update)~~',
'Class:TriggerOnObjectUpdate+' => 'Trigger on object update of [a child class of] the given class~~',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Target fields~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '~~',
]);

View File

@@ -707,7 +707,6 @@ Dict::Add('TR TR', 'Turkish', 'Türkçe', [
Dict::Add('TR TR', 'Turkish', 'Türkçe', [
'Class:TriggerOnObjectUpdate' => 'Trigger (on object update)~~',
'Class:TriggerOnObjectUpdate+' => 'Trigger on object update of [a child class of] the given class~~',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => 'Target fields~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '',
]);

View File

@@ -739,7 +739,6 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', [
Dict::Add('ZH CN', 'Chinese', '简体中文', [
'Class:TriggerOnObjectUpdate' => '触发器 (对象更新时)',
'Class:TriggerOnObjectUpdate+' => '指定类型或子类型对象更新时的触发器',
'Class:TriggerOnObjectUpdate/Attribute:filter+' => 'This filter is computed after the object update in database. It restricts the objects which can trigger the actions~~',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes' => '目标字段',
'Class:TriggerOnObjectUpdate/Attribute:target_attcodes+' => '',
]);

17
git-split.md Normal file
View File

@@ -0,0 +1,17 @@
git copy of one file into two files without loosing history
```
git mv foo bar
git commit
SAVED=`git rev-parse HEAD`
git reset --hard HEAD^
git mv foo copy
git commit
git merge $SAVED # This will generate conflicts
git commit -a # Trivially resolved like this
git mv copy foo
git commit
```

View File

@@ -1,3 +1,19 @@
/**
* Image renderer plugin for TomSelect
*/
TomSelect.define('image_renderer', function(options) {
this.settings.render.option = function(data, escape) {
return `<div class="ibo-input-select-icon--menu--item">
<img src="${options['images_resource_path']+escape(data.value)}" alt="${data.text}" />${escape(data.text)}
</div>`;
};
this.settings.render.item = function(data, escape) {
return `<div class="ibo-input-select-icon--menu--item">
<img src="${options['images_resource_path']+escape(data.value)}" alt="${data.text}"/>${escape(data.text)}
</div>`;
};
});
class ChoicesElement extends HTMLSelectElement {
// register the custom element
@@ -16,6 +32,14 @@ class ChoicesElement extends HTMLSelectElement {
this.plugins.push('remove_button');
}
// plugins
if(this.hasAttribute('data-plugins')){
const aPlugins = JSON.parse(this.getAttribute('data-plugins'));
Array.from(aPlugins).values().forEach(plugin => {
this.plugins.push(plugin);
});
}
const options = {
plugins: this.plugins,
wrapperClass: 'ts-wrapper ibo-input-wrapper ibo-input-select-wrapper--with-buttons ibo-input-select-autocomplete-wrapper',
@@ -29,7 +53,7 @@ class ChoicesElement extends HTMLSelectElement {
};
if (this.getAttribute('data-tom-select-disable-auto-complete')) {
// options.controlInput = null;
options.controlInput = null;
}
if (this.getAttribute('data-tom-select-max-items-selected') && this.getAttribute('data-tom-select-max-items-selected') !== '') {
options.maxItems = parseInt(this.getAttribute('data-tom-select-max-items-selected'));
@@ -43,3 +67,4 @@ class ChoicesElement extends HTMLSelectElement {
}

View File

@@ -20,6 +20,8 @@ class TurboStreamEvent extends HTMLElement {
},
});
console.log(event);
document.dispatchEvent(event);
}

View File

@@ -0,0 +1,83 @@
class IboGridSlot extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
/** @type {string} unique cell id */
this.id = '';
/** @type {number} */
this.iPosX = this.getAttribute('gs-x') ? parseInt(this.getAttribute('gs-x'), 10) : 0;
/** @type {number} */
this.iPostY = this.getAttribute('gs-y') ? parseInt(this.getAttribute('gs-y'), 10) : 0;
/** @type {number} */
this.iWidth = this.getAttribute('gs-w') ? parseInt(this.getAttribute('gs-w'), 10) : 1;
/** @type {number} */
this.iHeight = this.getAttribute('gs-h') ? parseInt(this.getAttribute('gs-h'), 10) : 1;
/** @type {IboDashlet|null} contained dashlet id */
this.sDashletId = null;
/** @type {IboDashlet|null} contained dashlet element */
this.oDashlet = this.querySelector('ibo-dashlet') || null;
/** @type {Object} freeform metadata */
this.meta = {};
}
static MakeNew(oDashletElem, aOptions = {}) {
const oSlot = document.createElement('ibo-dashboard-grid-slot');
oDashletElem.classList.add("grid-stack-item-content");
oSlot.appendChild(oDashletElem);
oSlot.classList.add("grid-stack-item");
oSlot.oDashlet = oDashletElem;
return oSlot;
}
Serialize(bIncludeHtml = false) {
const oDashlet = this.oDashlet;
const aSlotData = {
position_x: this.iPosX,
position_y: this.iPostY,
width: this.iWidth,
height: this.iHeight
};
const aDashletData = oDashlet ? oDashlet.Serialize(bIncludeHtml) : {};
return {
...aSlotData,
dashlet: {...aDashletData}
};
}
static observedAttributes = ['gs-x', 'gs-y', 'gs-w', 'gs-h'];
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case 'gs-x':
this.iPosX = parseInt(newValue, 10) || 0;
break;
case 'gs-y':
this.iPostY = parseInt(newValue, 10) || 0;
break;
case 'gs-w':
this.iWidth = parseInt(newValue, 10) || 1;
break;
case 'gs-h':
this.iHeight = parseInt(newValue, 10) || 1;
break;
}
}
}
customElements.define('ibo-dashboard-grid-slot', IboGridSlot);

View File

@@ -0,0 +1,161 @@
class IboGrid extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
/** @type {number} */
this.columnsCount = 12;
/** @type {boolean} unused yet*/
this.bEditable = false;
/** @type {GridStack|null} GridStack instance */
this.oGrid = null;
/** @type {Array<IboGridSlot>} */
this.aSlots = [];
this.SetupGrid();
}
SetupGrid() {
let aCandidateSlots = Array.from(this.querySelectorAll('ibo-dashboard-grid-slot'));
aCandidateSlots.forEach(oSlot => {
const aAttrs = ['gs-x', 'gs-y', 'gs-w', 'gs-h', 'id'];
aAttrs.forEach(sAttr => {
const sVal = oSlot.getAttribute(sAttr) || oSlot.getAttribute('data-'+sAttr);
if (sVal !== null) {
oSlot.setAttribute(sAttr, sVal);
}
});
});
this.oGrid = GridStack.init({
column: this.columnsCount,
marginTop: 8,
marginLeft: 8,
marginRight: 0,
marginBottom: 0,
resizable: {handles: 'all'},
disableDrag: true,
disableResize: true,
float: true
}, this);
}
getSlots() {
return this.oGrid.getGridItems();
}
SetEditable(bIsEditable) {
this.bEditable = bIsEditable;
if (this.oGrid !== null) {
this.oGrid.enableMove(bIsEditable);
this.oGrid.enableResize(bIsEditable);
}
}
GetDashletElement(sDashletId) {
const aSlots = this.getSlots();
for (let oSlot of aSlots) {
if (oSlot.oDashlet && oSlot.oDashlet.sDashletId === sDashletId) {
return oSlot.oDashlet;
}
}
return null;
}
AddDashlet(sDashlet, aOptions = {}) {
// Get the dashlet as an object
const oParser = new DOMParser();
const oDocument = oParser.parseFromString(sDashlet, 'text/html');
const oDashlet = oDocument.body.firstChild;
const oSlot = IboGridSlot.MakeNew(oDashlet);
this.append(oSlot);
let aDefaultOptions = {
autoPosition: !(aOptions.hasOwnProperty('x') || aOptions.hasOwnProperty('y')),
}
this.oGrid.makeWidget(oSlot, Object.assign(aDefaultOptions, aOptions));
return oDashlet.sDashletId;
}
RefreshDashlet (sDashlet, aOptions = {}) {
const oParser = new DOMParser();
const oDocument = oParser.parseFromString(sDashlet, 'text/html');
const oNewDashlet = oDocument.body.firstElementChild;
// Can't use oNewDashet.sDashletId as it's not in the DOM yet and connectedCallback hasn't been called yet
const oExistingDashlet = this.GetDashletElement(oNewDashlet.getAttribute('data-dashlet-id') );
// Copy attributes
for (const sAttr of oNewDashlet.attributes) {
oExistingDashlet.setAttribute(sAttr.name, sAttr.value);
}
// If we refresh a dashlet, its parent slot remains the same, we just need to update its content
let oSlotForExistingDashlet = null;
const aSlots = this.getSlots();
for (let oSlot of aSlots) {
if (oSlot.oDashlet && oSlot.oDashlet.sDashletId === oNewDashlet.getAttribute('data-dashlet-id')) {
oSlotForExistingDashlet = oSlot;
break;
}
}
// There must be a slot for the existing dashlet
if(oSlotForExistingDashlet !== null) {
this.oGrid.removeWidget(oSlotForExistingDashlet);
oSlotForExistingDashlet.innerHTML = oNewDashlet.outerHTML;
this.oGrid.makeWidget(oSlotForExistingDashlet);
}
}
CloneDashlet(sDashletId) {
const aSlots = this.getSlots();
for (let oSlot of aSlots) {
if (oSlot.oDashlet && oSlot.oDashlet.sDashletId === sDashletId) {
const sWidth = oSlot.iWidth;
const sHeight = oSlot.iHeight;
// Ask a new rendered dashlet to avoid duplicating IDs
// Still we'll copy width and height
this.closest('ibo-dashboard')?.AddNewDashlet(oSlot.oDashlet.sType, oSlot.oDashlet.formData, {
'w': sWidth,
'h': sHeight
});
break;
}
}
}
RemoveDashlet(sDashletId) {
const aSlots = this.getSlots();
for (let oSlot of aSlots) {
if (oSlot.oDashlet && oSlot.oDashlet.sDashletId === sDashletId) {
this.oGrid.removeWidget(oSlot);
break;
}
}
}
ClearGrid() {
this.oGrid.removeAll();
}
Serialize(bIncludeHtml = false) {
const aSlots = this.getSlots();
return aSlots.reduce((aAccumulator, oSlot) => {
aAccumulator[oSlot.oDashlet.sDashletId] = oSlot.Serialize(bIncludeHtml);
return aAccumulator;
}, {});
}
}
customElements.define('ibo-dashboard-grid', IboGrid);

View File

@@ -0,0 +1,371 @@
class IboDashboard extends HTMLElement {
constructor() {
super();
/** @type {string} */
this.sId = "";
/** @type {string} */
this.sTitle = "";
/** @type {boolean} unused yet */
this.isEditable = false;
/** @type {boolean} */
this.bEditMode = false;
/** @type {boolean} unused yet */
this.bAutoRefresh = false;
/** @type {number} unused yet */
this.iRefreshRate = 0;
/** @type {IboGrid|null} */
this.oGrid = null;
/** @type {string|null} unused yet */
this.refreshUrl = null;
/** @type {string|null} unused yet */
this.csrfToken = null;
/** @type {number} Payload schema version */
this.schemaVersion = 2;
/** @type {object|null} Last saved state for cancel functionality, unused yet */
this.aLastSavedState = null;
}
connectedCallback() {
this.sId = this.getAttribute("id");
this.bEditMode = (this.getAttribute("data-edit-mode") === "edit");
this.SetupGrid();
this.BindEvents();
}
BindEvents() {
document.getElementById('ibo-dashboard-menu-edit-'+this.sId)?.addEventListener('click', (e) => {
e.preventDefault();
this.ToggleEditMode();
document.getElementById('ibo-dashboard-menu-edit-'+this.sId)?.classList.toggle('active', this.GetEditMode());
});
this.querySelector('[data-role="ibo-button"][name="save"]')?.addEventListener('click', (e) => {
e.preventDefault();
this.Save()
});
this.querySelector('[data-role="ibo-button"][name="cancel"]')?.addEventListener('click', (e) => {
e.preventDefault();
if (this.aLastSavedState) {
this.Load(this.aLastSavedState);
}
this.SetEditMode(false);
});
// TODO 3.3 Add event listener to dashboard toggler to get custom/default dashboard switching
// TODO 3.3 require load method that's not finished yet
// Bind IboDashboard object to these listener so we can access current instance
this._ListenToDashletFormSubmission = this._ListenToDashletFormSubmission.bind(this);
this._ListenToDashletFormCancellation = this._ListenToDashletFormCancellation.bind(this);
}
SetupGrid() {
if(this.oGrid !== null){
return;
}
this.oGrid = this.querySelector('ibo-dashboard-grid');
}
GetEditMode() {
return this.bEditMode;
}
ToggleEditMode(){
this.SetEditMode(!this.bEditMode);
}
SetEditMode(bEditMode) {
this.bEditMode = bEditMode;
this.oGrid.SetEditable(this.bEditMode);
if(this.bEditMode){
// TODO 3.3 If we are in default dashboard display, change to custom to allow editing
// TODO 3.3 Get the custom dashboard and load it, show a tooltip on the dashboard toggler to explain that we switched to custom mode
this.aLastSavedState = this.Serialize(true);
this.setAttribute("data-edit-mode", "edit");
}
else{
this.setAttribute("data-edit-mode", "view");
}
}
AddNewDashlet(sDashletClass, sDashletValues, aDashletOptions = {}) {
let sNewDashletUrl = GetAbsoluteUrlAppRoot() + '/pages/UI.php?route=dashboard.get_dashlet&dashlet_class='+encodeURIComponent(sDashletClass);
if(sDashletValues.length > 0) {
sNewDashletUrl += '&values=' + encodeURIComponent(sDashletValues);
}
fetch(sNewDashletUrl)
.then(async data => {
const sDashletId = this.oGrid.AddDashlet(await data.text(), aDashletOptions);
// TODO 3.3 Either open the dashlet form right away, or just enter edit mode
this.EditDashlet(sDashletId);
})
}
RefreshDashlet(oDashlet) {
let sGetDashletUrl = GetAbsoluteUrlAppRoot() + '/pages/UI.php?route=dashboard.get_dashlet&dashlet_class=' + encodeURIComponent(oDashlet.sType)
+'&dashlet_id=' + encodeURIComponent(oDashlet.sDashletId);
if(oDashlet.formData.length > 0) {
sGetDashletUrl += '&values=' + encodeURIComponent(oDashlet.formData);
}
return fetch(sGetDashletUrl)
.then(async data => {
this.oGrid.RefreshDashlet(await data.text());
});
}
HideDashletTogglers() {
const aTogglers = document.querySelector('.ibo-dashlet-panel--entries');
aTogglers.classList.add('ibo-is-hidden');
}
ShowDashletTogglers() {
const aTogglers = document.querySelector('.ibo-dashlet-panel--entries');
aTogglers.classList.remove('ibo-is-hidden');
}
SetDashletForm(sFormData) {
const oFormContainer = document.querySelector('.ibo-dashlet-panel--form-container');
oFormContainer.innerHTML = sFormData;
oFormContainer.classList.remove('ibo-is-hidden');
}
ClearDashletForm() {
const oFormContainer = document.querySelector('.ibo-dashlet-panel--form-container');
oFormContainer.innerHTML = '';
oFormContainer.classList.add('ibo-is-hidden');
}
DisableFormButtons() {
const aButtons = this.querySelectorAll('.ibo-dashboard--form--actions button');
aButtons.forEach( (oButton) => {
oButton.setAttribute('disabled', 'disabled');
});
}
EnableFormButtons() {
const aButtons = this.querySelectorAll('.ibo-dashboard--form--actions button');
aButtons.forEach( (oButton) => {
oButton.removeAttribute('disabled');
});
}
EditDashlet(sDashletId) {
const oDashlet = this.oGrid.GetDashletElement(sDashletId);
const me = this;
// Create backdrop to block interactions with other dashlets
if(this.oGrid.querySelector('.ibo-dashboard--grid--backdrop') === null) {
const oBackdrop = document.createElement("div");
oBackdrop.classList.add('ibo-dashboard--grid--backdrop');
this.oGrid.append(oBackdrop);
}
this.querySelector('ibo-dashlet[data-dashlet-id="'+sDashletId+'"]').setAttribute('data-edit-mode', 'edit');
// Disable dashboard buttons so we need to finish this edition first
this.DisableFormButtons();
// Fetch dashlet form from server
let sGetashletFormUrl = GetAbsoluteUrlAppRoot() + '/pages/UI.php?route=dashboard.get_dashlet_form&dashlet_class='+encodeURIComponent(oDashlet.sType);
if(oDashlet.formData.length > 0) {
sGetashletFormUrl += '&values=' + encodeURIComponent(oDashlet.formData);
}
fetch(sGetashletFormUrl)
.then(async formData => {
const sFormData = await formData.text();
this.HideDashletTogglers();
this.SetDashletForm(sFormData);
// Listen to form submission event and cancellation
document.addEventListener('itop:TurboStreamEvent', me._ListenToDashletFormSubmission);
document.querySelector('.ibo-dashlet-panel--form-container button[name="dashboard_cancel"]').addEventListener('click', me._ListenToDashletFormCancellation);
});
}
_ListenToDashletFormSubmission(event) {
const oDashlet = this.querySelector('ibo-dashlet[data-edit-mode="edit"]');
const sDashletId = oDashlet.GetDashletId();
if(event.detail.id === oDashlet.sType + '-turbo-stream-event' && event.detail.valid === "1") {
// Remove events
document.addEventListener('itop:TurboStreamEvent', this._ListenToDashletFormSubmission);
document.querySelector('.ibo-dashlet-panel--form-container button[name="dashboard_cancel"]').removeEventListener('click', this._ListenToDashletFormCancellation);
// Notify it all went well
CombodoToast.OpenToast('Dashlet created/updated', 'success');
// Clean edit mode
this.querySelector('ibo-dashlet[data-dashlet-id="'+sDashletId+'"]').setAttribute('data-edit-mode', 'view');
this.ShowDashletTogglers();
this.ClearDashletForm();
// Update local dashlet and refresh it
oDashlet.formData = event.detail.view_data;
this.RefreshDashlet(oDashlet);
// Re-enable dashboard buttons
this.EnableFormButtons();
}
}
_ListenToDashletFormCancellation(event) {
const oDashlet = this.querySelector('ibo-dashlet[data-edit-mode="edit"]');
const sDashletId = oDashlet.GetDashletId();
// Remove events
document.addEventListener('itop:TurboStreamEvent', this._ListenToDashletFormSubmission);
document.querySelector('.ibo-dashlet-panel--form-container button[name="dashboard_cancel"]').removeEventListener('click', this._ListenToDashletFormCancellation);
// Clean edit mode
this.querySelector('ibo-dashlet[data-dashlet-id="'+sDashletId+'"]').setAttribute('data-edit-mode', 'view');
this.ShowDashletTogglers();
this.ClearDashletForm();
// Re-enable dashboard buttons
this.EnableFormButtons();
// TODO 3.3 If this is an addition, remove the previewed dashlet
// TODO 3.3 If this is an edition, revert the dashlet to its initial state
}
CloneDashlet(sDashletId) {
this.oGrid.CloneDashlet(sDashletId);
}
RemoveDashlet(sDashletId) {
this.oGrid.RemoveDashlet(sDashletId);
}
RefreshFromBackend(bCustomDashboard = false) {
const sLoadDashboardUrl = GetAbsoluteUrlAppRoot() + `/pages/UI.php?route=dashboard.load&id=${this.sId}&custom=${bCustomDashboard ? 'true' : 'false'}`;
fetch(sLoadDashboardUrl)
.then(async oResponse => {
const oDashletData = await oResponse.json();
this.Load(oDashletData);
}
)
}
Serialize(bIncludeHtml = false) {
const sDashboardTitle = this.querySelector('.ibo-dashboard--form--inputs input[name="dashboard_title"]').value;
const sDashboardRefreshRate = this.querySelector('.ibo-dashboard--form--inputs select[name="refresh_interval"]').value;
const aSerializedGrid = this.oGrid.Serialize(bIncludeHtml);
return {
schema_version: this.schemaVersion,
id: this.sId,
title: sDashboardTitle,
refresh: sDashboardRefreshRate,
pos_dashlets: aSerializedGrid,
_token: ":)"
};
}
Save() {
// This payload shape is expected by the server
const aPayload = this.Serialize();
let sSaveUrl = GetAbsoluteUrlAppRoot() + '/pages/UI.php?route=dashboard.save&values='+encodeURIComponent(JSON.stringify(aPayload));
fetch(sSaveUrl)
.then(async data => {
const res = await data.json();
if(res.status === 'ok') {
CombodoToast.OpenToast(res.message, 'success');
this.aLastSavedState = this.Serialize(true);
this.SetEditMode(false);
} else {
CombodoToast.OpenToast(res.message, 'error');
}
})
}
Load(aSaveState) {
try {
// Validate schema version
if (aSaveState.schema_version !== this.schemaVersion) {
CombodoToast.OpenToast('Somehow, we got an incompatible dashboard schema version.', 'error');
return false;
}
// Update dashboard data
this.sId = aSaveState.id;
this.sTitle = aSaveState.title || "";
this.iRefreshRate = parseInt(aSaveState.refresh, 10) || 0;
// Update form inputs if they exist
const oTitleInput = this.querySelector('.ibo-dashboard--form--inputs input[name="dashboard_title"]');
if (oTitleInput) {
oTitleInput.value = this.sTitle;
}
const oRefreshSelect = this.querySelector('.ibo-dashboard--form--inputs select[name="refresh_interval"]');
if (oRefreshSelect) {
oRefreshSelect.value = aSaveState.refresh;
}
// Clear existing grid
this.ClearGrid();
// Load dashlets
const aDashletSlots = aSaveState.pos_dashlets || {};
for (const [sDashletId, aDashletData] of Object.entries(aDashletSlots)) {
const iPosX = aDashletData.position_x;
const iPosY = aDashletData.position_y;
const iWidth = aDashletData.width;
const iHeight = aDashletData.height;
const aDashlet = aDashletData.dashlet;
// We store the dashlet component in the HTML, not only the rendered dashlet
const sDashetHtml = aDashlet.html;
// Add dashlet to grid with its position and size
this.oGrid.AddDashlet(sDashetHtml, {
x: iPosX,
y: iPosY,
w: iWidth,
h: iHeight,
autoPosition: false
});
}
// Update last saved state
this.aLastSavedState = aSaveState;
return true;
} catch (error) {
console.error('Error loading dashboard state:', error);
CombodoToast.OpenToast('Error loading dashboard state', 'error');
return false;
}
}
ClearGrid() {
this.oGrid.ClearGrid();
}
DisplayError(sMessage, sSeverity = 'error') {
// TODO 3.3: Make this real
this.setAttribute("data-edit-mode", "error");
}
}
customElements.define('ibo-dashboard', IboDashboard);

View File

@@ -0,0 +1,68 @@
class IboDashlet extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
/** @type {string} */
this.sDashletId = this.GetDashletId();
/** @type {string} */
this.sType = this.GetDashletType();
/** @type {Object} */
this.formData = this.GetFormData();
/** @type {Object} unused yet */
this.meta = {};
this.BindEvents();
}
BindEvents() {
// Bind any dashlet-specific events here
this.querySelector('.ibo-dashlet--actions [data-role="ibo-dashlet-edit"]')?.addEventListener('click', (e) => {
this.closest('ibo-dashboard')?.EditDashlet(this.sDashletId);
});
this.querySelector('.ibo-dashlet--actions [data-role="ibo-dashlet-clone"]')?.addEventListener('click', (e) => {
this.closest('ibo-dashboard')?.CloneDashlet(this.sDashletId);
});
this.querySelector('.ibo-dashlet--actions [data-role="ibo-dashlet-remove"]')?.addEventListener('click', (e) => {
this.closest('ibo-dashboard')?.RemoveDashlet(this.sDashletId);
});
}
GetDashletId() {
return this.getAttribute('data-dashlet-id');
}
GetDashletType() {
return this.getAttribute("data-dashlet-type") || "";
}
GetFormData() {
return this.getAttribute("data-form-view-data") || "";
}
static MakeNew(sDashlet) {
const oDashlet = document.createElement('ibo-dashlet');
oDashlet.innerHTML = sDashlet;
return oDashlet;
}
Serialize(bIncludeHtml = false) {
// TODO 3.3 Should we use getters ?
let aDashletData = {
id: this.sDashletId,
type: this.sType,
properties: JSON.parse(this.formData),
};
if(bIncludeHtml) {
aDashletData.html = this.outerHTML;
}
return aDashletData;
}
}
customElements.define('ibo-dashlet', IboDashlet);

View File

@@ -130,6 +130,27 @@ return array(
'CheckableExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php',
'Collator' => $vendorDir . '/symfony/polyfill-intl-icu/Resources/stubs/Collator.php',
'Combodo\\iTop\\Application\\Branding' => $baseDir . '/sources/Application/Branding.php',
'Combodo\\iTop\\Application\\Dashboard\\Controller\\DashboardController' => $baseDir . '/sources/Application/Dashboard/Controller/DashboardController.php',
'Combodo\\iTop\\Application\\Dashboard\\DashboardException' => $baseDir . '/sources/Application/Dashboard/DashboardException.php',
'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashboardFormBlock' => $baseDir . '/sources/Application/Dashboard/FormBlock/DashboardFormBlock.php',
'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashletFormBlock' => $baseDir . '/sources/Application/Dashboard/FormBlock/DashletFormBlock.php',
'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashletPropertiesFormBlock' => $baseDir . '/sources/Application/Dashboard/FormBlock/DashletPropertiesFormBlock.php',
'Combodo\\iTop\\Application\\Dashboard\\Layout\\DashboardLayoutGrid' => $baseDir . '/sources/Application/Dashboard/Layout/DashboardLayoutGrid.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletBadge' => $baseDir . '/sources/Application/Dashlet/Core/DashletBadge.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupBy' => $baseDir . '/sources/Application/Dashlet/Core/DashletGroupBy.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupByBars' => $baseDir . '/sources/Application/Dashlet/Core/DashletGroupByBars.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupByPie' => $baseDir . '/sources/Application/Dashlet/Core/DashletGroupByPie.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupByTable' => $baseDir . '/sources/Application/Dashlet/Core/DashletGroupByTable.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletHeaderDynamic' => $baseDir . '/sources/Application/Dashlet/Core/DashletHeaderDynamic.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletHeaderStatic' => $baseDir . '/sources/Application/Dashlet/Core/DashletHeaderStatic.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletObjectList' => $baseDir . '/sources/Application/Dashlet/Core/DashletObjectList.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletPlainText' => $baseDir . '/sources/Application/Dashlet/Core/DashletPlainText.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletProxy' => $baseDir . '/sources/Application/Dashlet/Core/DashletProxy.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletUnknown' => $baseDir . '/sources/Application/Dashlet/Core/DashletUnknown.php',
'Combodo\\iTop\\Application\\Dashlet\\Dashlet' => $baseDir . '/sources/Application/Dashlet/Dashlet.php',
'Combodo\\iTop\\Application\\Dashlet\\DashletException' => $baseDir . '/sources/Application/Dashlet/DashletException.php',
'Combodo\\iTop\\Application\\Dashlet\\DashletFactory' => $baseDir . '/sources/Application/Dashlet/DashletFactory.php',
'Combodo\\iTop\\Application\\Dashlet\\Service\\DashletService' => $baseDir . '/sources/Application/Dashlet/Service/DashletService.php',
'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => $baseDir . '/sources/Application/EventRegister/ApplicationEvents.php',
'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => $baseDir . '/sources/Application/Helper/CKEditorHelper.php',
'Combodo\\iTop\\Application\\Helper\\ExportHelper' => $baseDir . '/sources/Application/Helper/ExportHelper.php',
@@ -170,6 +191,7 @@ return array(
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletFactory' => $baseDir . '/sources/Application/UI/Base/Component/Dashlet/DashletFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletHeaderStatic' => $baseDir . '/sources/Application/UI/Base/Component/Dashlet/DashletHeaderStatic.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletPlainText' => $baseDir . '/sources/Application/UI/Base/Component/Dashlet/DashletPlainText.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletWrapper' => $baseDir . '/sources/Application/UI/Base/Component/Dashlet/DashletWrapper.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\DataTable' => $baseDir . '/sources/Application/UI/Base/Component/DataTable/DataTable.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\DataTableConfig\\DataTableConfig' => $baseDir . '/sources/Application/UI/Base/Component/DataTable/DataTableConfig/DataTableConfig.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\DataTableSettings' => $baseDir . '/sources/Application/UI/Base/Component/DataTable/DataTableSettings.php',
@@ -270,8 +292,13 @@ return array(
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\CaseLogEntryForm\\CaseLogEntryForm' => $baseDir . '/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryForm.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\CaseLogEntryForm\\CaseLogEntryFormFactory' => $baseDir . '/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryFormFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardColumn' => $baseDir . '/sources/Application/UI/Base/Layout/Dashboard/DashboardColumn.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardGrid' => $baseDir . '/sources/Application/UI/Base/Layout/Dashboard/DashboardGrid.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardGridSlot' => $baseDir . '/sources/Application/UI/Base/Layout/Dashboard/DashboardGridSlot.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardLayout' => $baseDir . '/sources/Application/UI/Base/Layout/Dashboard/DashboardLayout.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardRow' => $baseDir . '/sources/Application/UI/Base/Layout/Dashboard/DashboardRow.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\DashletPanel\\DashletEntry' => $baseDir . '/sources/Application/UI/Base/Layout/DashletPanel/DashletEntry.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\DashletPanel\\DashletPanel' => $baseDir . '/sources/Application/UI/Base/Layout/DashletPanel/DashletPanel.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\DashletPanel\\DashletPanelFactory' => $baseDir . '/sources/Application/UI/Base/Layout/DashletPanel/DashletPanelFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\Column\\Column' => $baseDir . '/sources/Application/UI/Base/Layout/MultiColumn/Column/Column.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\Column\\ColumnUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Layout/MultiColumn/Column/ColumnUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\MultiColumn' => $baseDir . '/sources/Application/UI/Base/Layout/MultiColumn/MultiColumn.php',
@@ -483,6 +510,7 @@ return array(
'Combodo\\iTop\\Forms\\Block\\Base\\CheckboxFormBlock' => $baseDir . '/sources/Forms/Block/Base/CheckboxFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\ChoiceFormBlock' => $baseDir . '/sources/Forms/Block/Base/ChoiceFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\ChoiceFromInputsBlock' => $baseDir . '/sources/Forms/Block/Base/ChoiceFromInputsBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\ChoiceImageFormBlock' => $baseDir . '/sources/Forms/Block/Base/ChoiceImageFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\CollectionBlock' => $baseDir . '/sources/Forms/Block/Base/CollectionBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\DateFormBlock' => $baseDir . '/sources/Forms/Block/Base/DateFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\DateTimeFormBlock' => $baseDir . '/sources/Forms/Block/Base/DateTimeFormBlock.php',
@@ -490,6 +518,7 @@ return array(
'Combodo\\iTop\\Forms\\Block\\Base\\HiddenFormBlock' => $baseDir . '/sources/Forms/Block/Base/HiddenFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\IntegerFormBlock' => $baseDir . '/sources/Forms/Block/Base/IntegerFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\NumberFormBlock' => $baseDir . '/sources/Forms/Block/Base/NumberFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\PolymorphicFormBlock' => $baseDir . '/sources/Forms/Block/Base/PolymorphicFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\TextAreaFormBlock' => $baseDir . '/sources/Forms/Block/Base/TextAreaFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\TextFormBlock' => $baseDir . '/sources/Forms/Block/Base/TextFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\DataModel\\AttributeChoiceFormBlock' => $baseDir . '/sources/Forms/Block/DataModel/AttributeChoiceFormBlock.php',
@@ -557,15 +586,19 @@ return array(
'Combodo\\iTop\\PropertyType\\PropertyTypeFactory' => $baseDir . '/sources/PropertyType/PropertyTypeFactory.php',
'Combodo\\iTop\\PropertyType\\PropertyTypeService' => $baseDir . '/sources/PropertyType/PropertyTypeService.php',
'Combodo\\iTop\\PropertyType\\Serializer\\SerializerException' => $baseDir . '/sources/PropertyType/Serializer/SerializerException.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLEncoder' => $baseDir . '/sources/PropertyType/Serializer/XMLEncoder.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\AbstractXMLFormat' => $baseDir . '/sources/PropertyType/Serializer/XMLFormat/AbstractXMLFormat.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatCSV' => $baseDir . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatCSV.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatCollectionWithId' => $baseDir . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatCollectionWithId.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatFactory' => $baseDir . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatFactory.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatFlatArray' => $baseDir . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatFlatArray.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatValueAsId' => $baseDir . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatValueAsId.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLNormalizer' => $baseDir . '/sources/PropertyType/Serializer/XMLNormalizer.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLSerializer' => $baseDir . '/sources/PropertyType/Serializer/XMLSerializer.php',
'Combodo\\iTop\\PropertyType\\ValueType\\AbstractValueType' => $baseDir . '/sources/PropertyType/ValueType/AbstractValueType.php',
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\AbstractBranchValueType' => $baseDir . '/sources/PropertyType/ValueType/Branch/AbstractBranchValueType.php',
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\ValueTypeCollection' => $baseDir . '/sources/PropertyType/ValueType/Branch/ValueTypeCollection.php',
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\ValueTypePolymorphic' => $baseDir . '/sources/PropertyType/ValueType/Branch/ValueTypePolymorphic.php',
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\ValueTypePropertyTree' => $baseDir . '/sources/PropertyType/ValueType/Branch/ValueTypePropertyTree.php',
'Combodo\\iTop\\PropertyType\\ValueType\\Leaf\\AbstractLeafValueType' => $baseDir . '/sources/PropertyType/ValueType/Leaf/AbstractLeafValueType.php',
'Combodo\\iTop\\PropertyType\\ValueType\\Leaf\\ValueTypeAggregateFunction' => $baseDir . '/sources/PropertyType/ValueType/Leaf/ValueTypeAggregateFunction.php',
@@ -676,19 +709,6 @@ return array(
'DashboardLayoutThreeCols' => $baseDir . '/application/dashboardlayout.class.inc.php',
'DashboardLayoutTwoCols' => $baseDir . '/application/dashboardlayout.class.inc.php',
'DashboardMenuNode' => $baseDir . '/application/menunode.class.inc.php',
'Dashlet' => $baseDir . '/application/dashlet.class.inc.php',
'DashletBadge' => $baseDir . '/application/dashlet.class.inc.php',
'DashletEmptyCell' => $baseDir . '/application/dashlet.class.inc.php',
'DashletGroupBy' => $baseDir . '/application/dashlet.class.inc.php',
'DashletGroupByBars' => $baseDir . '/application/dashlet.class.inc.php',
'DashletGroupByPie' => $baseDir . '/application/dashlet.class.inc.php',
'DashletGroupByTable' => $baseDir . '/application/dashlet.class.inc.php',
'DashletHeaderDynamic' => $baseDir . '/application/dashlet.class.inc.php',
'DashletHeaderStatic' => $baseDir . '/application/dashlet.class.inc.php',
'DashletObjectList' => $baseDir . '/application/dashlet.class.inc.php',
'DashletPlainText' => $baseDir . '/application/dashlet.class.inc.php',
'DashletProxy' => $baseDir . '/application/dashlet.class.inc.php',
'DashletUnknown' => $baseDir . '/application/dashlet.class.inc.php',
'Datamatrix' => $vendorDir . '/tecnickcom/tcpdf/include/barcodes/datamatrix.php',
'DateError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
'DateException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateException.php',

View File

@@ -516,6 +516,27 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'CheckableExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php',
'Collator' => __DIR__ . '/..' . '/symfony/polyfill-intl-icu/Resources/stubs/Collator.php',
'Combodo\\iTop\\Application\\Branding' => __DIR__ . '/../..' . '/sources/Application/Branding.php',
'Combodo\\iTop\\Application\\Dashboard\\Controller\\DashboardController' => __DIR__ . '/../..' . '/sources/Application/Dashboard/Controller/DashboardController.php',
'Combodo\\iTop\\Application\\Dashboard\\DashboardException' => __DIR__ . '/../..' . '/sources/Application/Dashboard/DashboardException.php',
'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashboardFormBlock' => __DIR__ . '/../..' . '/sources/Application/Dashboard/FormBlock/DashboardFormBlock.php',
'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashletFormBlock' => __DIR__ . '/../..' . '/sources/Application/Dashboard/FormBlock/DashletFormBlock.php',
'Combodo\\iTop\\Application\\Dashboard\\FormBlock\\DashletPropertiesFormBlock' => __DIR__ . '/../..' . '/sources/Application/Dashboard/FormBlock/DashletPropertiesFormBlock.php',
'Combodo\\iTop\\Application\\Dashboard\\Layout\\DashboardLayoutGrid' => __DIR__ . '/../..' . '/sources/Application/Dashboard/Layout/DashboardLayoutGrid.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletBadge' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletBadge.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupBy' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletGroupBy.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupByBars' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletGroupByBars.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupByPie' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletGroupByPie.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletGroupByTable' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletGroupByTable.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletHeaderDynamic' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletHeaderDynamic.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletHeaderStatic' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletHeaderStatic.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletObjectList' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletObjectList.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletPlainText' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletPlainText.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletProxy' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletProxy.php',
'Combodo\\iTop\\Application\\Dashlet\\Core\\DashletUnknown' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Core/DashletUnknown.php',
'Combodo\\iTop\\Application\\Dashlet\\Dashlet' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Dashlet.php',
'Combodo\\iTop\\Application\\Dashlet\\DashletException' => __DIR__ . '/../..' . '/sources/Application/Dashlet/DashletException.php',
'Combodo\\iTop\\Application\\Dashlet\\DashletFactory' => __DIR__ . '/../..' . '/sources/Application/Dashlet/DashletFactory.php',
'Combodo\\iTop\\Application\\Dashlet\\Service\\DashletService' => __DIR__ . '/../..' . '/sources/Application/Dashlet/Service/DashletService.php',
'Combodo\\iTop\\Application\\EventRegister\\ApplicationEvents' => __DIR__ . '/../..' . '/sources/Application/EventRegister/ApplicationEvents.php',
'Combodo\\iTop\\Application\\Helper\\CKEditorHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/CKEditorHelper.php',
'Combodo\\iTop\\Application\\Helper\\ExportHelper' => __DIR__ . '/../..' . '/sources/Application/Helper/ExportHelper.php',
@@ -556,6 +577,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Dashlet/DashletFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletHeaderStatic' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Dashlet/DashletHeaderStatic.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletPlainText' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Dashlet/DashletPlainText.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\Dashlet\\DashletWrapper' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Dashlet/DashletWrapper.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\DataTable' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/DataTable/DataTable.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\DataTableConfig\\DataTableConfig' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/DataTable/DataTableConfig/DataTableConfig.php',
'Combodo\\iTop\\Application\\UI\\Base\\Component\\DataTable\\DataTableSettings' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/DataTable/DataTableSettings.php',
@@ -656,8 +678,13 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\CaseLogEntryForm\\CaseLogEntryForm' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryForm.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\ActivityPanel\\CaseLogEntryForm\\CaseLogEntryFormFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/ActivityPanel/CaseLogEntryForm/CaseLogEntryFormFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardColumn' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/Dashboard/DashboardColumn.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardGrid' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/Dashboard/DashboardGrid.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardGridSlot' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/Dashboard/DashboardGridSlot.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardLayout' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/Dashboard/DashboardLayout.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\Dashboard\\DashboardRow' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/Dashboard/DashboardRow.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\DashletPanel\\DashletEntry' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/DashletPanel/DashletEntry.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\DashletPanel\\DashletPanel' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/DashletPanel/DashletPanel.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\DashletPanel\\DashletPanelFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/DashletPanel/DashletPanelFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\Column\\Column' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/MultiColumn/Column/Column.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\Column\\ColumnUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/MultiColumn/Column/ColumnUIBlockFactory.php',
'Combodo\\iTop\\Application\\UI\\Base\\Layout\\MultiColumn\\MultiColumn' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Layout/MultiColumn/MultiColumn.php',
@@ -869,6 +896,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Forms\\Block\\Base\\CheckboxFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/CheckboxFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\ChoiceFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/ChoiceFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\ChoiceFromInputsBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/ChoiceFromInputsBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\ChoiceImageFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/ChoiceImageFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\CollectionBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/CollectionBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\DateFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/DateFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\DateTimeFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/DateTimeFormBlock.php',
@@ -876,6 +904,7 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\Forms\\Block\\Base\\HiddenFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/HiddenFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\IntegerFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/IntegerFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\NumberFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/NumberFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\PolymorphicFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/PolymorphicFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\TextAreaFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/TextAreaFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\Base\\TextFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/Base/TextFormBlock.php',
'Combodo\\iTop\\Forms\\Block\\DataModel\\AttributeChoiceFormBlock' => __DIR__ . '/../..' . '/sources/Forms/Block/DataModel/AttributeChoiceFormBlock.php',
@@ -943,15 +972,19 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'Combodo\\iTop\\PropertyType\\PropertyTypeFactory' => __DIR__ . '/../..' . '/sources/PropertyType/PropertyTypeFactory.php',
'Combodo\\iTop\\PropertyType\\PropertyTypeService' => __DIR__ . '/../..' . '/sources/PropertyType/PropertyTypeService.php',
'Combodo\\iTop\\PropertyType\\Serializer\\SerializerException' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/SerializerException.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLEncoder' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/XMLEncoder.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\AbstractXMLFormat' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/XMLFormat/AbstractXMLFormat.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatCSV' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatCSV.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatCollectionWithId' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatCollectionWithId.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatFactory' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatFactory.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatFlatArray' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatFlatArray.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLFormat\\XMLFormatValueAsId' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/XMLFormat/XMLFormatValueAsId.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLNormalizer' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/XMLNormalizer.php',
'Combodo\\iTop\\PropertyType\\Serializer\\XMLSerializer' => __DIR__ . '/../..' . '/sources/PropertyType/Serializer/XMLSerializer.php',
'Combodo\\iTop\\PropertyType\\ValueType\\AbstractValueType' => __DIR__ . '/../..' . '/sources/PropertyType/ValueType/AbstractValueType.php',
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\AbstractBranchValueType' => __DIR__ . '/../..' . '/sources/PropertyType/ValueType/Branch/AbstractBranchValueType.php',
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\ValueTypeCollection' => __DIR__ . '/../..' . '/sources/PropertyType/ValueType/Branch/ValueTypeCollection.php',
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\ValueTypePolymorphic' => __DIR__ . '/../..' . '/sources/PropertyType/ValueType/Branch/ValueTypePolymorphic.php',
'Combodo\\iTop\\PropertyType\\ValueType\\Branch\\ValueTypePropertyTree' => __DIR__ . '/../..' . '/sources/PropertyType/ValueType/Branch/ValueTypePropertyTree.php',
'Combodo\\iTop\\PropertyType\\ValueType\\Leaf\\AbstractLeafValueType' => __DIR__ . '/../..' . '/sources/PropertyType/ValueType/Leaf/AbstractLeafValueType.php',
'Combodo\\iTop\\PropertyType\\ValueType\\Leaf\\ValueTypeAggregateFunction' => __DIR__ . '/../..' . '/sources/PropertyType/ValueType/Leaf/ValueTypeAggregateFunction.php',
@@ -1062,19 +1095,6 @@ class ComposerStaticInit7f81b4a2a468a061c306af5e447a9a9f
'DashboardLayoutThreeCols' => __DIR__ . '/../..' . '/application/dashboardlayout.class.inc.php',
'DashboardLayoutTwoCols' => __DIR__ . '/../..' . '/application/dashboardlayout.class.inc.php',
'DashboardMenuNode' => __DIR__ . '/../..' . '/application/menunode.class.inc.php',
'Dashlet' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
'DashletBadge' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
'DashletEmptyCell' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
'DashletGroupBy' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
'DashletGroupByBars' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
'DashletGroupByPie' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
'DashletGroupByTable' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
'DashletHeaderDynamic' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
'DashletHeaderStatic' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
'DashletObjectList' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
'DashletPlainText' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
'DashletProxy' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
'DashletUnknown' => __DIR__ . '/../..' . '/application/dashlet.class.inc.php',
'Datamatrix' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/barcodes/datamatrix.php',
'DateError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
'DateException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateException.php',

16
node_modules/.package-lock.json generated vendored
View File

@@ -186,6 +186,22 @@
"delegate": "^3.1.2"
}
},
"node_modules/gridstack": {
"version": "12.4.2",
"resolved": "https://registry.npmjs.org/gridstack/-/gridstack-12.4.2.tgz",
"integrity": "sha512-aXbJrQpi3LwpYXYOr4UriPM5uc/dPcjK01SdOE5PDpx2vi8tnLhU7yBg/1i4T59UhNkG/RBfabdFUObuN+gMnw==",
"funding": [
{
"type": "paypal",
"url": "https://www.paypal.me/alaind831"
},
{
"type": "venmo",
"url": "https://www.venmo.com/adumesny"
}
],
"license": "MIT"
},
"node_modules/jquery": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",

21
node_modules/gridstack/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019-2025 Alain Dumesny. v0.4.0 and older (c) 2014-2018 Pavel Reznikov, Dylan Weiss
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

565
node_modules/gridstack/README.md generated vendored Normal file
View File

@@ -0,0 +1,565 @@
# gridstack.js
[![NPM version](https://img.shields.io/npm/v/gridstack.svg)](https://www.npmjs.com/package/gridstack)
[![Coverage Status](https://coveralls.io/repos/github/gridstack/gridstack.js/badge.svg?branch=develop)](https://coveralls.io/github/gridstack/gridstack.js?branch=develop)
[![downloads](https://img.shields.io/npm/dm/gridstack.svg)](https://www.npmjs.com/package/gridstack)
Mobile-friendly modern Typescript library for dashboard layout and creation. Making a drag-and-drop, multi-column responsive dashboard has never been easier. Has multiple bindings and works great with [Angular](https://angular.io/) (included), [React](https://reactjs.org/), [Vue](https://vuejs.org/), [Knockout.js](http://knockoutjs.com), [Ember](https://www.emberjs.com/) and others (see [frameworks](#specific-frameworks) section).
Inspired by no-longer maintained gridster, built with love.
Check http://gridstackjs.com and [these demos](http://gridstackjs.com/demo/).
If you find this lib useful, please donate [PayPal](https://www.paypal.me/alaind831) (use **“send to a friend”** to avoid 3% fee) or [Venmo](https://www.venmo.com/adumesny) (adumesny) and help support it!
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/alaind831)
[![Donate](https://img.shields.io/badge/Donate-Venmo-g.svg)](https://www.venmo.com/adumesny)
Join us on Slack: [https://gridstackjs.slack.com](https://join.slack.com/t/gridstackjs/shared_invite/zt-3978nsff6-HDNE_N45DydP36NBSV9JFQ)
<!-- [![Slack Status](https://gridstackjs.com/badge.svg)](https://gridstackjs.slack.com) -->
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)*
- [Demo and API Documentation](#demo-and-api-documentation)
- [Usage](#usage)
- [Install](#install)
- [Include](#include)
- [Basic usage](#basic-usage)
- [Requirements](#requirements)
- [Specific frameworks](#specific-frameworks)
- [Extend Library](#extend-library)
- [Extend Engine](#extend-engine)
- [Change grid columns](#change-grid-columns)
- [Custom columns CSS (OLD, not needed with v12+)](#custom-columns-css-old-not-needed-with-v12)
- [Override resizable/draggable options](#override-resizabledraggable-options)
- [Touch devices support](#touch-devices-support)
- [Migrating](#migrating)
- [Migrating to v0.6](#migrating-to-v06)
- [Migrating to v1](#migrating-to-v1)
- [Migrating to v2](#migrating-to-v2)
- [Migrating to v3](#migrating-to-v3)
- [Migrating to v4](#migrating-to-v4)
- [Migrating to v5](#migrating-to-v5)
- [Migrating to v6](#migrating-to-v6)
- [Migrating to v7](#migrating-to-v7)
- [Migrating to v8](#migrating-to-v8)
- [Migrating to v9](#migrating-to-v9)
- [Migrating to v10](#migrating-to-v10)
- [Migrating to v11](#migrating-to-v11)
- [Migrating to v12](#migrating-to-v12)
- [jQuery Application](#jquery-application)
- [Changes](#changes)
- [Usage Trend](#usage-trend)
- [The Team](#the-team)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
# Demo and API Documentation
Please visit http://gridstackjs.com and [these demos](http://gridstackjs.com/demo/), and complete [API documentation](https://gridstack.github.io/gridstack.js/doc/html/) ([markdown](https://github.com/gridstack/gridstack.js/tree/master/doc/API.md))
# Usage
## Install
[![NPM version](https://img.shields.io/npm/v/gridstack.svg)](https://www.npmjs.com/package/gridstack)
```js
yarn add gridstack
// or
npm install --save gridstack
```
## Include
ES6 or Typescript
```js
import 'gridstack/dist/gridstack.min.css';
import { GridStack } from 'gridstack';
```
Alternatively (single combined file, notice the -all.js) in html
```html
<link href="node_modules/gridstack/dist/gridstack.min.css" rel="stylesheet"/>
<script src="node_modules/gridstack/dist/gridstack-all.js"></script>
```
**Note**: IE support was dropped in v2, but restored in v4.4 by an external contributor (I have no interest in testing+supporting obsolete browser so this likely will break again in the future) and DROPPED again in v12 (css variable needed).
You can use the es5 files and polyfill (larger) for older browser instead. For example:
```html
<link href="node_modules/gridstack/dist/gridstack.min.css" rel="stylesheet"/>
<script src="node_modules/gridstack/dist/es5/gridstack-poly.js"></script>
<script src="node_modules/gridstack/dist/es5/gridstack-all.js"></script>
```
## Basic usage
creating items dynamically...
```js
// ...in your HTML
<div class="grid-stack"></div>
// ...in your script
var grid = GridStack.init();
grid.addWidget({w: 2, content: 'item 1'});
```
... or creating from list
```js
// using serialize data instead of .addWidget()
const serializedData = [
{x: 0, y: 0, w: 2, h: 2},
{x: 2, y: 3, w: 3, content: 'item 2'},
{x: 1, y: 3}
];
grid.load(serializedData);
```
... or DOM created items
```js
// ...in your HTML
<div class="grid-stack">
<div class="grid-stack-item">
<div class="grid-stack-item-content">Item 1</div>
</div>
<div class="grid-stack-item" gs-w="2">
<div class="grid-stack-item-content">Item 2 wider</div>
</div>
</div>
// ...in your script
GridStack.init();
```
...or see list of all [API and options](https://github.com/gridstack/gridstack.js/tree/master/doc) available.
see [stackblitz sample](https://stackblitz.com/edit/gridstack-demo) as running example too.
## Requirements
GridStack no longer requires external dependencies as of v1 (lodash was removed in v0.5 and jquery API in v1). v3 is a complete HTML5 re-write removing need for jquery. v6 is native mouse and touch event for mobile support, and no longer have jquery-ui version. All you need to include now is `gridstack-all.js` and `gridstack.min.css` (layouts are done using CSS column based %).
## Specific frameworks
search for ['gridstack' under NPM](https://www.npmjs.com/search?q=gridstack&ranking=popularity) for latest, more to come...
- **Angular**: we ship out of the box an Angular wrapper - see <a href="https://github.com/gridstack/gridstack.js/tree/master/angular" target="_blank">Angular Component</a>.
- **Angular9**: [lb-gridstack](https://github.com/pfms84/lb-gridstack) Note: very old v0.3 gridstack instance so recommend for **concept ONLY if you wish to use directive instead**. Code has **not been vented** at as I use components.
- **AngularJS**: [gridstack-angular](https://github.com/kdietrich/gridstack-angular)
- **Ember**: [ember-gridstack](https://github.com/yahoo/ember-gridstack)
- **knockout**: see [demo](https://gridstackjs.com/demo/knockout.html) using component, but check [custom bindings ticket](https://github.com/gridstack/gridstack.js/issues/465) which is likely better approach.
- **Rails**: [gridstack-js-rails](https://github.com/randoum/gridstack-js-rails)
- **React**: work in progress to have wrapper code: see <a href="https://github.com/gridstack/gridstack.js/tree/master/react" target="_blank">React Component</a>. But also see [demo](https://gridstackjs.com/demo/react.html) with [src](https://github.com/gridstack/gridstack.js/tree/master/demo/react.html), or [react-gridstack-example](https://github.com/Inder2108/react-gridstack-example/tree/master/src/App.js), or read on what [hooks to use](https://github.com/gridstack/gridstack.js/issues/735#issuecomment-329888796)
- **Vue**: see [demo](https://gridstackjs.com/demo/vue3js.html) with [v3 src](https://github.com/gridstack/gridstack.js/tree/master/demo/vue3js.html) or [v2 src](https://github.com/gridstack/gridstack.js/tree/master/demo/vue2js.html)
- **Aurelia**: [aurelia-gridstack](https://github.com/aurelia-ui-toolkits/aurelia-gridstack), see [demo](https://aurelia-ui-toolkits.github.io/aurelia-gridstack/)
## Extend Library
You can easily extend or patch gridstack with code like this:
```js
// extend gridstack with our own custom method
GridStack.prototype.printCount = function() {
console.log('grid has ' + this.engine.nodes.length + ' items');
};
let grid = GridStack.init();
// you can now call
grid.printCount();
```
## Extend Engine
You can now (5.1+) easily create your own layout engine to further customize your usage. Here is a typescript example
```ts
import { GridStack, GridStackEngine, GridStackNode, GridStackMoveOpts } from 'gridstack';
class CustomEngine extends GridStackEngine {
/** refined this to move the node to the given new location */
public override moveNode(node: GridStackNode, o: GridStackMoveOpts): boolean {
// keep the same original X and Width and let base do it all...
o.x = node.x;
o.w = node.w;
return super.moveNode(node, o);
}
}
GridStack.registerEngine(CustomEngine); // globally set our custom class
```
## Change grid columns
GridStack makes it very easy if you need [1-12] columns out of the box (default is 12), but you always need **2 things** if you need to customize this:
1) Change the `column` grid option when creating a grid to your number N
```js
GridStack.init( {column: N} );
```
NOTE: step 2 is OLD and not needed with v12+ which uses CSS variables instead of classes
2) also include `gridstack-extra.css` if **N < 12** (else custom CSS - see next). Without these, things will not render/work correctly.
```html
<link href="node_modules/gridstack/dist/gridstack.min.css" rel="stylesheet"/>
<link href="node_modules/gridstack/dist/gridstack-extra.min.css" rel="stylesheet"/>
<div class="grid-stack">...</div>
```
Note: class `.grid-stack-N` will automatically be added and we include `gridstack-extra.min.css` which defines CSS for grids with custom [2-11] columns. Anything more and you'll need to generate the SASS/CSS yourself (see next).
See example: [2 grids demo](http://gridstack.github.io/gridstack.js/demo/two.html) with 6 columns
## Custom columns CSS (OLD, not needed with v12+)
NOTE: step is OLD and not needed with v12+ which uses CSS variables instead of classes
If you need > 12 columns or want to generate the CSS manually you will need to generate CSS rules for `.grid-stack-item[gs-w="X"]` and `.grid-stack-item[gs-x="X"]`.
For instance for 4-column grid you need CSS to be:
```css
.gs-4 > .grid-stack-item[gs-x="1"] { left: 25% }
.gs-4 > .grid-stack-item[gs-x="2"] { left: 50% }
.gs-4 > .grid-stack-item[gs-x="3"] { left: 75% }
.gs-4 > .grid-stack-item { width: 25% }
.gs-4 > .grid-stack-item[gs-w="2"] { width: 50% }
.gs-4 > .grid-stack-item[gs-w="3"] { width: 75% }
.gs-4 > .grid-stack-item[gs-w="4"] { width: 100% }
```
Better yet, here is a SCSS code snippet, you can use sites like [sassmeister.com](https://www.sassmeister.com/) to generate the CSS for you instead:
```scss
$columns: 20;
@function fixed($float) {
@return round($float * 1000) / 1000; // total 2+3 digits being %
}
.gs-#{$columns} > .grid-stack-item {
width: fixed(100% / $columns);
@for $i from 1 through $columns - 1 {
&[gs-x='#{$i}'] { left: fixed((100% / $columns) * $i); }
&[gs-w='#{$i+1}'] { width: fixed((100% / $columns) * ($i+1)); }
}
}
```
you can also use the SCSS [src/gridstack-extra.scss](https://github.com/gridstack/gridstack.js/tree/master/src/gridstack-extra.scss) included in NPM package and modify to add more columns.
Sample gulp command for 30 columns:
```js
gulp.src('node_modules/gridstack/dist/src/gridstack-extra.scss')
.pipe(replace('$start: 2 !default;','$start: 30;'))
.pipe(replace('$end: 11 !default;','$end: 30;'))
.pipe(sass({outputStyle: 'compressed'}))
.pipe(rename({extname: '.min.css'}))
.pipe(gulp.dest('dist/css'))
```
## Override resizable/draggable options
You can override default `resizable`/`draggable` options. For instance to enable other then bottom right resizing handle
you can init gridstack like:
```js
GridStack.init({
resizable: {
handles: 'e,se,s,sw,w'
}
});
```
## Touch devices support
gridstack v6+ now support mobile out of the box, with the addition of native touch event (along with mouse event) for drag&drop and resize.
Older versions (3.2+) required the jq version with added touch punch, but doesn't work well with nested grids.
This option is now the default:
```js
let options = {
alwaysShowResizeHandle: 'mobile' // true if we're on mobile devices
};
GridStack.init(options);
```
See [example](http://gridstack.github.io/gridstack.js/demo/mobile.html).
# Migrating
## Migrating to v0.6
starting in 0.6.x `change` event are no longer sent (for pretty much most nodes!) when an item is just added/deleted unless it also changes other nodes (was incorrect and causing inefficiencies). You may need to track `added|removed` [events](https://github.com/gridstack/gridstack.js/tree/master/doc#events) if you didn't and relied on the old broken behavior.
## Migrating to v1
v1.0.0 removed Jquery from the API and external dependencies, which will require some code changes. Here is a list of the changes:
0. see previous step if not on v0.6 already
1. your code only needs to `import GridStack from 'gridstack'` or include `gridstack.all.js` and `gristack.css` (don't include other JS) and is recommended you do that as internal dependencies will change over time. If you are jquery based, see [jquery app](#jquery-application) section.
2. code change:
**OLD** initializing code + adding a widget + adding an event:
```js
// initialization returned Jquery element, requiring second call to get GridStack var
var grid = $('.grid-stack').gridstack(opts?).data('gridstack');
// returned Jquery element
grid.addWidget($('<div><div class="grid-stack-item-content"> test </div></div>'), undefined, undefined, 2, undefined, true);
// jquery event handler
$('.grid-stack').on('added', function(e, items) {/* items contains info */});
// grid access after init
var grid = $('.grid-stack').data('gridstack');
```
**NEW**
```js
// element identifier defaults to '.grid-stack', returns the grid
// Note: for Typescript use window.GridStack.init() until next native 2.x TS version
var grid = GridStack.init(opts?, element?);
// returns DOM element
grid.addWidget('<div><div class="grid-stack-item-content"> test </div></div>', {width: 2});
// Note: in 3.x it's ever simpler
// grid.addWidget({w:2, content: 'test'})
// event handler
grid.on('added', function(e, items) {/* items contains info */});
// grid access after init
var grid = el.gridstack; // where el = document.querySelector('.grid-stack') or other ways...
```
Other rename changes
```js
`GridStackUI` --> `GridStack`
`GridStackUI.GridStackEngine` --> `GridStack.Engine`
`grid.container` (jquery grid wrapper) --> `grid.el` // (grid DOM element)
`grid.grid` (GridStackEngine) --> `grid.engine`
`grid.setColumn(N)` --> `grid.column(N)` and `grid.column()` // to get value, old API still supported though
```
Recommend looking at the [many samples](./demo) for more code examples.
## Migrating to v2
make sure to read v1 migration first!
v2 is a Typescript rewrite of 1.x, removing all jquery events, using classes and overall code cleanup to support ES6 modules. Your code might need to change from 1.x
1. In general methods that used no args (getter) vs setter can't be used in TS when the arguments differ (set/get are also not function calls so API would have changed). Instead we decided to have <b>all set methods return</b> `GridStack` to they can be chain-able (ex: `grid.float(true).cellHeight(10).column(6)`). Also legacy methods that used to take many parameters will now take a single object (typically `GridStackOptions` or `GridStackWidget`).
```js
`addWidget(el, x, y, width, height)` --> `addWidget(el, {with: 2})`
// Note: in 2.1.x you can now just do addWidget({with: 2, content: "text"})
`float()` --> `getFloat()` // to get value
`cellHeight()` --> `getCellHeight()` // to get value
`verticalMargin` --> `margin` // grid options and API that applies to all 4 sides.
`verticalMargin()` --> `getMargin()` // to get value
```
2. event signatures are generic and not jquery-ui dependent anymore. `gsresizestop` has been removed as `resizestop|dragstop` are now called **after** the DOM attributes have been updated.
3. `oneColumnMode` would trigger when `window.width` < 768px by default. We now check for grid width instead (more correct and supports nesting). You might need to adjust grid `oneColumnSize` or `disableOneColumnMode`.
**Note:** 2.x no longer support legacy IE11 and older due to using more compact ES6 output and typecsript native code. You will need to stay at 1.x
## Migrating to v3
make sure to read v2 migration first!
v3 has a new HTML5 drag&drop plugging (63k total, all native code), while still allowing you to use the legacy jquery-ui version instead (188k), or a new static grid version (34k, no user drag&drop but full API support). You will need to decide which version to use as `gridstack.all.js` no longer exist (same is now `gridstack-jq.js`) - see [include info](#include).
**NOTE**: HTML5 version is almost on parity with the old jquery-ui based drag&drop. the `containment` (prevent a child from being dragged outside it's parent) and `revert` (not clear what it is for yet) are not yet implemented in initial release of v3.0.0.<br>
Also mobile devices don't support h5 `drag` events (will need to handle `touch`) whereas v3.2 jq version now now supports out of the box (see [v3.2 release](https://github.com/gridstack/gridstack.js/releases/tag/v3.2.0))
Breaking changes:
1. include (as mentioned) need to change
2. `GridStack.update(el, opt)` now takes single `GridStackWidget` options instead of only supporting (x,y,w,h) BUT legacy call in JS will continue working the same for now. That method is a complete re-write and does the right constrain and updates now for all the available params.
3. `locked()`, `move()`, `resize()`, `minWidth()`, `minHeight()`, `maxWidth()`, `maxHeight()` methods are hidden from Typescript (JS can still call for now) as they are just 1 liner wrapper around `update(el, opt)` anyway and will go away soon. (ex: `move(el, x, y)` => `update(el, {x, y})`)
4. item attribute like `data-gs-min-width` is now `gs-min-w`. We removed 'data-' from all attributes, and shorten 'width|height' to just 'w|h' to require less typing and more efficient (2k saved in .js alone!).
5. `GridStackWidget` used in most API `width|height|minWidth|minHeight|maxWidth|maxHeight` are now shorter `w|h|minW|minH|maxW|maxH` as well
## Migrating to v4
make sure to read v3 migration first!
v4 is a complete re-write of the collision and drag in/out heuristics to fix some very long standing request & bugs. It also greatly improved usability. Read the release notes for more detail.
**Unlikely** Breaking Changes (internal usage):
1. `removeTimeout` was removed (feedback over trash will be immediate - actual removal still on mouse up)
2. the following `GridStackEngine` methods changed (used internally, doesn't affect `GridStack` public API)
```js
// moved to 3 methods with new option params to support new code and pixel coverage check
`collision()` -> `collide(), collideAll(), collideCoverage()`
`moveNodeCheck(node, x, y, w, h)` -> `moveNodeCheck(node, opt: GridStackMoveOpts)`
`isNodeChangedPosition(node, x, y, w, h)` -> `changedPosConstrain(node, opt: GridStackMoveOpts)`
`moveNode(node, x, y, w, h, noPack)` -> `moveNode(node, opt: GridStackMoveOpts)`
```
3. removed old obsolete (v0.6-v1 methods/attrs) `getGridHeight()`, `verticalMargin`, `data-gs-current-height`,
`locked()`, `maxWidth()`, `minWidth()`, `maxHeight()`, `minHeight()`, `move()`, `resize()`
## Migrating to v5
make sure to read v4 migration first!
v5 does not have any breaking changes from v4, but a focus on nested grids in h5 mode:
You can now drag in/out of parent into nested child, with new API parameters values. See the release notes.
## Migrating to v6
the API did not really change from v5, but a complete re-write of Drag&Drop to use native `mouseevent` (instead of HTML draggable=true which is buggy on Mac Safari, and doesn't work on mobile devices) and `touchevent` (mobile), and we no longer have jquery ui option (wasn't working well for nested grids, didn't want to maintain legacy lib).
The main difference is you only need to include gridstack.js and get D&D (desktop and mobile) out of the box for the same size as h5 version.
## Migrating to v7
New addition, no API breakage per say. See release notes about creating sub-grids on the fly.
## Migrating to v8
Possible breaking change if you use nested grid JSON format, or original Angular wrapper, or relied on specific CSS paths. Also target is now ES2020 (see release notes).
* `GridStackOptions.subGrid` -> `GridStackOptions.subGridOpts` rename. We now have `GridStackWidget.subGridOpts` vs `GridStackNode.subGrid` (was both types which is error prone)
* `GridStackOptions.addRemoveCB` -> `GridStack.addRemoveCB` is now global instead of grid option
* removed `GridStackOptions.dragInOptions` since `GridStack.setupDragIn()` has it replaced since 4.0
* remove `GridStackOptions.minWidth` obsolete since 5.1, use `oneColumnSize` instead
* CSS rules removed `.grid-stack` prefix for anything already gs based, 12 column (default) now uses `.gs-12`, extra.css is less than 1/4th it original size!, `gs-min|max_w|h` attribute no longer written (but read)
## Migrating to v9
New addition - see release notes about `sizeToContent` feature.
Possible break:
* `GridStack.onParentResize()` is now called `onResize()` as grid now directly track size change, no longer involving parent per say to tell us anything. Note sure why it was public.
## Migrating to v10
we now support much richer responsive behavior with `GridStackOptions.columnOpts` including any breakpoint width:column pairs, or automatic column sizing.
breaking change:
* `disableOneColumnMode`, `oneColumnSize` have been removed (thought we temporary convert if you have them). use `columnOpts: { breakpoints: [{w:768, c:1}] }` for the same behavior.
* 1 column mode switch is no longer by default (`columnOpts` is not defined) as too many new users had issues. Instead set it explicitly (see above).
* `oneColumnModeDomSort` has been removed. Planning to support per column layouts at some future times. TBD
## Migrating to v11
* All instances of `el.innerHTML = 'some content'` have been removed for security reason as it opens up some potential for accidental XSS.
* Side panel drag&drop complete rewrite.
* new lazy loading option
**Breaking change:**
* V11 add new `GridStack.renderCB` that is called for you to create the widget content (entire GridStackWidget is passed so you can use id or some other field as logic) while GS creates the 2 needed parent divs + classes, unlike `GridStack.addRemoveCB` which doesn't create anything for you. Both can be handy for Angular/React/Vue frameworks.
* `addWidget(w: GridStackWidget)` is now the only supported format, no more string content passing. You will need to create content yourself as shown below, OR use `GridStack.createWidgetDivs()` to create parent divs, do the innerHtml, then call `makeWidget(el)` instead.
* if your code relies on `GridStackWidget.content` with real HTML (like a few demos) it is up to you to do this:
```ts
// NOTE: REAL apps would sanitize-html or DOMPurify before blinding setting innerHTML. see #2736
GridStack.renderCB = function(el: HTMLElement, w: GridStackNode) {
el.innerHTML = w.content;
};
// now you can create widgets like this again
let gridWidget = grid.addWidget({x, y, w, h, content: '<div>My html content</div>'});
```
**Potential breaking change:**
* BIG overall to how sidepanel helper drag&drop is done:
1. `clone()` helper is now passed full HTML element dragged, not an event on `grid-stack-item-content` so you can clone or set attr at the top.
2. use any class/structure you want for side panel items (see two.html)
3. `GridStack.setupDragIn()` now support associating a `GridStackWidget` for each sidepanel that will be used to define what to create on drop!
4. if no `GridStackWidget` is defined, the helper will now be inserted as is, and NOT original sidepanel item.
5. support DOM gs- attr as well as gridstacknode JSON (see two.html) alternatives.
## Migrating to v12
* column and cell height code has been re-writen to use browser CSS variables, and we no longer need a tons of custom CSS classes!
this fixes a long standing issue where people forget to include the right CSS for non 12 columns layouts, and a big speedup in many cases (many columns, or small cellHeight values).
**Potential breaking change:**
* `gridstack-extra.min.css` no longer exist, nor is custom column CSS classes needed. API/options hasn't changed.
* (v12.1) `ES5` folder content has been removed - was for IE support, which has been dropped.
* (v12.1) nested grid events are now sent to the main grid. You might have to adjust your workaround of this missing feature. nested.html demo has been adjusted.
# jQuery Application
This is **old and no longer apply to v6+**. You'll need to use v5.1.1 and before
```js
import 'gridstack/dist/gridstack.min.css';
import { GridStack } from 'gridstack';
import 'gridstack/dist/jq/gridstack-dd-jqueryui';
```
**Note**: `jquery` & `jquery-ui` are imported by name, so you will have to specify their location in your webpack (or equivalent) config file,
which means you can possibly bring your own version
```js
alias: {
'jquery': 'gridstack/dist/jq/jquery.js',
'jquery-ui': 'gridstack/dist/jq/jquery-ui.js',
'jquery.ui': 'gridstack/dist/jq/jquery-ui.js',
'jquery.ui.touch-punch': 'gridstack/dist/jq/jquery.ui.touch-punch.js',
},
```
Alternatively (single combined file) in html
```html
<link href="node_modules/gridstack/dist/gridstack.min.css" rel="stylesheet"/>
<!-- HTML5 drag&drop (70k) -->
<script src="node_modules/gridstack/dist/gridstack-h5.js"></script>
<!-- OR jquery-ui drag&drop (195k) -->
<script src="node_modules/gridstack/dist/gridstack-jq.js"></script>
<!-- OR static grid (40k) -->
<script src="node_modules/gridstack/dist/gridstack-static.js"></script>
```
We have a native HTML5 drag'n'drop through the plugin system (default), but the jquery-ui version can be used instead. It will bundle `jquery` (3.5.1) + `jquery-ui` (1.13.1 minimal drag|drop|resize) + `jquery-ui-touch-punch` (1.0.8 for mobile support) in `gridstack-jq.js`.
**NOTE: in v4, v3**: we ES6 module import jquery & jquery-ui by name, so you need to specify location of those .js files, which means you might be able to bring your own version as well. See the include instructions.
**NOTE: in v1.x** IFF you want to use gridstack-jq instead and your app needs to bring your own JQ version, you should **instead** include `gridstack-poly.min.js` (optional IE support) + `gridstack.min.js` + `gridstack.jQueryUI.min.js` after you import your JQ libs. But note that there are issue with jQuery and ES6 import (see [1306](https://github.com/gridstack/gridstack.js/issues/1306)).
As for events, you can still use `$(".grid-stack").on(...)` for the version that uses jquery-ui for things we don't support.
# Changes
View our change log [here](https://github.com/gridstack/gridstack.js/tree/master/doc/CHANGES.md).
# Usage Trend
[Usage Trend of gridstack](https://npm-compare.com/gridstack#timeRange=THREE_YEARS)
<a href="https://npm-compare.com/gridstack#timeRange=THREE_YEARS" target="_blank">
<img src="https://npm-compare.com/img/npm-trend/THREE_YEARS/gridstack.png" width="70%" alt="NPM Usage Trend of gridstack" />
</a>
# The Team
gridstack.js is currently maintained by [Alain Dumesny](https://github.com/adumesny), before that [Dylan Weiss](https://github.com/radiolips), originally created by [Pavel Reznikov](https://github.com/troolee). We appreciate [all contributors](https://github.com/gridstack/gridstack.js/graphs/contributors) for help.

69
node_modules/gridstack/dist/dd-base-impl.d.ts generated vendored Normal file
View File

@@ -0,0 +1,69 @@
/**
* dd-base-impl.ts 12.4.2
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
*/
/**
* Type for event callback functions used in drag & drop operations.
* Can return boolean to indicate if the event should continue propagation.
*/
export type EventCallback = (event: Event) => boolean | void;
/**
* Abstract base class for all drag & drop implementations.
* Provides common functionality for event handling, enable/disable state,
* and lifecycle management used by draggable, droppable, and resizable implementations.
*/
export declare abstract class DDBaseImplement {
/**
* Returns the current disabled state.
* Note: Use enable()/disable() methods to change state as other operations need to happen.
*/
get disabled(): boolean;
/**
* Register an event callback for the specified event.
*
* @param event - Event name to listen for
* @param callback - Function to call when event occurs
*/
on(event: string, callback: EventCallback): void;
/**
* Unregister an event callback for the specified event.
*
* @param event - Event name to stop listening for
*/
off(event: string): void;
/**
* Enable this drag & drop implementation.
* Subclasses should override to perform additional setup.
*/
enable(): void;
/**
* Disable this drag & drop implementation.
* Subclasses should override to perform additional cleanup.
*/
disable(): void;
/**
* Destroy this drag & drop implementation and clean up resources.
* Removes all event handlers and clears internal state.
*/
destroy(): void;
/**
* Trigger a registered event callback if one exists and the implementation is enabled.
*
* @param eventName - Name of the event to trigger
* @param event - DOM event object to pass to the callback
* @returns Result from the callback function, if any
*/
triggerEvent(eventName: string, event: Event): boolean | void;
}
/**
* Interface for HTML elements extended with drag & drop options.
* Used to associate DD configuration with DOM elements.
*/
export interface HTMLElementExtendOpt<T> {
/** The HTML element being extended */
el: HTMLElement;
/** The drag & drop options/configuration */
option: T;
/** Method to update the options and return the DD implementation */
updateOption(T: any): DDBaseImplement;
}

70
node_modules/gridstack/dist/dd-base-impl.js generated vendored Normal file
View File

@@ -0,0 +1,70 @@
/**
* dd-base-impl.ts 12.4.2
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
*/
/**
* Abstract base class for all drag & drop implementations.
* Provides common functionality for event handling, enable/disable state,
* and lifecycle management used by draggable, droppable, and resizable implementations.
*/
export class DDBaseImplement {
constructor() {
/** @internal */
this._eventRegister = {};
}
/**
* Returns the current disabled state.
* Note: Use enable()/disable() methods to change state as other operations need to happen.
*/
get disabled() { return this._disabled; }
/**
* Register an event callback for the specified event.
*
* @param event - Event name to listen for
* @param callback - Function to call when event occurs
*/
on(event, callback) {
this._eventRegister[event] = callback;
}
/**
* Unregister an event callback for the specified event.
*
* @param event - Event name to stop listening for
*/
off(event) {
delete this._eventRegister[event];
}
/**
* Enable this drag & drop implementation.
* Subclasses should override to perform additional setup.
*/
enable() {
this._disabled = false;
}
/**
* Disable this drag & drop implementation.
* Subclasses should override to perform additional cleanup.
*/
disable() {
this._disabled = true;
}
/**
* Destroy this drag & drop implementation and clean up resources.
* Removes all event handlers and clears internal state.
*/
destroy() {
delete this._eventRegister;
}
/**
* Trigger a registered event callback if one exists and the implementation is enabled.
*
* @param eventName - Name of the event to trigger
* @param event - DOM event object to pass to the callback
* @returns Result from the callback function, if any
*/
triggerEvent(eventName, event) {
if (!this.disabled && this._eventRegister && this._eventRegister[eventName])
return this._eventRegister[eventName](event);
}
}
//# sourceMappingURL=dd-base-impl.js.map

1
node_modules/gridstack/dist/dd-base-impl.js.map generated vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"dd-base-impl.js","sourceRoot":"","sources":["../src/dd-base-impl.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH;;;;GAIG;AACH,MAAM,OAAgB,eAAe;IAArC;QASE,gBAAgB;QACN,mBAAc,GAEpB,EAAE,CAAC;IAwDT,CAAC;IAnEC;;;OAGG;IACH,IAAW,QAAQ,KAAgB,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;IAS3D;;;;;OAKG;IACI,EAAE,CAAC,KAAa,EAAE,QAAuB;QAC9C,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC;IACxC,CAAC;IAED;;;;OAIG;IACI,GAAG,CAAC,KAAa;QACtB,OAAO,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IACpC,CAAC;IAED;;;OAGG;IACI,MAAM;QACX,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;IACzB,CAAC;IAED;;;OAGG;IACI,OAAO;QACZ,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;IACxB,CAAC;IAED;;;OAGG;IACI,OAAO;QACZ,OAAO,IAAI,CAAC,cAAc,CAAC;IAC7B,CAAC;IAED;;;;;;OAMG;IACI,YAAY,CAAC,SAAiB,EAAE,KAAY;QACjD,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC;YACzE,OAAO,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC;CACF","sourcesContent":["/**\n * dd-base-impl.ts 12.4.2\n * Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license\n */\n\n/**\n * Type for event callback functions used in drag & drop operations.\n * Can return boolean to indicate if the event should continue propagation.\n */\nexport type EventCallback = (event: Event) => boolean|void;\n\n/**\n * Abstract base class for all drag & drop implementations.\n * Provides common functionality for event handling, enable/disable state,\n * and lifecycle management used by draggable, droppable, and resizable implementations.\n */\nexport abstract class DDBaseImplement {\n /**\n * Returns the current disabled state.\n * Note: Use enable()/disable() methods to change state as other operations need to happen.\n */\n public get disabled(): boolean { return this._disabled; }\n\n /** @internal */\n protected _disabled: boolean; // initial state to differentiate from false\n /** @internal */\n protected _eventRegister: {\n [eventName: string]: EventCallback;\n } = {};\n\n /**\n * Register an event callback for the specified event.\n *\n * @param event - Event name to listen for\n * @param callback - Function to call when event occurs\n */\n public on(event: string, callback: EventCallback): void {\n this._eventRegister[event] = callback;\n }\n\n /**\n * Unregister an event callback for the specified event.\n *\n * @param event - Event name to stop listening for\n */\n public off(event: string): void {\n delete this._eventRegister[event];\n }\n\n /**\n * Enable this drag & drop implementation.\n * Subclasses should override to perform additional setup.\n */\n public enable(): void {\n this._disabled = false;\n }\n\n /**\n * Disable this drag & drop implementation.\n * Subclasses should override to perform additional cleanup.\n */\n public disable(): void {\n this._disabled = true;\n }\n\n /**\n * Destroy this drag & drop implementation and clean up resources.\n * Removes all event handlers and clears internal state.\n */\n public destroy(): void {\n delete this._eventRegister;\n }\n\n /**\n * Trigger a registered event callback if one exists and the implementation is enabled.\n *\n * @param eventName - Name of the event to trigger\n * @param event - DOM event object to pass to the callback\n * @returns Result from the callback function, if any\n */\n public triggerEvent(eventName: string, event: Event): boolean|void {\n if (!this.disabled && this._eventRegister && this._eventRegister[eventName])\n return this._eventRegister[eventName](event);\n }\n}\n\n/**\n * Interface for HTML elements extended with drag & drop options.\n * Used to associate DD configuration with DOM elements.\n */\nexport interface HTMLElementExtendOpt<T> {\n /** The HTML element being extended */\n el: HTMLElement;\n /** The drag & drop options/configuration */\n option: T;\n /** Method to update the options and return the DD implementation */\n updateOption(T): DDBaseImplement;\n}\n"]}

20
node_modules/gridstack/dist/dd-draggable.d.ts generated vendored Normal file
View File

@@ -0,0 +1,20 @@
/**
* dd-draggable.ts 12.4.2
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
*/
import { DDBaseImplement, HTMLElementExtendOpt } from './dd-base-impl';
import { GridItemHTMLElement, DDDragOpt } from './types';
type DDDragEvent = 'drag' | 'dragstart' | 'dragstop';
export declare class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt<DDDragOpt> {
el: GridItemHTMLElement;
option: DDDragOpt;
helper: HTMLElement;
constructor(el: GridItemHTMLElement, option?: DDDragOpt);
on(event: DDDragEvent, callback: (event: DragEvent) => void): void;
off(event: DDDragEvent): void;
enable(): void;
disable(forDestroy?: boolean): void;
destroy(): void;
updateOption(opts: DDDragOpt): DDDraggable;
}
export {};

367
node_modules/gridstack/dist/dd-draggable.js generated vendored Normal file
View File

@@ -0,0 +1,367 @@
/**
* dd-draggable.ts 12.4.2
* Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license
*/
import { DDManager } from './dd-manager';
import { Utils } from './utils';
import { DDBaseImplement } from './dd-base-impl';
import { isTouch, touchend, touchmove, touchstart, pointerdown, DDTouch } from './dd-touch';
// make sure we are not clicking on known object that handles mouseDown
const skipMouseDown = 'input,textarea,button,select,option,[contenteditable="true"],.ui-resizable-handle';
// let count = 0; // TEST
class DDDraggable extends DDBaseImplement {
constructor(el, option = {}) {
super();
this.el = el;
this.option = option;
/** @internal */
this.dragTransform = {
xScale: 1,
yScale: 1,
xOffset: 0,
yOffset: 0
};
// get the element that is actually supposed to be dragged by
const handleName = option?.handle?.substring(1);
const n = el.gridstackNode;
this.dragEls = !handleName || el.classList.contains(handleName) ? [el] : (n?.subGrid ? [el.querySelector(option.handle) || el] : Array.from(el.querySelectorAll(option.handle)));
if (this.dragEls.length === 0) {
this.dragEls = [el];
}
// create var event binding so we can easily remove and still look like TS methods (unlike anonymous functions)
this._mouseDown = this._mouseDown.bind(this);
this._mouseMove = this._mouseMove.bind(this);
this._mouseUp = this._mouseUp.bind(this);
this._keyEvent = this._keyEvent.bind(this);
this.enable();
}
on(event, callback) {
super.on(event, callback);
}
off(event) {
super.off(event);
}
enable() {
if (this.disabled === false)
return;
super.enable();
this.dragEls.forEach(dragEl => {
dragEl.addEventListener('mousedown', this._mouseDown);
if (isTouch) {
dragEl.addEventListener('touchstart', touchstart);
dragEl.addEventListener('pointerdown', pointerdown);
// dragEl.style.touchAction = 'none'; // not needed unlike pointerdown doc comment
}
});
this.el.classList.remove('ui-draggable-disabled');
}
disable(forDestroy = false) {
if (this.disabled === true)
return;
super.disable();
this.dragEls.forEach(dragEl => {
dragEl.removeEventListener('mousedown', this._mouseDown);
if (isTouch) {
dragEl.removeEventListener('touchstart', touchstart);
dragEl.removeEventListener('pointerdown', pointerdown);
}
});
if (!forDestroy)
this.el.classList.add('ui-draggable-disabled');
}
destroy() {
if (this.dragTimeout)
window.clearTimeout(this.dragTimeout);
delete this.dragTimeout;
if (this.mouseDownEvent)
this._mouseUp(this.mouseDownEvent);
this.disable(true);
delete this.el;
delete this.helper;
delete this.option;
super.destroy();
}
updateOption(opts) {
Object.keys(opts).forEach(key => this.option[key] = opts[key]);
return this;
}
/** @internal call when mouse goes down before a dragstart happens */
_mouseDown(e) {
// if real brower event (trusted:true vs false for our simulated ones) and we didn't correctly clear the last touch event, clear things up
if (DDTouch.touchHandled && e.isTrusted)
DDTouch.touchHandled = false;
// don't let more than one widget handle mouseStart
if (DDManager.mouseHandled)
return;
if (e.button !== 0)
return true; // only left click
// make sure we are not clicking on known object that handles mouseDown, or ones supplied by the user
if (!this.dragEls.find(el => el === e.target) && e.target.closest(skipMouseDown))
return true;
if (this.option.cancel) {
if (e.target.closest(this.option.cancel))
return true;
}
this.mouseDownEvent = e;
delete this.dragging;
delete DDManager.dragElement;
delete DDManager.dropElement;
// document handler so we can continue receiving moves as the item is 'fixed' position, and capture=true so WE get a first crack
document.addEventListener('mousemove', this._mouseMove, { capture: true, passive: true }); // true=capture, not bubble
document.addEventListener('mouseup', this._mouseUp, true);
if (isTouch) {
e.currentTarget.addEventListener('touchmove', touchmove);
e.currentTarget.addEventListener('touchend', touchend);
}
e.preventDefault();
// preventDefault() prevents blur event which occurs just after mousedown event.
// if an editable content has focus, then blur must be call
if (document.activeElement)
document.activeElement.blur();
DDManager.mouseHandled = true;
return true;
}
/** @internal method to call actual drag event */
_callDrag(e) {
if (!this.dragging)
return;
const ev = Utils.initEvent(e, { target: this.el, type: 'drag' });
if (this.option.drag) {
this.option.drag(ev, this.ui());
}
this.triggerEvent('drag', ev);
}
/** @internal called when the main page (after successful mousedown) receives a move event to drag the item around the screen */
_mouseMove(e) {
// console.log(`${count++} move ${e.x},${e.y}`)
const s = this.mouseDownEvent;
this.lastDrag = e;
if (this.dragging) {
this._dragFollow(e);
// delay actual grid handling drag until we pause for a while if set
if (DDManager.pauseDrag) {
const pause = Number.isInteger(DDManager.pauseDrag) ? DDManager.pauseDrag : 100;
if (this.dragTimeout)
window.clearTimeout(this.dragTimeout);
this.dragTimeout = window.setTimeout(() => this._callDrag(e), pause);
}
else {
this._callDrag(e);
}
}
else if (Math.abs(e.x - s.x) + Math.abs(e.y - s.y) > 3) {
/**
* don't start unless we've moved at least 3 pixels
*/
this.dragging = true;
DDManager.dragElement = this;
// if we're dragging an actual grid item, set the current drop as the grid (to detect enter/leave)
const grid = this.el.gridstackNode?.grid;
if (grid) {
DDManager.dropElement = grid.el.ddElement.ddDroppable;
}
else {
delete DDManager.dropElement;
}
this.helper = this._createHelper();
this._setupHelperContainmentStyle();
this.dragTransform = Utils.getValuesFromTransformedElement(this.helperContainment);
this.dragOffset = this._getDragOffset(e, this.el, this.helperContainment);
this._setupHelperStyle(e);
const ev = Utils.initEvent(e, { target: this.el, type: 'dragstart' });
if (this.option.start) {
this.option.start(ev, this.ui());
}
this.triggerEvent('dragstart', ev);
// now track keyboard events to cancel or rotate
document.addEventListener('keydown', this._keyEvent);
}
// e.preventDefault(); // passive = true. OLD: was needed otherwise we get text sweep text selection as we drag around
return true;
}
/** @internal call when the mouse gets released to drop the item at current location */
_mouseUp(e) {
document.removeEventListener('mousemove', this._mouseMove, true);
document.removeEventListener('mouseup', this._mouseUp, true);
if (isTouch && e.currentTarget) { // destroy() during nested grid call us again wit fake _mouseUp
e.currentTarget.removeEventListener('touchmove', touchmove, true);
e.currentTarget.removeEventListener('touchend', touchend, true);
}
if (this.dragging) {
delete this.dragging;
delete this.el.gridstackNode?._origRotate;
document.removeEventListener('keydown', this._keyEvent);
// reset the drop target if dragging over ourself (already parented, just moving during stop callback below)
if (DDManager.dropElement?.el === this.el.parentElement) {
delete DDManager.dropElement;
}
this.helperContainment.style.position = this.parentOriginStylePosition || null;
if (this.helper !== this.el)
this.helper.remove(); // hide now
this._removeHelperStyle();
const ev = Utils.initEvent(e, { target: this.el, type: 'dragstop' });
if (this.option.stop) {
this.option.stop(ev); // NOTE: destroy() will be called when removing item, so expect NULL ptr after!
}
this.triggerEvent('dragstop', ev);
// call the droppable method to receive the item
if (DDManager.dropElement) {
DDManager.dropElement.drop(e);
}
}
delete this.helper;
delete this.mouseDownEvent;
delete DDManager.dragElement;
delete DDManager.dropElement;
delete DDManager.mouseHandled;
e.preventDefault();
}
/** @internal call when keys are being pressed - use Esc to cancel, R to rotate */
_keyEvent(e) {
const n = this.el.gridstackNode;
const grid = n?.grid || DDManager.dropElement?.el?.gridstack;
if (e.key === 'Escape') {
if (n && n._origRotate) {
n._orig = n._origRotate;
delete n._origRotate;
}
grid?.cancelDrag();
this._mouseUp(this.mouseDownEvent);
}
else if (n && grid && (e.key === 'r' || e.key === 'R')) {
if (!Utils.canBeRotated(n))
return;
n._origRotate = n._origRotate || { ...n._orig }; // store the real orig size in case we Esc after doing rotation
delete n._moving; // force rotate to happen (move waits for >50% coverage otherwise)
grid.setAnimation(false) // immediate rotate so _getDragOffset() gets the right dom size below
.rotate(n.el, { top: -this.dragOffset.offsetTop, left: -this.dragOffset.offsetLeft })
.setAnimation();
n._moving = true;
this.dragOffset = this._getDragOffset(this.lastDrag, n.el, this.helperContainment);
this.helper.style.width = this.dragOffset.width + 'px';
this.helper.style.height = this.dragOffset.height + 'px';
Utils.swap(n._orig, 'w', 'h');
delete n._rect;
this._mouseMove(this.lastDrag);
}
}
/** @internal create a clone copy (or user defined method) of the original drag item if set */
_createHelper() {
let helper = this.el;
if (typeof this.option.helper === 'function') {
helper = this.option.helper(this.el);
}
else if (this.option.helper === 'clone') {
helper = Utils.cloneNode(this.el);
}
if (!helper.parentElement) {
Utils.appendTo(helper, this.option.appendTo === 'parent' ? this.el.parentElement : this.option.appendTo);
}
this.dragElementOriginStyle = DDDraggable.originStyleProp.map(prop => this.el.style[prop]);
return helper;
}
/** @internal set the fix position of the dragged item */
_setupHelperStyle(e) {
this.helper.classList.add('ui-draggable-dragging');
this.el.gridstackNode?.grid?.el.classList.add('grid-stack-dragging');
// TODO: set all at once with style.cssText += ... ? https://stackoverflow.com/questions/3968593
const style = this.helper.style;
style.pointerEvents = 'none'; // needed for over items to get enter/leave
// style.cursor = 'move'; // TODO: can't set with pointerEvents=none ! (no longer in CSS either as no-op)
style.width = this.dragOffset.width + 'px';
style.height = this.dragOffset.height + 'px';
style.willChange = 'left, top';
style.position = 'fixed'; // let us drag between grids by not clipping as parent .grid-stack is position: 'relative'
this._dragFollow(e); // now position it
style.transition = 'none'; // show up instantly
setTimeout(() => {
if (this.helper) {
style.transition = null; // recover animation
}
}, 0);
return this;
}
/** @internal restore back the original style before dragging */
_removeHelperStyle() {
this.helper.classList.remove('ui-draggable-dragging');
this.el.gridstackNode?.grid?.el.classList.remove('grid-stack-dragging');
const node = this.helper?.gridstackNode;
// don't bother restoring styles if we're gonna remove anyway...
if (!node?._isAboutToRemove && this.dragElementOriginStyle) {
const helper = this.helper;
// don't animate, otherwise we animate offseted when switching back to 'absolute' from 'fixed'.
// TODO: this also removes resizing animation which doesn't have this issue, but others.
// Ideally both would animate ('move' would immediately restore 'absolute' and adjust coordinate to match,
// then trigger a delay (repaint) to restore to final dest with animate) but then we need to make sure 'resizestop'
// is called AFTER 'transitionend' event is received (see https://github.com/gridstack/gridstack.js/issues/2033)
const transition = this.dragElementOriginStyle['transition'] || null;
helper.style.transition = this.dragElementOriginStyle['transition'] = 'none'; // can't be NULL #1973
DDDraggable.originStyleProp.forEach(prop => helper.style[prop] = this.dragElementOriginStyle[prop] || null);
setTimeout(() => helper.style.transition = transition, 50); // recover animation from saved vars after a pause (0 isn't enough #1973)
}
delete this.dragElementOriginStyle;
return this;
}
/** @internal updates the top/left position to follow the mouse */
_dragFollow(e) {
const containmentRect = { left: 0, top: 0 };
// if (this.helper.style.position === 'absolute') { // we use 'fixed'
// const { left, top } = this.helperContainment.getBoundingClientRect();
// containmentRect = { left, top };
// }
const style = this.helper.style;
const offset = this.dragOffset;
style.left = (e.clientX + offset.offsetLeft - containmentRect.left) * this.dragTransform.xScale + 'px';
style.top = (e.clientY + offset.offsetTop - containmentRect.top) * this.dragTransform.yScale + 'px';
}
/** @internal */
_setupHelperContainmentStyle() {
this.helperContainment = this.helper.parentElement;
if (this.helper.style.position !== 'fixed') {
this.parentOriginStylePosition = this.helperContainment.style.position;
if (getComputedStyle(this.helperContainment).position.match(/static/)) {
this.helperContainment.style.position = 'relative';
}
}
return this;
}
/** @internal */
_getDragOffset(event, el, parent) {
// in case ancestor has transform/perspective css properties that change the viewpoint
let xformOffsetX = 0;
let xformOffsetY = 0;
if (parent) {
xformOffsetX = this.dragTransform.xOffset;
xformOffsetY = this.dragTransform.yOffset;
}
const targetOffset = el.getBoundingClientRect();
return {
left: targetOffset.left,
top: targetOffset.top,
offsetLeft: -event.clientX + targetOffset.left - xformOffsetX,
offsetTop: -event.clientY + targetOffset.top - xformOffsetY,
width: targetOffset.width * this.dragTransform.xScale,
height: targetOffset.height * this.dragTransform.yScale
};
}
/** @internal TODO: set to public as called by DDDroppable! */
ui() {
const containmentEl = this.el.parentElement;
const containmentRect = containmentEl.getBoundingClientRect();
const offset = this.helper.getBoundingClientRect();
return {
position: {
top: (offset.top - containmentRect.top) * this.dragTransform.yScale,
left: (offset.left - containmentRect.left) * this.dragTransform.xScale
}
/* not used by GridStack for now...
helper: [this.helper], //The object arr representing the helper that's being dragged.
offset: { top: offset.top, left: offset.left } // Current offset position of the helper as { top, left } object.
*/
};
}
}
/** @internal properties we change during dragging, and restore back */
DDDraggable.originStyleProp = ['width', 'height', 'transform', 'transform-origin', 'transition', 'pointerEvents', 'position', 'left', 'top', 'minWidth', 'willChange'];
export { DDDraggable };
//# sourceMappingURL=dd-draggable.js.map

Some files were not shown because too many files have changed in this diff Show More