";
- $rand = mt_rand();
- Html::initEditorSystem($field['name'].$rand);
- echo "";
- break;
- }
- }
-
}
diff --git a/inc/supplier.class.php b/inc/supplier.class.php
index 8a1a58337d1..0c139ea8e42 100644
--- a/inc/supplier.class.php
+++ b/inc/supplier.class.php
@@ -104,7 +104,7 @@ function defineTabs($options = []) {
* - target form target
* - withtemplate boolean : template or basic item
*
- *@return Nothing (display)
+ *@return void
**/
function showForm($ID, $options = []) {
@@ -453,7 +453,7 @@ function getLinks($withname = false) {
/**
* Print the HTML array for infocoms linked
*
- *@return Nothing (display)
+ *@return void
*
**/
function showInfocoms() {
diff --git a/inc/ticket.class.php b/inc/ticket.class.php
index a4ba89eb228..b18c00b9d24 100644
--- a/inc/ticket.class.php
+++ b/inc/ticket.class.php
@@ -832,11 +832,24 @@ static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtem
function defineTabs($options = []) {
+ global $CFG_GLPI;
+
$ong = [];
+
$this->defineDefaultObjectTabs($ong, $options);
$this->addStandardTab('TicketValidation', $ong, $options);
$this->addStandardTab('KnowbaseItem_Item', $ong, $options);
$this->addStandardTab('Item_Ticket', $ong, $options);
+
+ // Enable impact tab if there is a valid linked item
+ foreach ($this->getLinkedItems() as $linkedItem) {
+ $class = $linkedItem['itemtype'];
+ if (isset($CFG_GLPI['impact_asset_types'][$class])) {
+ $this->addStandardTab('Impact', $ong, $options);
+ break;
+ }
+ }
+
$this->addStandardTab('TicketCost', $ong, $options);
$this->addStandardTab('Itil_Project', $ong, $options);
$this->addStandardTab('ProjectTask_Ticket', $ong, $options);
@@ -851,7 +864,7 @@ function defineTabs($options = []) {
/**
* Retrieve data of the hardware linked to the ticket if exists
*
- * @return nothing : set computerfound to 1 if founded
+ * @return void
**/
function getAdditionalDatas() {
@@ -1943,7 +1956,7 @@ function post_addItem() {
*
* @param $input array : input array
*
- * @return nothing
+ * @return boolean
**/
function manageValidationAdd($input) {
@@ -2214,6 +2227,47 @@ function countActiveTicketsForItem($itemtype, $items_id) {
return $result['cpt'];
}
+ /**
+ * Get active tickets for an item
+ *
+ * @since 9.5
+ *
+ * @param string $itemtype Item type
+ * @param integer $items_id ID of the Item
+ * @param string $type Type of the tickets (incident or request)
+ *
+ * @return DBmysqlIterator
+ */
+ public function getActiveTicketsForItem($itemtype, $items_id, $type) {
+ global $DB;
+
+ return $DB->request([
+ 'SELECT' => [
+ $this->getTable() . '.id',
+ $this->getTable() . '.name'
+ ],
+ 'FROM' => $this->getTable(),
+ 'LEFT JOIN' => [
+ 'glpi_items_tickets' => [
+ 'ON' => [
+ 'glpi_items_tickets' => 'tickets_id',
+ $this->getTable() => 'id'
+ ]
+ ]
+ ],
+ 'WHERE' => [
+ 'glpi_items_tickets.itemtype' => $itemtype,
+ 'glpi_items_tickets.items_id' => $items_id,
+ $this->getTable() . '.type' => $type,
+ 'NOT' => [
+ $this->getTable() . '.status' => array_merge(
+ $this->getSolvedStatusArray(),
+ $this->getClosedStatusArray()
+ )
+ ]
+ ]
+ ]);
+ }
/**
* Count solved tickets for an hardware last X days
@@ -3418,7 +3472,7 @@ static function computeTco(CommonDBTM $item) {
* @param $ticket_template boolean ticket template for preview : false if not used for preview
* (false by default)
*
- * @return nothing (print the helpdesk)
+ * @return void
**/
function showFormHelpdesk($ID, $ticket_template = false) {
global $CFG_GLPI;
@@ -7074,4 +7128,38 @@ public static function merge(int $merge_target_id, array $ticket_ids, array &$st
}
return true;
}
+
+ /**
+ * Get assets linked to this object
+ *
+ * @since 9.5
+ *
+ * @param bool $addNames Insert asset names
+ *
+ * @return array
+ */
+ public function getLinkedItems(bool $addNames = true) {
+ global $DB;
+
+ $assets = $DB->request([
+ 'SELECT' => ["id", "itemtype", "items_id"],
+ 'FROM' => "glpi_items_tickets",
+ 'WHERE' => ["tickets_id" => $this->getID()]
+ ]);
+
+ $assets = iterator_to_array($assets);
+
+ if ($addNames) {
+ foreach ($assets as $key => $asset) {
+ /** @var CommonDBTM $item */
+ $item = new $asset['itemtype'];
+ $item->getFromDB($asset['items_id']);
+
+ // Add name
+ $assets[$key]['name'] = $item->fields['name'];
+ }
+ }
+
+ return $assets;
+ }
}
diff --git a/inc/ticket_ticket.class.php b/inc/ticket_ticket.class.php
index c5c0317b5e6..18ff35efa3c 100644
--- a/inc/ticket_ticket.class.php
+++ b/inc/ticket_ticket.class.php
@@ -166,7 +166,7 @@ static function getLinkedTicketsTo ($ID) {
*
* @param $ID ID of the ticket id
*
- * @return nothing display
+ * @return void
**/
static function displayLinkedTicketsTo ($ID) {
global $DB, $CFG_GLPI;
diff --git a/inc/ticketrecurrent.class.php b/inc/ticketrecurrent.class.php
index dc58fc736dd..ba1ee185618 100644
--- a/inc/ticketrecurrent.class.php
+++ b/inc/ticketrecurrent.class.php
@@ -292,7 +292,7 @@ function rawSearchOptions() {
/**
* Show next creation date
*
- * @return nothing only display
+ * @return void
**/
function showInfos() {
diff --git a/inc/tickettemplate.class.php b/inc/tickettemplate.class.php
index 61b5f586699..df03a108ea6 100644
--- a/inc/tickettemplate.class.php
+++ b/inc/tickettemplate.class.php
@@ -125,7 +125,7 @@ static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtem
*
* @param $tt ITILTemplate object
*
- * @return Nothing (call to classes members)
+ * @return void
**/
static function showHelpdeskPreview(ITILTemplate $tt) {
diff --git a/inc/toolbox.class.php b/inc/toolbox.class.php
index 789d1983792..7f001b088eb 100644
--- a/inc/toolbox.class.php
+++ b/inc/toolbox.class.php
@@ -723,26 +723,16 @@ static function setDebugMode($mode = null, $debug_sql = null, $debug_vars = null
// If debug mode activated : display some information
if ($_SESSION['glpi_use_mode'] == Session::DEBUG_MODE) {
- // display_errors only need for for E_ERROR, E_PARSE, ... which cannot be catched
// Recommended development settings
ini_set('display_errors', 'On');
- error_reporting(E_ALL | E_STRICT);
+ error_reporting(E_ALL);
set_error_handler(['Toolbox','userErrorHandlerDebug']);
-
- } else {
+ } else if (!defined('TU_USER')) {
// Recommended production settings
ini_set('display_errors', 'Off');
- if (defined('TU_USER')) {
- //do not set error_reporting to a low level for unit tests
- error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
- }
+ error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
set_error_handler(['Toolbox', 'userErrorHandlerNormal']);
}
-
- if (defined('TU_USER')) {
- //user default error handler from tests
- set_error_handler(null);
- }
}
@@ -3215,4 +3205,10 @@ static function getPictureUrl($path) {
return $CFG_GLPI["root_doc"] . '/front/document.send.php?file=_pictures/' . $path;
}
+
+ public static function throwBadRequest($message) {
+ http_response_code(400);
+ Toolbox::logWarning($message);
+ die(json_encode(["message" => $message]));
+ }
}
diff --git a/inc/transfer.class.php b/inc/transfer.class.php
index c685c52faa2..b51f2382198 100644
--- a/inc/transfer.class.php
+++ b/inc/transfer.class.php
@@ -1043,7 +1043,7 @@ function simulateTransfer($items) {
*
* Transfer item to a new Item if $ID==$newID : only update entities_id field :
* $ID!=$new ID -> copy datas (like template system)
- * @return nothing (diplays)
+ * @return void
**/
function transferItem($itemtype, $ID, $newID) {
global $CFG_GLPI, $DB;
diff --git a/inc/useremail.class.php b/inc/useremail.class.php
index 3ec267051b4..fc5d455fbb8 100644
--- a/inc/useremail.class.php
+++ b/inc/useremail.class.php
@@ -192,7 +192,7 @@ function showChildForItemForm($canedit, $field_name, $id) {
*
* @param $user User object
*
- * @return nothing
+ * @return void
**/
static function showForUser(User $user) {
diff --git a/install/empty_data.php b/install/empty_data.php
index 3d6549c0be5..2cece04d488 100644
--- a/install/empty_data.php
+++ b/install/empty_data.php
@@ -7083,10 +7083,42 @@
'profiles_id' => '8',
'name' => 'cluster',
'rights' => 1,
-
+ ], [
+ 'profiles_id' => '1',
+ 'name' => 'externalevent',
+ 'rights' => 0,
+ ], [
+ 'profiles_id' => '2',
+ 'name' => 'externalevent',
+ 'rights' => 1,
+ ], [
+ 'profiles_id' => '3',
+ 'name' => 'externalevent',
+ 'rights' => 1055,
+ ], [
+ 'profiles_id' => '4',
+ 'name' => 'externalevent',
+ 'rights' => 1055,
+ ], [
+ 'profiles_id' => '5',
+ 'name' => 'externalevent',
+ 'rights' => 0,
+ ], [
+ 'profiles_id' => '6',
+ 'name' => 'externalevent',
+ 'rights' => 1,
+ ], [
+ 'profiles_id' => '7',
+ 'name' => 'externalevent',
+ 'rights' => 31,
+ ], [
+ 'profiles_id' => '8',
+ 'name' => 'externalevent',
+ 'rights' => 1,
],
];
+
$tables['glpi_profiles'] = [
[
'id' => '1',
diff --git a/install/mysql/glpi-empty.sql b/install/mysql/glpi-empty.sql
index 24ac00b003b..598bc753787 100644
--- a/install/mysql/glpi-empty.sql
+++ b/install/mysql/glpi-empty.sql
@@ -1178,6 +1178,66 @@ CREATE TABLE `glpi_configs` (
UNIQUE KEY `unicity` (`context`,`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+### Dump table glpi_impacts
+
+DROP TABLE IF EXISTS `glpi_impactrelations`;
+CREATE TABLE `glpi_impactrelations` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `itemtype_source` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ `items_id_source` INT(11) NOT NULL DEFAULT '0',
+ `itemtype_impacted` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ `items_id_impacted` INT(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `unicity` (
+ `itemtype_source`,
+ `items_id_source`,
+ `itemtype_impacted`,
+ `items_id_impacted`
+ ),
+ KEY `source_asset` (`itemtype_source`, `items_id_source`),
+ KEY `impacted_asset` (`itemtype_impacted`, `items_id_impacted`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
+
+### Dump table glpi_impacts_compounds
+
+DROP TABLE IF EXISTS `glpi_impactcompounds`;
+CREATE TABLE `glpi_impactcompounds` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `name` VARCHAR(255) NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ `color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
+
+### Dump table glpi_impacts_parent
+
+DROP TABLE IF EXISTS `glpi_impactitems`;
+CREATE TABLE `glpi_impactitems` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `itemtype` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ `items_id` INT(11) NOT NULL DEFAULT '0',
+ `parent_id` INT(11) NOT NULL DEFAULT '0',
+ `zoom` FLOAT NOT NULL DEFAULT '0',
+ `pan_x` FLOAT NOT NULL DEFAULT '0',
+ `pan_y` FLOAT NOT NULL DEFAULT '0',
+ `impact_color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ `depends_color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ `impact_and_depends_color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ `position_x` FLOAT NOT NULL DEFAULT '0',
+ `position_y` FLOAT NOT NULL DEFAULT '0',
+ `show_depends` TINYINT NOT NULL DEFAULT '1',
+ `show_impact` TINYINT NOT NULL DEFAULT '1',
+ `max_depth` INT(11) NOT NULL DEFAULT '5',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `unicity` (
+ `itemtype`,
+ `items_id`
+ ),
+ KEY `source` (`itemtype`, `items_id`),
+ KEY `parent_id` (`parent_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
### Dump table glpi_consumableitems
DROP TABLE IF EXISTS `glpi_consumableitems`;
@@ -2273,7 +2333,7 @@ CREATE TABLE `glpi_documents_items` (
`date_mod` timestamp NULL DEFAULT NULL,
`users_id` int(11) DEFAULT '0',
`timeline_position` tinyint(1) NOT NULL DEFAULT '0',
- `date_creation` datetime DEFAULT NULL,
+ `date_creation` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unicity` (`documents_id`,`itemtype`,`items_id`),
KEY `item` (`itemtype`,`items_id`,`entities_id`,`is_recursive`),
@@ -7583,3 +7643,78 @@ CREATE TABLE `glpi_items_clusters` (
UNIQUE KEY `unicity` (`clusters_id`,`itemtype`,`items_id`),
KEY `item` (`itemtype`,`items_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
+### Dump table glpi_planningexternalevents
+
+DROP TABLE IF EXISTS `glpi_planningexternalevents`;
+CREATE TABLE `glpi_planningexternalevents` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `planningexternaleventtemplates_id` int(11) NOT NULL DEFAULT '0',
+ `entities_id` int(11) NOT NULL DEFAULT '0',
+ `date` timestamp NULL DEFAULT NULL,
+ `users_id` int(11) NOT NULL DEFAULT '0',
+ `groups_id` int(11) NOT NULL DEFAULT '0',
+ `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
+ `text` text COLLATE utf8_unicode_ci,
+ `begin` timestamp NULL DEFAULT NULL,
+ `end` timestamp NULL DEFAULT NULL,
+ `rrule` text COLLATE utf8_unicode_ci,
+ `state` int(11) NOT NULL DEFAULT '0',
+ `planningeventcategories_id` int(11) NOT NULL DEFAULT '0',
+ `background` tinyint(1) NOT NULL DEFAULT '0',
+ `date_mod` timestamp NULL DEFAULT NULL,
+ `date_creation` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `planningexternaleventtemplates_id` (`planningexternaleventtemplates_id`),
+ KEY `entities_id` (`entities_id`),
+ KEY `date` (`date`),
+ KEY `begin` (`begin`),
+ KEY `end` (`end`),
+ KEY `users_id` (`users_id`),
+ KEY `groups_id` (`groups_id`),
+ KEY `state` (`state`),
+ KEY `planningeventcategories_id` (`planningeventcategories_id`),
+ KEY `date_mod` (`date_mod`),
+ KEY `date_creation` (`date_creation`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
+### Dump table glpi_planningexternaleventtemplates
+
+DROP TABLE IF EXISTS `glpi_planningexternaleventtemplates`;
+CREATE TABLE `glpi_planningexternaleventtemplates` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `entities_id` int(11) NOT NULL DEFAULT '0',
+ `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
+ `text` text COLLATE utf8_unicode_ci,
+ `comment` text COLLATE utf8_unicode_ci,
+ `duration` int(11) NOT NULL DEFAULT '0',
+ `before_time` int(11) NOT NULL DEFAULT '0',
+ `rrule` text COLLATE utf8_unicode_ci,
+ `state` int(11) NOT NULL DEFAULT '0',
+ `planningeventcategories_id` int(11) NOT NULL DEFAULT '0',
+ `background` tinyint(1) NOT NULL DEFAULT '0',
+ `date_mod` timestamp NULL DEFAULT NULL,
+ `date_creation` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `entities_id` (`entities_id`),
+ KEY `state` (`state`),
+ KEY `planningeventcategories_id` (`planningeventcategories_id`),
+ KEY `date_mod` (`date_mod`),
+ KEY `date_creation` (`date_creation`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
+### Dump table glpi_planningeventcategories
+
+DROP TABLE IF EXISTS `glpi_planningeventcategories`;
+CREATE TABLE `glpi_planningeventcategories` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
+ `color` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
+ `comment` text COLLATE utf8_unicode_ci,
+ `date_mod` timestamp NULL DEFAULT NULL,
+ `date_creation` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `name` (`name`),
+ KEY `date_mod` (`date_mod`),
+ KEY `date_creation` (`date_creation`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
diff --git a/install/update.php b/install/update.php
index 9a854917e90..68ff5b957e8 100644
--- a/install/update.php
+++ b/install/update.php
@@ -128,7 +128,7 @@ function update_importDropdown ($table, $name) {
/**
* Display the form of content update (addslashes compatibility (V0.4))
*
- * @return nothing (displays)
+ * @return void
*/
function showContentUpdateForm() {
$_SESSION['do_content_update'] = true;
diff --git a/install/update_042_05.php b/install/update_042_05.php
index bb49f38b51c..9405966f3e9 100644
--- a/install/update_042_05.php
+++ b/install/update_042_05.php
@@ -1379,7 +1379,7 @@ function dropMaintenanceField() {
* @param $compDpdName string the name of the dropdown foreign key on glpi_computers (eg : hdtype, processor)
* @param $specif string the name of the dropdown value entry on glpi_computer (eg : hdspace, processor_speed) optionnal argument.
*
- * @return nothing if everything is good, else display mysql query and error.
+ * @return void
*/
function compDpd2Device($devtype, $devname, $dpdname, $compDpdName, $specif = '') {
global $DB;
diff --git a/install/update_94_95.php b/install/update_94_95.php
index 9062317aef3..2dd0c6d7d09 100644
--- a/install/update_94_95.php
+++ b/install/update_94_95.php
@@ -365,7 +365,7 @@ function update94to95() {
/** Add "date_creation" field on document_items */
if (!$DB->fieldExists('glpi_documents_items', 'date_creation')) {
- $migration->addField('glpi_documents_items', 'date_creation', 'datetime');
+ $migration->addField('glpi_documents_items', 'date_creation', 'timestamp', ['null' => true]);
$migration->addPostQuery(
$DB->buildUpdate(
'glpi_documents_items',
@@ -455,6 +455,104 @@ function update94to95() {
$migration->addKey('glpi_tickettemplatepredefinedfields', 'tickettemplates_id');
/** /ITiL templates */
+ /** /Add Externals events for planning */
+ if (!$DB->tableExists('glpi_planningexternalevents')) {
+ $query = "CREATE TABLE `glpi_planningexternalevents` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `planningexternaleventtemplates_id` int(11) NOT NULL DEFAULT '0',
+ `entities_id` int(11) NOT NULL DEFAULT '0',
+ `date` timestamp NULL DEFAULT NULL,
+ `users_id` int(11) NOT NULL DEFAULT '0',
+ `groups_id` int(11) NOT NULL DEFAULT '0',
+ `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
+ `text` text COLLATE utf8_unicode_ci,
+ `begin` timestamp NULL DEFAULT NULL,
+ `end` timestamp NULL DEFAULT NULL,
+ `rrule` text COLLATE utf8_unicode_ci,
+ `state` int(11) NOT NULL DEFAULT '0',
+ `planningeventcategories_id` int(11) NOT NULL DEFAULT '0',
+ `background` tinyint(1) NOT NULL DEFAULT '0',
+ `date_mod` timestamp NULL DEFAULT NULL,
+ `date_creation` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `planningexternaleventtemplates_id` (`planningexternaleventtemplates_id`),
+ KEY `entities_id` (`entities_id`),
+ KEY `date` (`date`),
+ KEY `begin` (`begin`),
+ KEY `end` (`end`),
+ KEY `users_id` (`users_id`),
+ KEY `groups_id` (`groups_id`),
+ KEY `state` (`state`),
+ KEY `planningeventcategories_id` (`planningeventcategories_id`),
+ KEY `date_mod` (`date_mod`),
+ KEY `date_creation` (`date_creation`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci";
+ $DB->queryOrDie($query, "add table glpi_planningexternalevents");
+
+ $new_rights = ALLSTANDARDRIGHT + PlanningExternalEvent::MANAGE_BG_EVENTS;
+ $migration->addRight('externalevent', $new_rights, [
+ 'planning' => Planning::READMY
+ ]);
+ }
+
+ // partial update (for developers)
+ if (!$DB->fieldExists('glpi_planningexternalevents', 'planningexternaleventtemplates_id')) {
+ $migration->addField('glpi_planningexternalevents', 'planningexternaleventtemplates_id', 'int', [
+ 'after' => 'id'
+ ]);
+ $migration->addKey('glpi_planningexternalevents', 'planningexternaleventtemplates_id');
+ }
+
+ if (!$DB->tableExists('glpi_planningeventcategories')) {
+ $query = "CREATE TABLE `glpi_planningeventcategories` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
+ `color` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
+ `comment` text COLLATE utf8_unicode_ci,
+ `date_mod` timestamp NULL DEFAULT NULL,
+ `date_creation` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `name` (`name`),
+ KEY `date_mod` (`date_mod`),
+ KEY `date_creation` (`date_creation`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;";
+ $DB->queryOrDie($query, "add table glpi_planningeventcategories");
+ }
+
+ // partial update (for developers)
+ if (!$DB->fieldExists('glpi_planningeventcategories', 'color')) {
+ $migration->addField("glpi_planningeventcategories", "color", "string", [
+ 'after' => "name"
+ ]
+ );
+ }
+
+ if (!$DB->tableExists('glpi_planningexternaleventtemplates')) {
+ $query = "CREATE TABLE `glpi_planningexternaleventtemplates` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `entities_id` int(11) NOT NULL DEFAULT '0',
+ `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
+ `text` text COLLATE utf8_unicode_ci,
+ `comment` text COLLATE utf8_unicode_ci,
+ `duration` int(11) NOT NULL DEFAULT '0',
+ `before_time` int(11) NOT NULL DEFAULT '0',
+ `rrule` text COLLATE utf8_unicode_ci,
+ `state` int(11) NOT NULL DEFAULT '0',
+ `planningeventcategories_id` int(11) NOT NULL DEFAULT '0',
+ `background` tinyint(1) NOT NULL DEFAULT '0',
+ `date_mod` timestamp NULL DEFAULT NULL,
+ `date_creation` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `entities_id` (`entities_id`),
+ KEY `state` (`state`),
+ KEY `planningeventcategories_id` (`planningeventcategories_id`),
+ KEY `date_mod` (`date_mod`),
+ KEY `date_creation` (`date_creation`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci";
+ $DB->queryOrDie($query, "add table glpi_planningexternaleventtemplates");
+ }
+ /** /Add Externals events for planning */
+
if (!$DB->fieldExists('glpi_entities', 'autopurge_delay')) {
$migration->addField("glpi_entities", "autopurge_delay", "integer", [
'after' => "autoclose_delay",
@@ -604,6 +702,72 @@ function update94to95() {
}
/** /Add source item id to TicketTask. Used by tasks created by merging tickets */
+ /** Impact analysis */
+ // Impact config
+ $migration->addConfig(['impact_assets_list' => '[]']);
+
+ // Impact dependencies
+ if (!$DB->tableExists('glpi_impactrelations')) {
+ $query = "CREATE TABLE `glpi_impactrelations` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `itemtype_source` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ `items_id_source` INT(11) NOT NULL DEFAULT '0',
+ `itemtype_impacted` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ `items_id_impacted` INT(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `unicity` (
+ `itemtype_source`,
+ `items_id_source`,
+ `itemtype_impacted`,
+ `items_id_impacted`
+ ),
+ KEY `source_asset` (`itemtype_source`, `items_id_source`),
+ KEY `impacted_asset` (`itemtype_impacted`, `items_id_impacted`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci";
+ $DB->queryOrDie($query, "add table glpi_impacts");
+ }
+
+ // Impact compounds
+ if (!$DB->tableExists('glpi_impactcompounds')) {
+ $query = "CREATE TABLE `glpi_impactcompounds` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `name` VARCHAR(255) NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ `color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ PRIMARY KEY (`id`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci";
+ $DB->queryOrDie($query, "add table glpi_impacts_compounds");
+ }
+
+ // Impact parents
+ if (!$DB->tableExists('glpi_impactitems')) {
+ $query = "CREATE TABLE `glpi_impactitems` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `itemtype` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ `items_id` INT(11) NOT NULL DEFAULT '0',
+ `parent_id` INT(11) NOT NULL DEFAULT '0',
+ `zoom` FLOAT NOT NULL DEFAULT '0',
+ `pan_x` FLOAT NOT NULL DEFAULT '0',
+ `pan_y` FLOAT NOT NULL DEFAULT '0',
+ `impact_color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ `depends_color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ `impact_and_depends_color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci',
+ `position_x` FLOAT NOT NULL DEFAULT '0',
+ `position_y` FLOAT NOT NULL DEFAULT '0',
+ `show_depends` TINYINT NOT NULL DEFAULT '1',
+ `show_impact` TINYINT NOT NULL DEFAULT '1',
+ `max_depth` INT(11) NOT NULL DEFAULT '5',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `unicity` (
+ `itemtype`,
+ `items_id`
+ ),
+ KEY `source` (`itemtype`, `items_id`),
+ KEY `parent_id` (`parent_id`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci";
+ $DB->queryOrDie($query, "add table glpi_impacts_parent");
+ }
+ /** /Impact analysis */
+
// ************ Keep it at the end **************
foreach ($ADDTODISPLAYPREF as $type => $tab) {
$rank = 1;
diff --git a/js/impact.js b/js/impact.js
new file mode 100644
index 00000000000..cf0f7e9f48d
--- /dev/null
+++ b/js/impact.js
@@ -0,0 +1,2330 @@
+/**
+ * ---------------------------------------------------------------------
+ * GLPI - Gestionnaire Libre de Parc Informatique
+ * Copyright (C) 2015-2019 Teclib' and contributors.
+ *
+ * http://glpi-project.org
+ *
+ * based on GLPI - Gestionnaire Libre de Parc Informatique
+ * Copyright (C) 2003-2014 by the INDEPNET Development Team.
+ *
+ * ---------------------------------------------------------------------
+ *
+ * LICENSE
+ *
+ * This file is part of GLPI.
+ *
+ * GLPI is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * GLPI is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with GLPI. If not, see .
+ * ---------------------------------------------------------------------
+ */
+
+
+// Load cytoscape
+var cytoscape = window.cytoscape;
+
+// Needed for JS lint validation
+/* global _ */
+/* global showMenu */
+
+var GLPIImpact = {
+
+ // Constants to represent nodes and edges
+ NODE: 1,
+ EDGE: 2,
+
+ // Constants for graph direction (bitmask)
+ DEFAULT : 0, // 0b00
+ FORWARD : 1, // 0b01
+ BACKWARD: 2, // 0b10
+ BOTH : 3, // 0b11
+
+ // Constants for graph edition mode
+ EDITION_DEFAULT : 1,
+ EDITION_ADD_NODE : 2,
+ EDITION_ADD_EDGE : 3,
+ EDITION_DELETE : 4,
+ EDITION_ADD_COMPOUND: 5,
+
+ // Constants for ID separator
+ NODE_ID_SEPERATOR: "::",
+ EDGE_ID_SEPERATOR: "->",
+
+ // Constants for delta action
+ DELTA_ACTION_ADD : 1,
+ DELTA_ACTION_UPDATE: 2,
+ DELTA_ACTION_DELETE: 3,
+
+ // Store the initial state of the graph
+ initialState: null,
+
+ // Store translated labels
+ locales: {},
+
+ // Store the visibility settings of the different direction of the graph
+ directionVisibility: {},
+
+ // Store color for egdes
+ edgeColors: {},
+
+ // Cytoscape instance
+ cy: null,
+
+ // The impact network container
+ impactContainer: null,
+
+ // The graph edition mode
+ editionMode: null,
+
+ // Start node of the graph
+ startNode: null,
+
+ // Form
+ form: null,
+
+ // Maximum depth of the graph
+ maxDepth: 5,
+
+ // Store registered dialogs and their inputs
+ dialogs: {
+ addNode: {
+ id: null,
+ inputs: {
+ itemType: null,
+ itemID : null
+ }
+ },
+ configColor: {
+ id: null,
+ inputs: {
+ dependsColor : null,
+ impactColor : null,
+ impactAndDependsColor: null
+ }
+ },
+ ongoingDialog: {
+ id: null
+ },
+ editCompoundDialog: {
+ id: null,
+ inputs: {
+ name : null,
+ color: null
+ }
+ }
+ },
+
+ // Store registered toolbar items
+ toolbar: {
+ helpText : null,
+ tools : null,
+ save : null,
+ addNode : null,
+ addEdge : null,
+ addCompound : null,
+ deleteElement: null,
+ export : null,
+ expandToolbar: null,
+ toggleImpact : null,
+ toggleDepends: null,
+ colorPicker : null,
+ maxDepth : null,
+ maxDepthView : null,
+ },
+
+ // Data that needs to be stored/shared between events
+ eventData: {
+ addEdgeStart : null, // Store starting node of a new edge
+ tmpEles : null, // Temporary collection used when adding an edge
+ lastClick : null, // Store last click timestamp
+ boxSelected : [],
+ grabNodeStart: null,
+ boundingBox : null
+ },
+
+ /**
+ * Get network style
+ *
+ * @returns {Array}
+ */
+ getNetworkStyle: function() {
+ return [
+ {
+ selector: 'core',
+ style: {
+ 'selection-box-opacity' : '0.2',
+ 'selection-box-border-width': '0',
+ 'selection-box-color' : '#24acdf'
+ }
+ },
+ {
+ selector: 'node:parent',
+ style: {
+ 'padding' : '30px',
+ 'shape' : 'roundrectangle',
+ 'border-width' : '1px',
+ 'background-opacity': '0.5',
+ 'font-size' : '1.1em',
+ 'background-color' : '#d2d2d2',
+ 'text-margin-y' : '20px',
+ 'text-opacity' : 0.7,
+ }
+ },
+ {
+ selector: 'node:parent[label]',
+ style: {
+ 'label': 'data(label)',
+ }
+ },
+ {
+ selector: 'node:parent[color]',
+ style: {
+ 'border-color' : 'data(color)',
+ 'background-color' : 'data(color)',
+ }
+ },
+ {
+ selector: ':selected',
+ style: {
+ 'overlay-opacity': 0.2,
+ }
+ },
+ {
+ selector: '[todelete=1]:selected',
+ style: {
+ 'overlay-opacity': 0.2,
+ 'overlay-color': 'red',
+ }
+ },
+ {
+ selector: 'node[image]',
+ style: {
+ 'label' : 'data(label)',
+ 'shape' : 'rectangle',
+ 'background-color' : '#666',
+ 'background-image' : 'data(image)',
+ 'background-fit' : 'contain',
+ 'background-opacity': '0',
+ 'font-size' : '1em',
+ 'text-opacity' : 0.7,
+ }
+ },
+ {
+ selector: '[hidden=1], [depth > ' + GLPIImpact.maxDepth + ']',
+ style: {
+ 'opacity': '0',
+ }
+ },
+ {
+ selector: '[id="tmp_node"]',
+ style: {
+ 'opacity': '0',
+ }
+ },
+ {
+ selector: 'edge',
+ style: {
+ 'width' : 1,
+ 'line-color' : this.edgeColors[0],
+ 'target-arrow-color': this.edgeColors[0],
+ 'target-arrow-shape': 'triangle',
+ 'arrow-scale' : 0.7,
+ 'curve-style' : 'bezier'
+ }
+ },
+ {
+ selector: '[flag=' + GLPIImpact.FORWARD + ']',
+ style: {
+ 'line-color' : this.edgeColors[GLPIImpact.FORWARD],
+ 'target-arrow-color': this.edgeColors[GLPIImpact.FORWARD],
+ }
+ },
+ {
+ selector: '[flag=' + GLPIImpact.BACKWARD + ']',
+ style: {
+ 'line-color' : this.edgeColors[GLPIImpact.BACKWARD],
+ 'target-arrow-color': this.edgeColors[GLPIImpact.BACKWARD],
+ }
+ },
+ {
+ selector: '[flag=' + GLPIImpact.BOTH + ']',
+ style: {
+ 'line-color' : this.edgeColors[GLPIImpact.BOTH],
+ 'target-arrow-color': this.edgeColors[GLPIImpact.BOTH],
+ }
+ }
+ ];
+ },
+
+ /**
+ * Get network layout
+ *
+ * @returns {Object}
+ */
+ getPresetLayout: function () {
+ return {
+ name: 'preset',
+ positions: function(node) {
+ return {
+ x: parseFloat(node.data('position_x')),
+ y: parseFloat(node.data('position_y')),
+ };
+ }
+ };
+ },
+
+ /**
+ * Get network layout
+ *
+ * @returns {Object}
+ */
+ getDagreLayout: function () {
+ return {
+ name: 'dagre',
+ rankDir: 'LR',
+ fit: false
+ };
+ },
+
+ /**
+ * Get the current state of the graph
+ *
+ * @returns {Object}
+ */
+ getCurrentState: function() {
+ var data = {edges: {}, compounds: {}, items: {}};
+
+ // Load edges
+ GLPIImpact.cy.edges().forEach(function(edge) {
+ data.edges[edge.data('id')] = {
+ source: edge.data('source'),
+ target: edge.data('target'),
+ };
+ });
+
+ // Load compounds
+ GLPIImpact.cy.filter("node:parent").forEach(function(compound) {
+ data.compounds[compound.data('id')] = {
+ name: compound.data('label'),
+ color: compound.data('color'),
+ };
+ });
+
+ // Load items
+ GLPIImpact.cy.filter("node:childless").forEach(function(node) {
+ data.items[node.data('id')] = {
+ impactitem_id: node.data('impactitem_id'),
+ parent : node.data('parent'),
+ position : node.position()
+ };
+ });
+
+ return data;
+ },
+
+ /**
+ * Delta computation for edges
+ *
+ * @returns {Object}
+ */
+ computeEdgeDelta: function(currentEdges) {
+ var edgesDelta = {};
+
+ // First iterate on the edges we had in the initial state
+ Object.keys(GLPIImpact.initialState.edges).forEach(function(edgeID) {
+ var edge = GLPIImpact.initialState.edges[edgeID];
+ if (Object.prototype.hasOwnProperty.call(currentEdges, edgeID)) {
+ // If the edge is still here in the current state, nothing happened
+ // Remove it from the currentEdges data so we can skip it later
+ delete currentEdges[edgeID];
+ } else {
+ // If the edge is missing in the current state, it has been deleted
+ var source = edge.source.split(GLPIImpact.NODE_ID_SEPERATOR);
+ var target = edge.target.split(GLPIImpact.NODE_ID_SEPERATOR);
+ edgesDelta[edgeID] = {
+ action : GLPIImpact.DELTA_ACTION_DELETE,
+ itemtype_source : source[0],
+ items_id_source : source[1],
+ itemtype_impacted: target[0],
+ items_id_impacted: target[1]
+ };
+ }
+ });
+
+ // Now iterate on the edges we have in the current state
+ // Since we removed the edges that were not modified in the previous step,
+ // the remaining edges can only be new ones
+ Object.keys(currentEdges).forEach(function (edgeID) {
+ var edge = currentEdges[edgeID];
+ var source = edge.source.split(GLPIImpact.NODE_ID_SEPERATOR);
+ var target = edge.target.split(GLPIImpact.NODE_ID_SEPERATOR);
+ edgesDelta[edgeID] = {
+ action : GLPIImpact.DELTA_ACTION_ADD,
+ itemtype_source : source[0],
+ items_id_source : source[1],
+ itemtype_impacted: target[0],
+ items_id_impacted: target[1]
+ };
+ });
+
+ return edgesDelta;
+ },
+
+ /**
+ * Delta computation for compounds
+ *
+ * @returns {Object}
+ */
+ computeCompoundsDelta: function(currentCompounds) {
+ var compoundsDelta = {};
+
+ // First iterate on the compounds we had in the initial state
+ Object.keys(GLPIImpact.initialState.compounds).forEach(function(compoundID) {
+ var compound = GLPIImpact.initialState.compounds[compoundID];
+ if (Object.prototype.hasOwnProperty.call(currentCompounds, compoundID)) {
+ // If the compound is still here in the current state
+ var currentCompound = currentCompounds[compoundID];
+
+ // Check for updates ...
+ if (compound.name != currentCompound.name
+ || compound.color != currentCompound.color) {
+ compoundsDelta[compoundID] = {
+ action: GLPIImpact.DELTA_ACTION_UPDATE,
+ name : currentCompound.name,
+ color : currentCompound.color
+ };
+ }
+
+ // Remove it from the currentCompounds data
+ delete currentCompounds[compoundID];
+ } else {
+ // If the compound is missing in the current state, it's been deleted
+ compoundsDelta[compoundID] = {
+ action : GLPIImpact.DELTA_ACTION_DELETE,
+ };
+ }
+ });
+
+ // Now iterate on the compounds we have in the current state
+ Object.keys(currentCompounds).forEach(function (compoundID) {
+ compoundsDelta[compoundID] = {
+ action: GLPIImpact.DELTA_ACTION_ADD,
+ name : currentCompounds[compoundID].name,
+ color : currentCompounds[compoundID].color
+ };
+ });
+
+ return compoundsDelta;
+ },
+
+ /**
+ * Delta computation for parents
+ *
+ * @returns {Object}
+ */
+ computeItemsDelta: function(currentNodes) {
+ var itemsDelta = {};
+
+ // Now iterate on the parents we have in the current state
+ Object.keys(currentNodes).forEach(function (nodeID) {
+ var node = currentNodes[nodeID];
+ itemsDelta[node.impactitem_id] = {
+ action : GLPIImpact.DELTA_ACTION_UPDATE,
+ parent_id: node.parent,
+ };
+
+ // Set parent to 0 if null
+ if (node.parent == undefined) {
+ node.parent = 0;
+ }
+
+ if (nodeID == GLPIImpact.startNode) {
+ // Starting node of the graph, save viewport and edge colors
+ itemsDelta[node.impactitem_id] = {
+ action : GLPIImpact.DELTA_ACTION_UPDATE,
+ parent_id : node.parent,
+ position_x : node.position.x,
+ position_y : node.position.y,
+ zoom : GLPIImpact.cy.zoom(),
+ pan_x : GLPIImpact.cy.pan().x,
+ pan_y : GLPIImpact.cy.pan().y,
+ impact_color : GLPIImpact.edgeColors[GLPIImpact.FORWARD],
+ depends_color : GLPIImpact.edgeColors[GLPIImpact.BACKWARD],
+ impact_and_depends_color: GLPIImpact.edgeColors[GLPIImpact.BOTH],
+ show_depends : GLPIImpact.directionVisibility[GLPIImpact.BACKWARD],
+ show_impact : GLPIImpact.directionVisibility[GLPIImpact.FORWARD],
+ max_depth : GLPIImpact.maxDepth,
+ };
+ } else {
+ // Others nodes of the graph, store only their parents and position
+ itemsDelta[node.impactitem_id] = {
+ action : GLPIImpact.DELTA_ACTION_UPDATE,
+ parent_id : node.parent,
+ position_x: node.position.x,
+ position_y: node.position.y,
+ };
+ }
+
+ });
+
+ return itemsDelta;
+ },
+
+ /**
+ * Compute the delta betwteen the initial state and the current state
+ *
+ * @returns {Object}
+ */
+ computeDelta: function () {
+ // Store the delta for edges, compounds and parent
+ var result = {};
+
+ // Get the current state of the graph
+ var currentState = this.getCurrentState();
+
+ // Compute each deltas
+ result.edges = this.computeEdgeDelta(currentState.edges);
+ result.compounds = this.computeCompoundsDelta(currentState.compounds);
+ result.items = this.computeItemsDelta(currentState.items);
+
+ return result;
+ },
+
+ /**
+ * Get the context menu items
+ *
+ * @returns {Array}
+ */
+ getContextMenuItems: function(){
+ return [
+ {
+ id : 'goTo',
+ content : '' + this.getLocale("goTo"),
+ tooltipText : this.getLocale("goTo+"),
+ selector : 'node',
+ onClickFunction: this.menuOnGoTo
+ },
+ {
+ id : 'showOngoing',
+ content : '' + this.getLocale("showOngoing"),
+ tooltipText : this.getLocale("showOngoing+"),
+ selector : 'node[hasITILObjects=1]',
+ onClickFunction: this.menuOnShowOngoing
+ },
+ {
+ id : 'editCompound',
+ content : '' + this.getLocale("compoundProperties"),
+ tooltipText : this.getLocale("compoundProperties+"),
+ selector : 'node:parent',
+ onClickFunction: this.menuOnEditCompound
+ },
+ {
+ id : 'removeFromCompound',
+ content : '' + this.getLocale("removeFromCompound"),
+ tooltipText : this.getLocale("removeFromCompound+"),
+ selector : 'node:child',
+ onClickFunction: this.menuOnRemoveFromCompound
+ },
+ {
+ id : 'delete',
+ content : '' + this.getLocale("delete"),
+ tooltipText : this.getLocale("delete+"),
+ selector : 'node, edge',
+ onClickFunction: this.menuOnDelete
+ },
+ {
+ id : 'new',
+ content : '' + this.getLocale("new"),
+ tooltipText : this.getLocale("new+"),
+ coreAsWell : true,
+ onClickFunction: this.menuOnNew
+ }
+ ];
+ },
+
+ /**
+ * Build the add node dialog
+ *
+ * @param {string} itemID
+ * @param {string} itemType
+ * @param {Object} position x, y
+ *
+ * @returns {Object}
+ */
+ getAddNodeDialog: function(itemID, itemType, position) {
+ // Build a new graph from the selected node and insert it
+ var buttonAdd = {
+ text: GLPIImpact.getLocale("add"),
+ click: function() {
+ var node = {
+ itemtype: $(itemID).val(),
+ items_id: $(itemType).val(),
+ };
+ var nodeID = GLPIImpact.makeID(GLPIImpact.NODE, node.itemtype, node.items_id);
+
+ // Check if the node is already on the graph
+ if (GLPIImpact.cy.filter('node[id="' + nodeID + '"]').length > 0) {
+ alert(GLPIImpact.getLocale("duplicateAsset"));
+ return;
+ }
+
+ // Build the new subgraph
+ $.when(GLPIImpact.buildGraphFromNode(node)).then(
+ function (graph) {
+ // Insert the new graph data into the current graph
+ GLPIImpact.insertGraph(graph, {
+ id: nodeID,
+ x: position.x,
+ y: position.y
+ });
+ GLPIImpact.updateFlags();
+ $(GLPIImpact.dialogs.addNode.id).dialog("close");
+ GLPIImpact.setEditionMode(GLPIImpact.EDITION_DEFAULT);
+ },
+ function () {
+ // Ajax failed
+ alert(GLPIImpact.getLocale("unexpectedError"));
+ }
+ );
+ }
+ };
+
+ // Exit edit mode
+ var buttonCancel = {
+ text: GLPIImpact.getLocale("cancel"),
+ click: function() {
+ $(this).dialog("close");
+ GLPIImpact.setEditionMode(GLPIImpact.EDITION_DEFAULT);
+ }
+ };
+
+ return {
+ title: this.getLocale("newAsset"),
+ modal: true,
+ position: {
+ my: 'center',
+ at: 'center',
+ of: GLPIImpact.impactContainer
+ },
+ buttons: [buttonAdd, buttonCancel]
+ };
+ },
+
+ /**
+ * Build the color picker dialog
+ *
+ * @param {JQuery} backward
+ * @param {JQuery} forward
+ * @param {JQuery} both
+ *
+ * @returns {Object}
+ */
+ getColorPickerDialog: function(backward, forward, both) {
+ // Update color fields to match saved values
+ $(GLPIImpact.dialogs.configColor.inputs.dependsColor).spectrum(
+ "set",
+ GLPIImpact.edgeColors[GLPIImpact.BACKWARD]
+ );
+ $(GLPIImpact.dialogs.configColor.inputs.impactColor).spectrum(
+ "set",
+ GLPIImpact.edgeColors[GLPIImpact.FORWARD]
+ );
+ $(GLPIImpact.dialogs.configColor.inputs.impactAndDependsColor).spectrum(
+ "set",
+ GLPIImpact.edgeColors[GLPIImpact.BOTH]
+ );
+
+ var buttonUpdate = {
+ text: "Update",
+ click: function() {
+ GLPIImpact.setEdgeColors({
+ backward: backward.val(),
+ forward : forward.val(),
+ both : both.val(),
+ });
+ GLPIImpact.updateStyle();
+ $(this).dialog( "close" );
+ GLPIImpact.cy.trigger("change");
+ }
+ };
+
+ return {
+ modal: true,
+ width: 'auto',
+ position: {
+ my: 'center',
+ at: 'center',
+ of: GLPIImpact.impactContainer
+ },
+ draggable: false,
+ title: this.getLocale("colorConfiguration"),
+ buttons: [buttonUpdate]
+ };
+ },
+
+ /**
+ * Build the add node dialog
+ *
+ * @returns {Object}
+ */
+ getOngoingDialog: function() {
+ return {
+ title: GLPIImpact.getLocale("ongoingTickets"),
+ modal: true,
+ position: {
+ my: 'center',
+ at: 'center',
+ of: GLPIImpact.impactContainer
+ },
+ buttons: []
+ };
+ },
+
+ /**
+ * Build the add node dialog
+ *
+ * @param {string} itemID
+ * @param {string} itemType
+ * @param {Object} position x, y
+ *
+ * @returns {Object}
+ */
+ getEditCompoundDialog: function(compound) {
+ // Reset inputs:
+ $(GLPIImpact.dialogs.editCompoundDialog.inputs.name).val(
+ compound.data('label')
+ );
+ $(GLPIImpact.dialogs.editCompoundDialog.inputs.color).spectrum(
+ "set",
+ compound.data('color')
+ );
+
+ // Save group details
+ var buttonSave = {
+ text: GLPIImpact.getLocale("save"),
+ click: function() {
+ // Save compound name
+ compound.data(
+ 'label',
+ $(GLPIImpact.dialogs.editCompoundDialog.inputs.name).val()
+ );
+
+ // Save compound color
+ compound.data(
+ 'color',
+ $(GLPIImpact.dialogs.editCompoundDialog.inputs.color).val()
+ );
+
+ // Close dialog
+ $(this).dialog("close");
+ GLPIImpact.cy.trigger("change");
+ }
+ };
+
+ return {
+ title: GLPIImpact.getLocale("editGroup"),
+ modal: true,
+ position: {
+ my: 'center',
+ at: 'center',
+ of: GLPIImpact.impactContainer
+ },
+ buttons: [buttonSave]
+ };
+ },
+
+ /**
+ * Register the dialogs generated by the backend server
+ *
+ * @param {string} key
+ * @param {string} id
+ * @param {Object} inputs
+ */
+ registerDialog: function(key, id, inputs) {
+ GLPIImpact.dialogs[key]['id'] = id;
+ if (inputs) {
+ Object.keys(inputs).forEach(function (inputKey){
+ GLPIImpact.dialogs[key]['inputs'][inputKey] = inputs[inputKey];
+ });
+ }
+ },
+
+ /**
+ * Register the toolbar elements generated by the backend server
+ *
+ * @param {string} key
+ * @param {string} id
+ */
+ registerToobar: function(key, id) {
+ GLPIImpact.toolbar[key] = id;
+ },
+
+ /**
+ * Create a tooltip for a toolbar's item
+ *
+ * @param {string} content
+ *
+ * @returns {Object}
+ */
+ getTooltip: function(content) {
+ return {
+ position: {
+ my: 'bottom center',
+ at: 'top center'
+ },
+ content: this.getLocale(content),
+ style: {
+ classes: 'qtip-shadow qtip-bootstrap'
+ },
+ show: {
+ solo: true,
+ delay: 100
+ },
+ hide: {
+ fixed: true,
+ delay: 100
+ }
+ };
+ },
+
+ /**
+ * Initialise variables
+ *
+ * @param {JQuery} impactContainer
+ * @param {string} locales json
+ * @param {Object} colors properties: default, forward, backward, both
+ * @param {string} startNode
+ * @param {string} dialogs json
+ * @param {string} toolbar json
+ */
+ prepareNetwork: function(
+ impactContainer,
+ locales,
+ colors,
+ startNode,
+ form,
+ dialogs,
+ toolbar
+ ) {
+
+ // Set container
+ this.impactContainer = impactContainer;
+
+ // Set locales from json
+ this.locales = JSON.parse(locales);
+
+ // Init directionVisibility
+ this.directionVisibility[GLPIImpact.FORWARD] = true;
+ this.directionVisibility[GLPIImpact.BACKWARD] = true;
+
+ // Set colors for edges
+ this.setEdgeColors(colors);
+
+ // Set start node
+ this.startNode = startNode;
+
+ // Register form
+ this.form = form;
+
+ // Register dialogs
+ JSON.parse(dialogs).forEach(function(dialog) {
+ GLPIImpact.registerDialog(dialog.key, dialog.id, dialog.inputs);
+ });
+
+ // Register toolbars
+ JSON.parse(toolbar).forEach(function(element) {
+ GLPIImpact.registerToobar(element.key, element.id);
+ });
+ this.initToolbar();
+ },
+
+ /**
+ * Build the network graph
+ *
+ * @param {string} data (json)
+ */
+ buildNetwork: function(data, params) {
+ // Init workspace status
+ GLPIImpact.showDefaultWorkspaceStatus();
+
+ // Apply custom colors if defined
+ if (params.impact_color != '') {
+ this.setEdgeColors({
+ forward : params.impact_color,
+ backward: params.depends_color,
+ both : params.impact_and_depends_color,
+ });
+ }
+
+ // Preset layout
+ var layout = this.getPresetLayout();
+
+ // Init cytoscape
+ this.cy = cytoscape({
+ container: this.impactContainer,
+ elements : data,
+ style : this.getNetworkStyle(),
+ layout : layout,
+ wheelSensitivity: 0.25,
+ });
+
+ // Store initial data
+ this.initialState = this.getCurrentState();
+
+ // Enable context menu
+ this.cy.contextMenus({
+ menuItems: this.getContextMenuItems(),
+ menuItemClasses: [],
+ contextMenuClasses: []
+ });
+
+ // Enable grid
+ this.cy.gridGuide({
+ gridStackOrder: 0,
+ snapToGridOnRelease: true,
+ snapToGridDuringDrag: true,
+ gridSpacing: 12,
+ drawGrid: true,
+ panGrid: true,
+ });
+
+ // Apply saved visibility
+ if (!parseInt(params.show_depends)) {
+ GLPIImpact.toggleVisibility(GLPIImpact.BACKWARD);
+ }
+ if (!parseInt(params.show_impact)) {
+ GLPIImpact.toggleVisibility(GLPIImpact.FORWARD);
+ }
+
+ // Apply max depth
+ this.maxDepth = params.max_depth;
+ this.updateFlags();
+
+ // Set viewport
+ if (params.zoom != '0') {
+ // If viewport params are set, apply them
+ this.cy.viewport({
+ zoom: parseFloat(params.zoom),
+ pan: {
+ x: parseFloat(params.pan_x),
+ y: parseFloat(params.pan_y),
+ }
+ });
+
+ // Check viewport is not empty or contains only one item
+ var viewport = GLPIImpact.cy.extent();
+ var empty = true;
+ GLPIImpact.cy.nodes().forEach(function(node) {
+ if (node.position().x > viewport.x1
+ && node.position().x < viewport.x2
+ && node.position().y > viewport.x1
+ && node.position().y < viewport.x2
+ ){
+ empty = false;
+ }
+ });
+
+ if (empty || GLPIImpact.cy.filter("node:childless").length == 1) {
+ this.cy.fit();
+
+ if (this.cy.zoom() > 2.3) {
+ this.cy.zoom(2.3);
+ this.cy.center();
+ }
+ }
+ } else {
+ // Else fit the graph and reduce zoom if needed
+ this.cy.fit();
+
+ if (this.cy.zoom() > 2.3) {
+ this.cy.zoom(2.3);
+ this.cy.center();
+ }
+ }
+
+ // Register events handlers for cytoscape object
+ this.cy.on('mousedown', 'node', this.nodeOnMousedown);
+ this.cy.on('mouseup', 'node', this.nodeOnMouseup);
+ this.cy.on('mousemove', this.onMousemove);
+ this.cy.on('mouseover', this.onMouseover);
+ this.cy.on('mouseout', this.onMouseout);
+ this.cy.on('click', this.onClick);
+ this.cy.on('click', 'edge', this.edgeOnClick);
+ this.cy.on('click', 'node', this.nodeOnClick);
+ this.cy.on('box', this.onBox);
+ this.cy.on('drag add remove pan zoom change', this.onChange);
+ this.cy.on('doubleClick', this.onDoubleClick);
+
+ // Global events
+ $(document).keydown(this.onKeyDown);
+
+ // Enter EDITION_DEFAULT mode
+ this.setEditionMode(GLPIImpact.EDITION_DEFAULT);
+
+ // Init depth value
+ var text = GLPIImpact.maxDepth;
+ if (GLPIImpact.maxDepth >= 10) {
+ text = "infinity";
+ }
+ $(GLPIImpact.toolbar.maxDepthView).html("Max depth: " + text);
+ $(GLPIImpact.toolbar.maxDepth).val(GLPIImpact.maxDepth);
+ },
+
+ /**
+ * Create ID for nodes and egdes
+ *
+ * @param {number} type (NODE or EDGE)
+ * @param {string} a
+ * @param {string} b
+ *
+ * @returns {string|null}
+ */
+ makeID: function(type, a, b) {
+ switch (type) {
+ case GLPIImpact.NODE:
+ return a + "::" + b;
+ case GLPIImpact.EDGE:
+ return a + "->" + b;
+ }
+
+ return null;
+ },
+
+ /**
+ * Helper to make an ID selector
+ * We can't use the short syntax "#id" because our ids contains
+ * non-alpha-numeric characters
+ *
+ * @param {string} id
+ *
+ * @returns {string}
+ */
+ makeIDSelector: function(id) {
+ return "[id='" + id + "']";
+ },
+
+ /**
+ * Reload the graph style
+ */
+ updateStyle: function() {
+ this.cy.style(this.getNetworkStyle());
+ // If either the source of the target node of an edge is hidden, hide the
+ // edge too by setting it's dept to the maximum value
+ this.cy.edges().forEach(function(edge) {
+ var source = GLPIImpact.cy.filter(GLPIImpact.makeIDSelector(edge.data('source')));
+ var target = GLPIImpact.cy.filter(GLPIImpact.makeIDSelector(edge.data('target')));
+ if (source.visible() && target.visible()) {
+ edge.data('depth', 0);
+ } else {
+ edge.data('depth', Number.MAX_VALUE);
+ }
+ });
+ },
+
+ /**
+ * Update the flags of the edges of the graph
+ * Explore the graph forward then backward
+ */
+ updateFlags: function() {
+ // Keep track of visited nodes
+ var exploredNodes;
+
+ // Set all flag to the default value (0)
+ this.cy.edges().forEach(function(edge) {
+ edge.data("flag", 0);
+ });
+ this.cy.nodes().data("depth", 0);
+
+ // Run through the graph forward
+ exploredNodes = {};
+ exploredNodes[this.startNode] = true;
+ this.exploreGraph(exploredNodes, GLPIImpact.FORWARD, this.startNode, 0);
+
+ // Run through the graph backward
+ exploredNodes = {};
+ exploredNodes[this.startNode] = true;
+ this.exploreGraph(exploredNodes, GLPIImpact.BACKWARD, this.startNode, 0);
+
+ this.updateStyle();
+ },
+
+ /**
+ * Toggle impact/depends visibility
+ *
+ * @param {*} toToggle
+ */
+ toggleVisibility: function(toToggle) {
+ // Update toolbar icons
+ if (toToggle == GLPIImpact.FORWARD) {
+ $(GLPIImpact.toolbar.toggleImpact).find('i').toggleClass("fa-eye fa-eye-slash");
+ } else {
+ $(GLPIImpact.toolbar.toggleDepends).find('i').toggleClass("fa-eye fa-eye-slash");
+ }
+
+ // Update visibility setting
+ GLPIImpact.directionVisibility[toToggle] = !GLPIImpact.directionVisibility[toToggle];
+
+ // Compute direction
+ var direction;
+ var forward = GLPIImpact.directionVisibility[GLPIImpact.FORWARD];
+ var backward = GLPIImpact.directionVisibility[GLPIImpact.BACKWARD];
+
+ if (forward && backward) {
+ direction = GLPIImpact.BOTH;
+ } else if (!forward && backward) {
+ direction = GLPIImpact.BACKWARD;
+ } else if (forward && !backward) {
+ direction = GLPIImpact.FORWARD;
+ } else {
+ direction = 0;
+ }
+
+ // Hide all nodes
+ GLPIImpact.cy.filter("node").data('hidden', 1);
+
+ // Show/Hide edges according to the direction
+ GLPIImpact.cy.filter("edge").forEach(function(edge) {
+ if (edge.data('flag') & direction) {
+ edge.data('hidden', 0);
+
+ // If the edge is visible, show the nodes they are connected to it
+ var sourceFilter = "node[id='" + edge.data('source') + "']";
+ var targetFilter = "node[id='" + edge.data('target') + "']";
+ GLPIImpact.cy.filter(sourceFilter + ", " + targetFilter)
+ .data("hidden", 0);
+
+ // Make the parents of theses node visibles too
+ GLPIImpact.cy.filter(sourceFilter + ", " + targetFilter)
+ .parent()
+ .data("hidden", 0);
+ } else {
+ edge.data('hidden', 1);
+ }
+ });
+
+ // Start node should always be visible
+ GLPIImpact.cy.filter(GLPIImpact.makeIDSelector(GLPIImpact.startNode))
+ .data("hidden", 0);
+
+ GLPIImpact.updateStyle();
+ },
+
+ /**
+ * Explore a graph in a given direction using recursion
+ *
+ * @param {Array} exploredNodes
+ * @param {number} direction
+ * @param {string} currentNodeID
+ * @param {number} depth
+ */
+ exploreGraph: function(exploredNodes, direction, currentNodeID, depth) {
+ // Set node depth
+ var node = this.cy.filter(this.makeIDSelector(currentNodeID));
+ if (node.data('depth') == 0 || node.data('depth') > depth) {
+ node.data('depth', depth);
+ }
+
+ // If node has a parent, set it's depth too
+ if (node.isChild() && (
+ node.parent().data('depth') == 0 ||
+ node.parent().data('depth') > depth
+ )) {
+ node.parent().data('depth', depth);
+ }
+
+ depth++;
+
+ // Depending on the direction, we are looking for edge that either begin
+ // from the current node (source) or end on the current node (target)
+ var sourceOrTarget;
+
+ // The next node is the opposite of sourceOrTarget : if our node is at
+ // the start (source) then the next is at the end (target)
+ var nextNode;
+
+ switch (direction) {
+ case GLPIImpact.FORWARD:
+ sourceOrTarget = "source";
+ nextNode = "target";
+ break;
+ case GLPIImpact.BACKWARD:
+ sourceOrTarget = "target";
+ nextNode = "source";
+ break;
+ }
+
+ // Find the edges connected to the current node
+ this.cy.elements('edge[' + sourceOrTarget + '="' + currentNodeID + '"]')
+ .forEach(function(edge) {
+ // Get target node from computer nextNode att name
+ var targetNode = edge.data(nextNode);
+
+ // Set flag
+ edge.data("flag", direction | edge.data("flag"));
+
+ // Check we haven't go through this node yet
+ if(exploredNodes[targetNode] == undefined) {
+ exploredNodes[targetNode] = true;
+ // Go to next node
+ GLPIImpact.exploreGraph(exploredNodes, direction, targetNode, depth);
+ }
+ });
+ },
+
+ /**
+ * Get translated value for a given key
+ *
+ * @param {string} key
+ */
+ getLocale: function(key) {
+ return this.locales[key];
+ },
+
+ /**
+ * Ask the backend to build a graph from a specific node
+ *
+ * @param {Object} node
+ * @returns {Array|null}
+ */
+ buildGraphFromNode: function(node) {
+ var dfd = jQuery.Deferred();
+
+ // Request to backend
+ $.ajax({
+ type: "GET",
+ url: CFG_GLPI.root_doc + "/ajax/impact.php",
+ dataType: "json",
+ data: node,
+ success: function(data) {
+ dfd.resolve(JSON.parse(data.graph));
+ },
+ error: function () {
+ dfd.reject();
+ }
+ });
+
+ return dfd.promise();
+ },
+
+
+ getDistance: function(a, b) {
+ return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
+ },
+
+ /**
+ * Insert another new graph into the current one
+ *
+ * @param {Array} graph
+ * @param {Object} startNode data, x, y
+ */
+ insertGraph: function(graph, startNode) {
+ var toAdd = [];
+
+ // Find closest available space near the graph
+ var boundingBox = this.cy.filter().boundingBox();
+ var distances = {
+ right: this.getDistance(
+ {
+ x: boundingBox.x2,
+ y: (boundingBox.y1 + boundingBox.y2) / 2
+ },
+ startNode
+ ),
+ left: this.getDistance(
+ {
+ x: boundingBox.x1,
+ y: (boundingBox.y1 + boundingBox.y2) / 2
+ },
+ startNode
+ ),
+ top: this.getDistance(
+ {
+ x: (boundingBox.x1 + boundingBox.x2) / 2,
+ y: boundingBox.y2
+ },
+ startNode
+ ),
+ bottom: this.getDistance(
+ {
+ x: (boundingBox.x1 + boundingBox.x2) / 2,
+ y: boundingBox.y1
+ },
+ startNode
+ ),
+ };
+ var lowest = Math.min.apply(null, Object.values(distances));
+ var direction = Object.keys(distances).filter(function (x) {
+ return distances[x] === lowest;
+ })[0];
+
+ // Try to add the new graph nodes
+ for (var i=0; i 0) {
+ continue;
+ }
+ // Store node to add them at once with a layout
+ toAdd.push(graph[i]);
+ }
+
+ // Just place the node if only one result is found
+ if (toAdd.length == 1) {
+ toAdd[0].position = {
+ x: startNode.x,
+ y: startNode.y,
+ };
+
+ this.cy.add(toAdd);
+ return;
+ }
+
+ // Add nodes and apply layout
+ var eles = this.cy.add(toAdd);
+ var options = GLPIImpact.getDagreLayout();
+
+ // Place the layout anywhere to compute it's bounding box
+ var layout = eles.layout(options);
+ layout.run();
+
+ var newGraphBoundingBox = eles.boundingBox();
+ var startingPoint;
+
+ // Now compute the real location where we want it
+ switch (direction) {
+ case 'right':
+ startingPoint = {
+ x: newGraphBoundingBox.x1,
+ y: (newGraphBoundingBox.y1 + newGraphBoundingBox.y2) / 2,
+ };
+ break;
+ case 'left':
+ startingPoint = {
+ x: newGraphBoundingBox.x2,
+ y: (newGraphBoundingBox.y1 + newGraphBoundingBox.y2) / 2,
+ };
+ break;
+ case 'top':
+ startingPoint = {
+ x: (newGraphBoundingBox.x1 + newGraphBoundingBox.x2) / 2,
+ y: newGraphBoundingBox.y1,
+ };
+ break;
+ case 'bottom':
+ startingPoint = {
+ x: (newGraphBoundingBox.x1 + newGraphBoundingBox.x2) / 2,
+ y: newGraphBoundingBox.y2,
+ };
+ break;
+ }
+
+ newGraphBoundingBox.x1 += startNode.x - startingPoint.x;
+ newGraphBoundingBox.x2 += startNode.x - startingPoint.x;
+ newGraphBoundingBox.y1 += startNode.y - startingPoint.y;
+ newGraphBoundingBox.y2 += startNode.y - startingPoint.y;
+
+ options.boundingBox = newGraphBoundingBox;
+
+ // Apply layout again with correct bounding box
+ layout = eles.layout(options);
+ layout.run();
+
+ this.cy.animate({
+ center: {
+ eles : GLPIImpact.cy.filter(""),
+ },
+ });
+ },
+
+ /**
+ * Set the colors
+ *
+ * @param {object} colors default, backward, forward, both
+ */
+ setEdgeColors: function (colors) {
+ this.setColorIfExist(GLPIImpact.DEFAULT, colors.default);
+ this.setColorIfExist(GLPIImpact.BACKWARD, colors.backward);
+ this.setColorIfExist(GLPIImpact.FORWARD, colors.forward);
+ this.setColorIfExist(GLPIImpact.BOTH, colors.both);
+ },
+
+ /**
+ * Set color if exist
+ *
+ * @param {object} colors default, backward, forward, both
+ */
+ setColorIfExist: function (index, color) {
+ if (color !== undefined) {
+ this.edgeColors[index] = color;
+ }
+ },
+
+ /**
+ * Exit current edition mode and enter a new one
+ *
+ * @param {number} mode
+ */
+ setEditionMode: function (mode) {
+ // Switching to a mode we are already in -> go to default
+ if (this.editionMode == mode) {
+ mode = GLPIImpact.EDITION_DEFAULT;
+ }
+
+ this.exitEditionMode();
+ this.enterEditionMode(mode);
+ this.editionMode = mode;
+ },
+
+ /**
+ * Exit current edition mode
+ */
+ exitEditionMode: function() {
+ switch (this.editionMode) {
+ case GLPIImpact.EDITION_DEFAULT:
+ GLPIImpact.cy.nodes().ungrabify();
+ break;
+
+ case GLPIImpact.EDITION_ADD_NODE:
+ $(this.toolbar.addNode).removeClass("active");
+ break;
+
+ case GLPIImpact.EDITION_ADD_EDGE:
+ $(GLPIImpact.toolbar.addEdge).removeClass("active");
+ // Empty event data and remove tmp node
+ GLPIImpact.eventData.addEdgeStart = null;
+ GLPIImpact.cy.filter("#tmp_node").remove();
+ break;
+
+ case GLPIImpact.EDITION_DELETE:
+ this.cy.filter().unselect();
+ this.cy.data('todelete', 0);
+ $(GLPIImpact.toolbar.deleteElement).removeClass("active");
+ break;
+
+ case GLPIImpact.EDITION_ADD_COMPOUND:
+ GLPIImpact.cy.panningEnabled(true);
+ GLPIImpact.cy.boxSelectionEnabled(false);
+ $(GLPIImpact.toolbar.addCompound).removeClass("active");
+ break;
+ }
+ },
+
+ /**
+ * Enter a new edition mode
+ *
+ * @param {number} mode
+ */
+ enterEditionMode: function(mode) {
+ switch (mode) {
+ case GLPIImpact.EDITION_DEFAULT:
+ this.clearHelpText();
+ GLPIImpact.cy.nodes().grabify();
+ $(this.impactContainer).css('cursor', "move");
+ break;
+
+ case GLPIImpact.EDITION_ADD_NODE:
+ this.showHelpText("addNodeHelpText");
+ $(this.toolbar.addNode).addClass("active");
+ $(this.impactContainer).css('cursor', "copy");
+ break;
+
+ case GLPIImpact.EDITION_ADD_EDGE:
+ this.showHelpText("addEdgeHelpText");
+ $(this.toolbar.addEdge).addClass("active");
+ $(this.impactContainer).css('cursor', "crosshair");
+ break;
+
+ case GLPIImpact.EDITION_DELETE:
+ this.cy.filter().unselect();
+ this.showHelpText("deleteHelpText");
+ $(this.toolbar.deleteElement).addClass("active");
+ break;
+
+ case GLPIImpact.EDITION_ADD_COMPOUND:
+ GLPIImpact.cy.panningEnabled(false);
+ GLPIImpact.cy.boxSelectionEnabled(true);
+ this.showHelpText("addCompoundHelpText");
+ $(this.toolbar.addCompound).addClass("active");
+ $(this.impactContainer).css('cursor', "crosshair");
+ break;
+ }
+ },
+
+ /**
+ * Hide the toolbar and show an help text
+ *
+ * @param {string} text
+ */
+ showHelpText: function(text) {
+ $(GLPIImpact.toolbar.helpText).html(this.getLocale(text)).show();
+ },
+
+ /**
+ * Hide the help text and show the toolbar
+ */
+ clearHelpText: function() {
+ $(GLPIImpact.toolbar.helpText).hide();
+ },
+
+ /**
+ * Export the graph in the given format
+ *
+ * @param {string} format
+ * @param {boolean} transparentBackground (png only)
+ *
+ * @returns {Object} filename, filecontent
+ */
+ download: function(format, transparentBackground) {
+ var filename;
+ var filecontent;
+
+ // Create fake link
+ GLPIImpact.impactContainer.append("");
+ var link = $('#impact_download');
+
+ switch (format) {
+ case 'png':
+ filename = "impact.png";
+ filecontent = this.cy.png({
+ bg: transparentBackground ? "transparent" : "white"
+ });
+ break;
+
+ case 'jpeg':
+ filename = "impact.jpeg";
+ filecontent = this.cy.jpg();
+ break;
+ }
+
+ // Trigger download and remore the link
+ link.prop('download', filename);
+ link.prop("href", filecontent);
+ link[0].click();
+ link.remove();
+ },
+
+ /**
+ * Get node at target position
+ *
+ * @param {Object} position x, y
+ * @param {function} filter if false return null
+ */
+ getNodeAt: function(position, filter) {
+ var nodes = this.cy.nodes();
+
+ for (var i=0; i position.x
+ && nodes[i].boundingBox().y1 < position.y
+ && nodes[i].boundingBox().y2 > position.y) {
+ // Check if the node is excluded
+ if (filter(nodes[i])) {
+ return nodes[i];
+ }
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Enable the save button
+ */
+ showCleanWorkspaceStatus: function() {
+ $(GLPIImpact.toolbar.save).removeClass('dirty');
+ $(GLPIImpact.toolbar.save).addClass('clean');
+ $(GLPIImpact.toolbar.save).find('i').removeClass("fas fa-exclamation-triangle");
+ $(GLPIImpact.toolbar.save).find('i').addClass("fas fa-check");
+ $(GLPIImpact.toolbar.save).find('i').qtip(GLPIImpact.getTooltip("workspaceSaved"));
+ },
+
+ /**
+ * Enable the save button
+ */
+ showDirtyWorkspaceStatus: function() {
+ $(GLPIImpact.toolbar.save).removeClass('clean');
+ $(GLPIImpact.toolbar.save).addClass('dirty');
+ $(GLPIImpact.toolbar.save).find('i').removeClass("fas fa-check");
+ $(GLPIImpact.toolbar.save).find('i').addClass("fas fa-exclamation-triangle");
+ $(GLPIImpact.toolbar.save).find('i').qtip(this.getTooltip("unsavedChanges"));
+ },
+
+ /**
+ * Enable the save button
+ */
+ showDefaultWorkspaceStatus: function() {
+ $(GLPIImpact.toolbar.save).removeClass('clean');
+ $(GLPIImpact.toolbar.save).removeClass('dirty');
+ $(GLPIImpact.toolbar.save).find('i').removeClass("fas fa-check");
+ $(GLPIImpact.toolbar.save).find('i').removeClass("fas fa-exclamation-triangle");
+ },
+
+ /**
+ * Build the ongoing dialog content according to the list of ITILObjects
+ *
+ * @param {Object} ITILObjects requests, incidents, changes, problems
+ *
+ * @returns {string}
+ */
+ buildOngoingDialogContent: function(ITILObjects) {
+ return this.listElements("requests", ITILObjects.requests, "ticket")
+ + this.listElements("incidents", ITILObjects.incidents, "ticket")
+ + this.listElements("changes", ITILObjects.changes , "change")
+ + this.listElements("problems", ITILObjects.problems, "problem");
+ },
+
+ /**
+ * Build an html list
+ *
+ * @param {string} title requests, incidents, changes, problems
+ * @param {string} elements requests, incidents, changes, problems
+ * @param {string} url key used to generate the URL
+ *
+ * @returns {string}
+ */
+ listElements: function(title, elements, url) {
+ var html = "";
+
+ if (elements.length > 0) {
+ html += "" + this.getLocale(title) + "";
+ html += "";
+
+ elements.forEach(function(element) {
+ var link = "./" + url + ".form.php?id=" + element.id;
+ html += '- ' + element.name
+ + '
';
+ });
+ html += " ";
+ }
+
+ return html;
+ },
+
+ /**
+ * Add a new compound from the selected nodes
+ */
+ addCompoundFromSelection: _.debounce(function(){
+ // Check that there is enough selected nodes
+ if (GLPIImpact.eventData.boxSelected.length < 2) {
+ alert(GLPIImpact.getLocale("notEnoughItems"));
+ } else {
+ // Create the compound
+ var newCompound = GLPIImpact.cy.add({group: 'nodes'});
+
+ // Set parent for coumpound member
+ GLPIImpact.eventData.boxSelected.forEach(function(ele) {
+ ele.move({'parent': newCompound.data('id')});
+ });
+
+ // Show edit dialog
+ $(GLPIImpact.dialogs.editCompoundDialog.id).dialog(
+ GLPIImpact.getEditCompoundDialog(newCompound)
+ );
+
+ // Back to default mode
+ GLPIImpact.setEditionMode(GLPIImpact.EDITION_DEFAULT);
+ }
+
+ // Clear the selection
+ GLPIImpact.eventData.boxSelected = [];
+ GLPIImpact.cy.filter(":selected").unselect();
+ }, 100, false),
+
+ /**
+ * Remove an element from the graph
+ *
+ * @param {object} ele
+ */
+ deleteFromGraph: function(ele) {
+ if (ele.data('id') == GLPIImpact.startNode) {
+ alert("Can't remove starting node");
+ return;
+ }
+
+ if (ele.isEdge()) {
+ // Case 1: removing an edge
+ ele.remove();
+ // this.cy.remove(impact.makeIDSelector(ele.data('id')));
+ } else if (ele.isParent()) {
+ // Case 2: removing a compound
+ // Remove only the parent
+ ele.children().move({parent: null});
+ ele.remove();
+
+ } else {
+ // Case 3: removing a node
+ // Remove parent if last child of a compound
+ if (!ele.isOrphan() && ele.parent().children().length <= 2) {
+ this.deleteFromGraph(ele.parent());
+ }
+
+ // Remove all edges connected to this node from graph and delta
+ ele.remove();
+ }
+
+ // Update flags
+ GLPIImpact.updateFlags();
+ },
+
+ /**
+ * Handle global click events
+ *
+ * @param {JQuery.Event} event
+ */
+ onClick: function (event) {
+ switch (GLPIImpact.editionMode) {
+ case GLPIImpact.EDITION_DEFAULT:
+ break;
+
+ case GLPIImpact.EDITION_ADD_NODE:
+ // Click in EDITION_ADD_NODE : add a new node
+ $(GLPIImpact.dialogs.addNode.id).dialog(GLPIImpact.getAddNodeDialog(
+ GLPIImpact.dialogs.addNode.inputs.itemType,
+ GLPIImpact.dialogs.addNode.inputs.itemID,
+ event.position
+ ));
+ break;
+
+ case GLPIImpact.EDITION_ADD_EDGE:
+ break;
+
+ case GLPIImpact.EDITION_DELETE:
+ break;
+ }
+ },
+
+ /**
+ * Handle click on edge
+ *
+ * @param {JQuery.Event} event
+ */
+ edgeOnClick: function (event) {
+ switch (GLPIImpact.editionMode) {
+ case GLPIImpact.EDITION_DEFAULT:
+ break;
+
+ case GLPIImpact.EDITION_ADD_NODE:
+ break;
+
+ case GLPIImpact.EDITION_ADD_EDGE:
+ break;
+
+ case GLPIImpact.EDITION_DELETE:
+ // Remove the edge from the graph
+ GLPIImpact.deleteFromGraph(event.target);
+ break;
+ }
+ },
+
+ /**
+ * Handle click on node
+ *
+ * @param {JQuery.Event} event
+ */
+ nodeOnClick: function (event) {
+ switch (GLPIImpact.editionMode) {
+ case GLPIImpact.EDITION_DEFAULT:
+ if (GLPIImpact.eventData.lastClick != null) {
+ // Trigger homemade double click event
+ if (event.timeStamp - GLPIImpact.eventData.lastClick < 500) {
+ event.target.trigger('doubleClick', event);
+ }
+ }
+
+ GLPIImpact.eventData.lastClick = event.timeStamp;
+ break;
+
+ case GLPIImpact.EDITION_ADD_NODE:
+ break;
+
+ case GLPIImpact.EDITION_ADD_EDGE:
+ break;
+
+ case GLPIImpact.EDITION_DELETE:
+ GLPIImpact.deleteFromGraph(event.target);
+ break;
+ }
+ },
+
+ /**
+ * Handle end of box selection event
+ *
+ * @param {JQuery.Event} event
+ */
+ onBox: function (event) {
+ switch (GLPIImpact.editionMode) {
+ case GLPIImpact.EDITION_DEFAULT:
+ break;
+
+ case GLPIImpact.EDITION_ADD_NODE:
+ break;
+
+ case GLPIImpact.EDITION_ADD_EDGE:
+ break;
+
+ case GLPIImpact.EDITION_DELETE:
+ break;
+
+ case GLPIImpact.EDITION_ADD_COMPOUND:
+ var ele = event.target;
+ // Add node to selected list if he is not part of a compound already
+ if (ele.isNode() && ele.isOrphan() && !ele.isParent()) {
+ GLPIImpact.eventData.boxSelected.push(ele);
+ }
+ GLPIImpact.addCompoundFromSelection();
+ break;
+ }
+ },
+
+ /**
+ * Handle any graph modification
+ *
+ * @param {*} event
+ */
+ onChange: function() {
+ GLPIImpact.showDirtyWorkspaceStatus();
+ },
+
+ /**
+ * Double click handler
+ * @param {JQuery.Event} event
+ */
+ onDoubleClick: function(event) {
+ // Open edit dialog on compound nodes
+ if (event.target.isParent()) {
+ $(GLPIImpact.dialogs.editCompoundDialog.id).dialog(
+ GLPIImpact.getEditCompoundDialog(event.target)
+ );
+ }
+ },
+
+ /**
+ * Handler for key down events
+ *
+ * @param {JQuery.Event} event
+ */
+ onKeyDown: function(event) {
+ switch (event.which) {
+ // ESC
+ case 27:
+ // Exit specific edition mode
+ if (GLPIImpact.editionMode != GLPIImpact.EDITION_DEFAULT) {
+ GLPIImpact.setEditionMode(GLPIImpact.EDITION_DEFAULT);
+ }
+ break;
+
+ // Delete
+ case 46:
+ // Delete selected elements
+ GLPIImpact.cy.filter(":selected").forEach(function(ele) {
+ GLPIImpact.deleteFromGraph(ele);
+ });
+ break;
+ }
+ },
+
+ /**
+ * Handle mousedown events on nodes
+ *
+ * @param {JQuery.Event} event
+ */
+ nodeOnMousedown: function (event) {
+ switch (GLPIImpact.editionMode) {
+ case GLPIImpact.EDITION_DEFAULT:
+ $(GLPIImpact.impactContainer).css('cursor', "grabbing");
+
+ // If we are not on a compound node or a node already inside one
+ if (event.target.isOrphan() && !event.target.isParent()) {
+ GLPIImpact.eventData.grabNodeStart = event.target;
+ }
+ break;
+
+ case GLPIImpact.EDITION_ADD_NODE:
+ break;
+
+ case GLPIImpact.EDITION_ADD_EDGE:
+ if (!event.target.isParent()) {
+ GLPIImpact.eventData.addEdgeStart = this.data('id');
+ }
+ break;
+
+ case GLPIImpact.EDITION_DELETE:
+ break;
+
+ case GLPIImpact.EDITION_ADD_COMPOUND:
+ break;
+ }
+ },
+
+ /**
+ * Handle mouseup events on nodes
+ *
+ * @param {JQuery.Event} event
+ */
+ nodeOnMouseup: function (event) {
+ switch (GLPIImpact.editionMode) {
+ case GLPIImpact.EDITION_DEFAULT:
+ $(GLPIImpact.impactContainer).css('cursor', "grab");
+
+ // Check if we were grabbing a node
+ if (GLPIImpact.eventData.grabNodeStart != null) {
+ // Reset eventData for node grabbing
+ GLPIImpact.eventData.grabNodeStart = null;
+ GLPIImpact.eventData.boundingBox = null;
+ }
+
+ break;
+
+ case GLPIImpact.EDITION_ADD_NODE:
+ break;
+
+ case GLPIImpact.EDITION_ADD_EDGE:
+ // Exit if no start node
+ if (GLPIImpact.eventData.addEdgeStart == null) {
+ return;
+ }
+
+ // Reset addEdgeStart
+ var startEdge = GLPIImpact.eventData.addEdgeStart; // Keep a copy to use later
+ GLPIImpact.eventData.addEdgeStart = null;
+
+ // Remove current tmp collection
+ event.cy.remove(GLPIImpact.eventData.tmpEles);
+ GLPIImpact.eventData.tmpEles = null;
+
+ // Option 1: Edge between a node and the fake tmp_node -> ignore
+ if (this.data('id') == 'tmp_node') {
+ return;
+ }
+
+ // Option 2: Edge between two nodes that already exist -> ignore
+ var edgeID = GLPIImpact.makeID(GLPIImpact.EDGE, startEdge, this.data('id'));
+ if (event.cy.filter('edge[id="' + edgeID + '"]').length > 0) {
+ return;
+ }
+
+ // Option 3: Both end of the edge are actually the same node -> ignore
+ if (startEdge == this.data('id')) {
+ return;
+ }
+
+ // Option 4: Edge between two nodes that does not exist yet -> create it!
+ event.cy.add({
+ group: 'edges',
+ data: {
+ id: edgeID,
+ source: startEdge,
+ target: this.data('id')
+ }
+ });
+
+ // Update dependencies flags according to the new link
+ GLPIImpact.updateFlags();
+ break;
+
+ case GLPIImpact.EDITION_DELETE:
+ break;
+ }
+ },
+
+ /**
+ * Handle mousemove events on nodes
+ *
+ * @param {JQuery.Event} event
+ */
+ onMousemove: _.throttle(function(event) {
+ var node;
+
+ switch (GLPIImpact.editionMode) {
+ case GLPIImpact.EDITION_DEFAULT:
+ // No action if we are not grabbing a node
+ if (GLPIImpact.eventData.grabNodeStart == null) {
+ return;
+ }
+
+ // Look for a compound at the cursor position
+ node = GLPIImpact.getNodeAt(event.position, function(node) {
+ return node.isParent();
+ });
+
+ if (node) {
+ // If we have a bounding box defined, the grabbed node is already
+ // being placed into a compound, we need to check if it was moved
+ // outside this original bouding box to know if the user is trying
+ // to move if away from the compound
+ if (GLPIImpact.eventData.boundingBox != null) {
+ // If the user tried to move out of the compound
+ if (GLPIImpact.eventData.boundingBox.x1 > event.position.x
+ || GLPIImpact.eventData.boundingBox.x2 < event.position.x
+ || GLPIImpact.eventData.boundingBox.y1 > event.position.y
+ || GLPIImpact.eventData.boundingBox.y2 < event.position.y) {
+ // Remove it from the compound
+ GLPIImpact.eventData.grabNodeStart.move({parent: null});
+ GLPIImpact.eventData.boundingBox = null;
+ }
+ } else {
+ // If we found a compound, add the grabbed node inside
+ GLPIImpact.eventData.grabNodeStart.move({parent: node.data('id')});
+
+ // Store the original bouding box of the compound
+ GLPIImpact.eventData.boundingBox = node.boundingBox();
+ }
+ } else {
+ // Else; reset it's parent so it can be removed from any temporary
+ // compound while the user is stil grabbing
+ GLPIImpact.eventData.grabNodeStart.move({parent: null});
+ }
+
+ break;
+
+ case GLPIImpact.EDITION_ADD_NODE:
+ break;
+
+ case GLPIImpact.EDITION_ADD_EDGE:
+ // No action if we are not placing an edge
+ if (GLPIImpact.eventData.addEdgeStart == null) {
+ return;
+ }
+
+ // Remove current tmp collection
+ if (GLPIImpact.eventData.tmpEles != null) {
+ event.cy.remove(GLPIImpact.eventData.tmpEles);
+ }
+
+ node = GLPIImpact.getNodeAt(event.position, function(node) {
+ var nodeID = node.data('id');
+
+ // Can't link to itself
+ if (nodeID == GLPIImpact.eventData.addEdgeStart) {
+ return false;
+ }
+
+ // Can't link to parent
+ if (node.isParent()) {
+ return false;
+ }
+
+ // The created edge shouldn't already exist
+ var edgeID = GLPIImpact.makeID(GLPIImpact.EDGE, GLPIImpact.eventData.addEdgeStart, nodeID);
+ if (GLPIImpact.cy.filter('edge[id="' + edgeID + '"]').length > 0) {
+ return false;
+ }
+
+ // The node must be visible
+ if (GLPIImpact.cy.getElementById(nodeID).data('hidden')) {
+ return false;
+ }
+
+ return true;
+ });
+
+ if (node != null) {
+ node = node.data('id');
+
+ // Add temporary edge to node hovered by the user
+ GLPIImpact.eventData.tmpEles = event.cy.add([
+ {
+ group: 'edges',
+ data: {
+ id: GLPIImpact.makeID(GLPIImpact.EDGE, GLPIImpact.eventData.addEdgeStart, node),
+ source: GLPIImpact.eventData.addEdgeStart,
+ target: node
+ }
+ }
+ ]);
+ } else {
+ // Add temporary edge to a new invisible node at mouse position
+ GLPIImpact.eventData.tmpEles = event.cy.add([
+ {
+ group: 'nodes',
+ data: {
+ id: 'tmp_node',
+ },
+ position: {
+ x: event.position.x,
+ y: event.position.y
+ }
+ },
+ {
+ group: 'edges',
+ data: {
+ id: GLPIImpact.makeID(
+ GLPIImpact.EDGE,
+ GLPIImpact.eventData.addEdgeStart,
+ "tmp_node"
+ ),
+ source: GLPIImpact.eventData.addEdgeStart,
+ target: 'tmp_node',
+ }
+ }
+ ]);
+ }
+ break;
+
+ case GLPIImpact.EDITION_DELETE:
+ break;
+ }
+ }, 25),
+
+ /**
+ * Handle global mouseover events
+ *
+ * @param {JQuery.Event} event
+ */
+ onMouseover: function(event) {
+ switch (GLPIImpact.editionMode) {
+ case GLPIImpact.EDITION_DEFAULT:
+ if (event.target.data('id') == undefined || !event.target.isNode()) {
+ break;
+ }
+ $(GLPIImpact.impactContainer).css('cursor', "grab");
+ break;
+
+ case GLPIImpact.EDITION_ADD_NODE:
+ break;
+
+ case GLPIImpact.EDITION_ADD_EDGE:
+ break;
+
+ case GLPIImpact.EDITION_DELETE:
+ if (event.target.data('id') == undefined) {
+ break;
+ }
+
+ $(GLPIImpact.impactContainer).css('cursor', "default");
+ var id = event.target.data('id');
+
+ // Remove red overlay
+ event.cy.filter().data('todelete', 0);
+ event.cy.filter().unselect();
+
+ // Store here if one default node
+ if (event.target.data('id') == GLPIImpact.startNode) {
+ $(GLPIImpact.impactContainer).css('cursor', "not-allowed");
+ break;
+ }
+
+ // Add red overlay
+ event.target.data('todelete', 1);
+ event.target.select();
+
+ if (event.target.isNode()){
+ var sourceFilter = "edge[source='" + id + "']";
+ var targetFilter = "edge[target='" + id + "']";
+ event.cy.filter(sourceFilter + ", " + targetFilter)
+ .data('todelete', 1)
+ .select();
+ }
+ break;
+ }
+ },
+
+ /**
+ * Handle global mouseout events
+ *
+ * @param {JQuery.Event} event
+ */
+ onMouseout: function(event) {
+ switch (GLPIImpact.editionMode) {
+ case GLPIImpact.EDITION_DEFAULT:
+ $(GLPIImpact.impactContainer).css('cursor', "move");
+ break;
+
+ case GLPIImpact.EDITION_ADD_NODE:
+ break;
+
+ case GLPIImpact.EDITION_ADD_EDGE:
+ break;
+
+ case GLPIImpact.EDITION_DELETE:
+ // Remove red overlay
+ $(GLPIImpact.impactContainer).css('cursor', "move");
+ event.cy.filter().data('todelete', 0);
+ event.cy.filter().unselect();
+ break;
+ }
+ },
+
+ /**
+ * Handle "goTo" menu event
+ *
+ * @param {JQuery.Event} event
+ */
+ menuOnGoTo: function(event) {
+ window.open(event.target.data('link'), 'blank');
+ },
+
+ /**
+ * Handle "showOngoing" menu event
+ *
+ * @param {JQuery.Event} event
+ */
+ menuOnShowOngoing: function(event) {
+ $(GLPIImpact.dialogs.ongoingDialog.id).html(
+ GLPIImpact.buildOngoingDialogContent(event.target.data('ITILObjects'))
+ );
+ $(GLPIImpact.dialogs.ongoingDialog.id).dialog(GLPIImpact.getOngoingDialog());
+ },
+
+ /**
+ * Handle "EditCompound" menu event
+ *
+ * @param {JQuery.Event} event
+ */
+ menuOnEditCompound: function (event) {
+ $(GLPIImpact.dialogs.editCompoundDialog.id).dialog(
+ GLPIImpact.getEditCompoundDialog(event.target)
+ );
+ },
+
+ /**
+ * Handler for "removeFromCompound" action
+ *
+ * @param {JQuery.Event} event
+ */
+ menuOnRemoveFromCompound: function(event) {
+ var parent = GLPIImpact.cy.getElementById(
+ event.target.data('parent')
+ );
+
+ // Remove node from compound
+ event.target.move({parent: null});
+
+ // Destroy compound if only one or zero member left
+ if (parent.children().length < 2) {
+ parent.children().move({parent: null});
+ GLPIImpact.cy.remove(parent);
+ }
+ },
+
+ /**
+ * Handler for "delete" menu action
+ *
+ * @param {JQuery.Event} event
+ */
+ menuOnDelete: function(event){
+ GLPIImpact.deleteFromGraph(event.target);
+ },
+
+ /**
+ * Handler for "new" menu action
+ *
+ * @param {JQuery.Event} event
+ */
+ menuOnNew: function(event) {
+ $(GLPIImpact.dialogs.addNode.id).dialog(GLPIImpact.getAddNodeDialog(
+ GLPIImpact.dialogs.addNode.inputs.itemType,
+ GLPIImpact.dialogs.addNode.inputs.itemID,
+ event.position
+ ));
+ },
+
+ /**
+ * Set event handler for toolbar events
+ */
+ initToolbar: function() {
+ // Save the graph
+ $(GLPIImpact.toolbar.save).click(function() {
+ GLPIImpact.showCleanWorkspaceStatus();
+ // Send data as JSON on submit
+ $.ajax({
+ type: "POST",
+ url: $(GLPIImpact.form).prop('action'),
+ data: {
+ 'impacts': JSON.stringify(GLPIImpact.computeDelta())
+ },
+ success: function(){
+ GLPIImpact.initialState = GLPIImpact.getCurrentState();
+ },
+ error: function(){
+ GLPIImpact.showDirtyWorkspaceStatus();
+ alert("error");
+ },
+ });
+ });
+
+ // Add a new node on the graph
+ $(GLPIImpact.toolbar.addNode).click(function() {
+ GLPIImpact.setEditionMode(GLPIImpact.EDITION_ADD_NODE);
+ });
+ $(GLPIImpact.toolbar.addNode).qtip(this.getTooltip("addNodeTooltip"));
+
+ // Add a new edge on the graph
+ $(GLPIImpact.toolbar.addEdge).click(function() {
+ GLPIImpact.setEditionMode(GLPIImpact.EDITION_ADD_EDGE);
+ });
+ $(GLPIImpact.toolbar.addEdge).qtip(this.getTooltip("addEdgeTooltip"));
+
+ // Add a new compound on the graph
+ $(GLPIImpact.toolbar.addCompound).click(function() {
+ GLPIImpact.setEditionMode(GLPIImpact.EDITION_ADD_COMPOUND);
+ });
+ $(GLPIImpact.toolbar.addCompound).qtip(this.getTooltip("addCompoundTooltip"));
+
+ // Enter delete mode
+ $(GLPIImpact.toolbar.deleteElement).click(function() {
+ GLPIImpact.setEditionMode(GLPIImpact.EDITION_DELETE);
+ });
+ $(GLPIImpact.toolbar.deleteElement).qtip(this.getTooltip("deleteTooltip"));
+
+ // Export graph
+ $(GLPIImpact.toolbar.export).click(function() {
+ GLPIImpact.download(
+ 'png',
+ false
+ );
+ });
+ $(GLPIImpact.toolbar.export).qtip(this.getTooltip("downloadTooltip"));
+
+ // "More" dropdown menu
+ $(GLPIImpact.toolbar.expandToolbar).click(showMenu);
+
+ // Toggle impact visibility
+ $(GLPIImpact.toolbar.toggleImpact).click(function() {
+ GLPIImpact.toggleVisibility(GLPIImpact.FORWARD);
+ GLPIImpact.cy.trigger("change");
+ });
+
+ // Toggle depends visibility
+ $(GLPIImpact.toolbar.toggleDepends).click(function() {
+ GLPIImpact.toggleVisibility(GLPIImpact.BACKWARD);
+ GLPIImpact.cy.trigger("change");
+ });
+
+ // Color picker
+ $(GLPIImpact.toolbar.colorPicker).click(function() {
+ $(GLPIImpact.dialogs.configColor.id).dialog(GLPIImpact.getColorPickerDialog(
+ $(GLPIImpact.dialogs.configColor.inputs.dependsColor),
+ $(GLPIImpact.dialogs.configColor.inputs.impactColor),
+ $(GLPIImpact.dialogs.configColor.inputs.impactAndDependsColor)
+ ));
+ });
+
+ // Depth selector
+ $(GLPIImpact.toolbar.maxDepth).on('input', function() {
+ var max = $(GLPIImpact.toolbar.maxDepth).val();
+ GLPIImpact.maxDepth = max;
+
+ if (max == 10) {
+ max = "infinity";
+ GLPIImpact.maxDepth = Number.MAX_VALUE;
+ }
+
+ $(GLPIImpact.toolbar.maxDepthView).html("Max depth: " + max);
+ GLPIImpact.updateStyle();
+ GLPIImpact.cy.trigger("change");
+ });
+ }
+};
+
+
diff --git a/js/planning.js b/js/planning.js
index 72d3d24ecb2..440134eb349 100644
--- a/js/planning.js
+++ b/js/planning.js
@@ -1,8 +1,8 @@
-
/* global FullCalendar, FullCalendarLocales */
-
-var calendar = null;
-var GLPIPlanning = {
+var GLPIPlanning = {
+ calendar: null,
+ all_resources: [],
+ visible_res: [],
display: function(params) {
// get passed options and merge it with default ones
@@ -12,11 +12,14 @@ var GLPIPlanning = {
full_view: true,
default_view: 'timeGridWeek',
height: GLPIPlanning.getHeight,
+ plugins: ['dayGrid', 'interaction', 'list', 'timeGrid', 'resourceTimeline', 'rrule'],
+ license_key: "",
+ resources: [],
rand: '',
header: {
left: 'prev,next,today',
center: 'title',
- right: 'dayGridMonth, timeGridWeek, timeGridDay, listFull'
+ right: 'dayGridMonth, timeGridWeek, timeGridDay, listFull, resourceWeek'
},
};
options = Object.assign({}, default_options, options);
@@ -27,8 +30,14 @@ var GLPIPlanning = {
var disable_qtip = false;
var disable_edit = false;
- calendar = new FullCalendar.Calendar(document.getElementById(dom_id), {
- plugins: ['dayGrid', 'interaction', 'list', 'timeGrid'],
+ // manage visible resources
+ this.all_resources = options.resources;
+ this.visible_res = Object.keys(this.all_resources).filter(function(index) {
+ return GLPIPlanning.all_resources[index].is_visible;
+ });
+
+ this.calendar = new FullCalendar.Calendar(document.getElementById(dom_id), {
+ plugins: options.plugins,
height: options.height,
theme: true,
weekNumbers: options.full_view ? true : false,
@@ -37,9 +46,22 @@ var GLPIPlanning = {
eventLimit: true, // show 'more' button when too mmany events
minTime: CFG_GLPI.planning_begin,
maxTime: CFG_GLPI.planning_end,
+ schedulerLicenseKey: options.license_key,
+ resourceAreaWidth: '15%',
+ nowIndicator: true,
listDayAltFormat: false,
agendaEventMinHeight: 13,
header: options.header,
+ //resources: options.resources,
+ resources: function(fetchInfo, successCallback) {
+ // Filter resources by whether their id is in visible_res.
+ var filteredResources = [];
+ filteredResources = options.resources.filter(function(elem, index) {
+ return GLPIPlanning.visible_res.indexOf(index.toString()) !== -1;
+ });
+
+ successCallback(filteredResources);
+ },
views: {
listFull: {
type: 'list',
@@ -53,6 +75,31 @@ var GLPIPlanning = {
end: (new Date(currentDate.getTime())).setFullYear(current_year + 5)
};
}
+ },
+ resourceWeek: {
+ type: 'resourceTimeline',
+ buttonText: 'Timeline Week',
+ duration: { weeks: 1 },
+ //hiddenDays: [6, 0],
+ groupByDateAndResource: true,
+ },
+ },
+ resourceRender: function(info) {
+ var icon = "";
+ switch (info.resource._resource.extendedProps.itemtype.toLowerCase()) {
+ case "group":
+ case "group_user":
+ icon = "users";
+ break;
+ case "user":
+ icon = "user";
+ }
+ $(info.el)
+ .find('.fc-cell-text')
+ .prepend(' ');
+
+ if (info.resource._resource.extendedProps.itemtype == 'Group_User') {
+ info.el.style.backgroundColor = 'lightgray';
}
},
eventRender: function(info) {
@@ -62,17 +109,28 @@ var GLPIPlanning = {
var view = info.view;
var eventtype_marker = '';
- element.find('.fc-content').after(eventtype_marker);
- element.find('.fc-list-item-title > a').prepend(eventtype_marker);
+ element.append(eventtype_marker);
var content = extProps.content;
var tooltip = extProps.tooltip;
if (view.type !== 'dayGridMonth'
&& view.type.indexOf('list') < 0
+ && event.rendering != "background"
&& !event.allDay){
element.append(''+content+' ');
}
+ // add icon if exists
+ if ("icon" in extProps) {
+ var icon_alt = "";
+ if ("icon_alt" in extProps) {
+ icon_alt = extProps.icon_alt;
+ }
+
+ element.find(".fc-title, .fc-list-item-title")
+ .append(" ");
+ }
+
// add classes to current event
var added_classes = '';
if (typeof event.end !== 'undefined'
@@ -139,9 +197,11 @@ var GLPIPlanning = {
datesRender: function(info) {
var view = info.view;
- // force refetch events from ajax on view change (don't refetch on firt load)
+ // force refetch events from ajax on view change (don't refetch on first load)
if (loaded) {
- calendar.refetchEvents();
+ GLPIPlanning.refresh();
+ } else {
+ loaded = true;
}
// specific process for full list
@@ -161,32 +221,30 @@ var GLPIPlanning = {
// show controls buttons
$('#planning .fc-left .fc-button-group').show();
}
+
+ // set end of day markers for timeline
+ GLPIPlanning.setEndofDays(info.view);
},
- eventAfterAllRender: function() {
- // set a var to force refetch events (see viewRender callback)
- loaded = true;
-
- // scroll div to first element needed to be viewed
- var scrolltoevent = $('#'+dom_id+' .event_past.event_todo').first();
- if (scrolltoevent.length == 0) {
- scrolltoevent = $('#'+dom_id+' .event_today').first();
- }
- if (scrolltoevent.length == 0) {
- scrolltoevent = $('#'+dom_id+' .event_future').first();
- }
- if (scrolltoevent.length == 0) {
- scrolltoevent = $('#'+dom_id+' .event_past').last();
- }
- if (scrolltoevent.length) {
- $('#'+dom_id+' .fc-scroller').scrollTop(scrolltoevent.prop('offsetTop')-25);
- }
+ viewSkeletonRender : function(info) {
+ // inform backend we changed view (to store it in session)
+ $.ajax({
+ url: CFG_GLPI.root_doc+"/ajax/planning.php",
+ type: 'POST',
+ data: {
+ action: 'view_changed',
+ view: info.view.type
+ }
+ });
+
+ // set end of day markers for timeline
+ GLPIPlanning.setEndofDays(info.view);
},
events: {
url: CFG_GLPI.root_doc+"/ajax/planning.php",
type: 'POST',
extraParams: function() {
- var view_name = calendar
- ? calendar.state.viewType
+ var view_name = GLPIPlanning.calendar
+ ? GLPIPlanning.calendar.state.viewType
: options.default_view;
var display_done_events = 1;
if (view_name.indexOf('list') >= 0) {
@@ -194,16 +252,17 @@ var GLPIPlanning = {
}
return {
'action': 'get_events',
- 'display_done_events': display_done_events
+ 'display_done_events': display_done_events,
+ 'view_name': view_name
};
},
success: function(data) {
if (!options.full_view && data.length == 0) {
- calendar.setOption('height', 0);
+ GLPIPlanning.calendar.setOption('height', 0);
}
},
- failure: function() {
- console.error('there was an error while fetching events!');
+ failure: function(error) {
+ console.error('there was an error while fetching events!', error);
}
},
@@ -211,9 +270,7 @@ var GLPIPlanning = {
// EDIT EVENTS
editable: true, // we can drag and resize events
eventResize: function(info) {
- var event = info.event;
- var revertFunc = info.revert;
- GLPIPlanning.editEventTimes(event, revertFunc);
+ GLPIPlanning.editEventTimes(info);
},
eventResizeStart: function() {
disable_edit = true;
@@ -230,9 +287,7 @@ var GLPIPlanning = {
},
eventDrop: function(info) {
disable_qtip = false;
- var event = info.event;
- var revertFunc = info.revert;
- GLPIPlanning.editEventTimes(event, revertFunc);
+ GLPIPlanning.editEventTimes(info);
},
eventClick: function(info) {
var event = info.event;
@@ -246,7 +301,7 @@ var GLPIPlanning = {
width: 'auto',
height: 'auto',
close: function() {
- calendar.refetchEvents();
+ GLPIPlanning.refresh();
}
})
.load(ajaxurl, function() {
@@ -258,10 +313,19 @@ var GLPIPlanning = {
// ADD EVENTS
selectable: true,
- /*selectHelper: function(start, end) {
- return $('').text(start+' '+end);
- },*/ // doesn't work anymore: see https://github.com/fullcalendar/fullcalendar/issues/2832
select: function(info) {
+ var itemtype = (((((info || {})
+ .resource || {})
+ ._resource || {})
+ .extendedProps || {})
+ .itemtype || {});
+
+ // prevent adding events on group users
+ if (itemtype === 'Group_User') {
+ GLPIPlanning.calendar.unselect();
+ return false;
+ }
+
var start = info.start;
var end = info.end;
$('').dialog({
@@ -281,6 +345,10 @@ var GLPIPlanning = {
}
);
},
+ close: function() {
+ $(this).dialog("close");
+ $(this).remove();
+ },
position: {
my: 'center',
at: 'center',
@@ -288,13 +356,13 @@ var GLPIPlanning = {
}
});
- calendar.unselect();
+ GLPIPlanning.calendar.unselect();
}
});
var loadedLocales = Object.keys(FullCalendarLocales);
if (loadedLocales.length === 1) {
- calendar.setOption('locale', loadedLocales[0]);
+ GLPIPlanning.calendar.setOption('locale', loadedLocales[0]);
}
$('.planning_on_central a')
@@ -313,8 +381,8 @@ var GLPIPlanning = {
window_focused = true;
};
- window.calendar = calendar; // Required as object is not accessible by forms callback
- calendar.render();
+ //window.calendar = calendar; // Required as object is not accessible by forms callback
+ GLPIPlanning.calendar.render();
// attach button (planning and refresh) in planning header
$('#'+dom_id+' .fc-toolbar .fc-center h2')
@@ -325,16 +393,230 @@ var GLPIPlanning = {
);
$('#refresh_planning').click(function() {
- calendar.refetchEvents();
+ GLPIPlanning.refresh();
});
// attach the date picker to planning
GLPIPlanning.initFCDatePicker();
},
+ refresh: function() {
+ if (typeof(GLPIPlanning.calendar.refetchResources) == 'function') {
+ GLPIPlanning.calendar.refetchResources();
+ }
+ GLPIPlanning.calendar.refetchEvents();
+ window.displayAjaxMessageAfterRedirect();
+ },
+
+ // add/remove resource (like when toggling it in side bar)
+ toggleResource: function(res_name, active) {
+ // find the index of current resource to find it in our array of visible resources
+ var index = GLPIPlanning.all_resources.findIndex(function(current) {
+ return current.id == res_name;
+ });
+
+ if (index !== -1) {
+ // add only if not already present
+ if (active && GLPIPlanning.visible_res.indexOf(index.toString()) === -1) {
+ GLPIPlanning.visible_res.push(index.toString());
+ } else if (!active) {
+ GLPIPlanning.visible_res.splice(GLPIPlanning.visible_res.indexOf(index.toString()), 1);
+ }
+ }
+ },
+
+ setEndofDays: function(view) {
+ // add a class to last col of day in timeline view
+ // to visualy separate days
+ if (view.constructor.name === "ResourceTimelineView") {
+ // compute the number of hour slots displayed
+ var time_beg = CFG_GLPI.planning_begin.split(':');
+ var time_end = CFG_GLPI.planning_end.split(':');
+ var int_beg = parseInt(time_beg[0]) * 60 + parseInt(time_beg[1]);
+ var int_end = parseInt(time_end[0]) * 60 + parseInt(time_end[1]);
+ var sec_inter = int_end - int_beg;
+ var nb_slots = Math.ceil(sec_inter / 60);
+
+ // add class to day list header
+ $('#planning .fc-time-area.fc-widget-header table tr:nth-child(2) th')
+ .addClass('end-of-day');
+
+ // add class to hours list header
+ $('#planning .fc-time-area.fc-widget-header table tr:nth-child(3) th:nth-child('+nb_slots+'n)')
+ .addClass('end-of-day');
+
+ // add class to content bg (content slots)
+ $('#planning .fc-time-area.fc-widget-content table td:nth-child('+nb_slots+'n)')
+ .addClass('end-of-day');
+ }
+ },
+ planningFilters: function() {
+ $('#planning_filter a.planning_add_filter' ).on( 'click', function( e ) {
+ e.preventDefault(); // to prevent change of url on anchor
+ var url = $(this).attr('href');
+ $(' ').dialog({
+ modal: true,
+ open: function () {
+ $(this).load(url);
+ },
+ position: {
+ my: 'top',
+ at: 'center',
+ of: $('#planning_filter')
+ }
+ });
+ });
+
+ $('#planning_filter .filter_option').on( 'click', function() {
+ $(this).children('ul').toggle();
+ });
+
+ $(document).click(function(e){
+ if ($(e.target).closest('#planning_filter .filter_option').length === 0) {
+ $('#planning_filter .filter_option ul').hide();
+ }
+ });
+
+ $('#planning_filter .delete_planning').on( 'click', function() {
+ var deleted = $(this);
+ var li = deleted.closest('ul.filters > li');
+ $.ajax({
+ url: CFG_GLPI.root_doc+"/ajax/planning.php",
+ type: 'POST',
+ data: {
+ action: 'delete_filter',
+ filter: deleted.attr('value'),
+ type: li.attr('event_type')
+ },
+ success: function() {
+ li.remove();
+ GLPIPlanning.refresh();
+ }
+ });
+ });
+
+ var sendDisplayEvent = function(current_checkbox, refresh_planning) {
+ var current_li = current_checkbox.parents('li');
+ var parent_name = null;
+ if (current_li.parent('ul.group_listofusers').length == 1) {
+ parent_name = current_li
+ .parent('ul.group_listofusers')
+ .parent('li')
+ .attr('event_name');
+ }
+ var event_name = current_li.attr('event_name');
+ var event_type = current_li.attr('event_type');
+ var checked = current_checkbox.is(':checked');
+
+ return $.ajax({
+ url: CFG_GLPI.root_doc+"/ajax/planning.php",
+ type: 'POST',
+ data: {
+ action: 'toggle_filter',
+ name: event_name,
+ type: event_type,
+ parent: parent_name,
+ display: checked
+ },
+ success: function() {
+ GLPIPlanning.toggleResource(event_name, checked);
+
+ if (refresh_planning) {
+ // don't refresh planning if event triggered from parent checkbox
+ GLPIPlanning.refresh();
+ }
+ }
+ });
+ };
+
+ $('#planning_filter li:not(li.group_users) input[type="checkbox"]')
+ .on( 'click', function() {
+ sendDisplayEvent($(this), true);
+ });
+
+ $('#planning_filter li.group_users > span > input[type="checkbox"]')
+ .on('change', function() {
+ var parent_checkbox = $(this);
+ var parent_li = parent_checkbox.parents('li');
+ var checked = parent_checkbox.prop('checked');
+ var event_name = parent_li.attr('event_name');
+ var chidren_checkboxes = parent_checkbox
+ .parents('li.group_users')
+ .find('ul.group_listofusers input[type="checkbox"]');
+ chidren_checkboxes.prop('checked', checked);
+ var promises = [];
+ chidren_checkboxes.each(function() {
+ promises.push(sendDisplayEvent($(this), false));
+ });
+
+ GLPIPlanning.toggleResource(event_name, checked);
+
+ // refresh planning once for all checkboxes (and not for each)
+ // after theirs promises done
+ $.when.apply($, promises).then(function() {
+ GLPIPlanning.refresh();
+ });
+ });
+
+ $('#planning_filter .color_input input').on('change', function(e, color) {
+ var current_li = $(this).parents('li');
+ var parent_name = null;
+ if (current_li.length >= 1) {
+ parent_name = current_li.eq(1).attr('event_name');
+ current_li = current_li.eq(0);
+ }
+ $.ajax({
+ url: CFG_GLPI.root_doc+"/ajax/planning.php",
+ type: 'POST',
+ data: {
+ action: 'color_filter',
+ name: current_li.attr('event_name'),
+ type: current_li.attr('event_type'),
+ parent: parent_name,
+ color: color.toHexString()
+ },
+ success: function() {
+ GLPIPlanning.refresh();
+ }
+ });
+ });
+
+ $('#planning_filter li.group_users .toggle').on('click', function() {
+ $(this).parent().toggleClass('expanded');
+ });
+
+ $('#planning_filter_toggle > a.toggle').on('click', function() {
+ $('#planning_filter_content').animate({ width:'toggle' }, 300, 'swing', function() {
+ $('#planning_filter').toggleClass('folded');
+ $('#planning_container').toggleClass('folded');
+ });
+ });
+ },
+
// send ajax for event storage (on event drag/resize)
- editEventTimes: function(event, revertFunc) {
- var extProps = event.extendedProps;
+ editEventTimes: function(info) {
+ var event = info.event;
+ var revertFunc = info.revert;
+ var extProps = event.extendedProps;
+
+ var old_itemtype = null;
+ var old_items_id = null;
+ var new_itemtype = null;
+ var new_items_id = null;
+
+ // manage moving the events between resources (users, groups)
+ if ("newResource" in info
+ && info.newResource !== null) {
+ var new_extProps = info.newResource._resource.extendedProps;
+ new_itemtype = new_extProps.itemtype;
+ new_items_id = new_extProps.items_id;
+ }
+ if ("oldResource" in info
+ && info.oldResource !== null) {
+ var old_extProps = info.oldResource._resource.extendedProps;
+ old_itemtype = old_extProps.itemtype;
+ old_items_id = old_extProps.items_id;
+ }
var start = event.start;
var end = event.end;
@@ -351,18 +633,21 @@ var GLPIPlanning = {
url: CFG_GLPI.root_doc+"/ajax/planning.php",
type: 'POST',
data: {
- action: 'update_event_times',
- start: start.toISOString(),
- end: end.toISOString(),
- itemtype: extProps.itemtype,
- items_id: extProps.items_id
+ action: 'update_event_times',
+ start: start.toISOString(),
+ end: end.toISOString(),
+ itemtype: extProps.itemtype,
+ items_id: extProps.items_id,
+ new_actor_itemtype: new_itemtype,
+ new_actor_items_id: new_items_id,
+ old_actor_itemtype: old_itemtype,
+ old_actor_items_id: old_items_id,
},
success: function(html) {
if (!html) {
revertFunc();
}
- calendar.refetchEvents();
- window.displayAjaxMessageAfterRedirect();
+ GLPIPlanning.refresh();
},
error: function() {
revertFunc();
@@ -381,7 +666,7 @@ var GLPIPlanning = {
dateFormat: 'DD, d MM, yy',
onSelect: function() {
var selected_date = $(this).datepicker('getDate');
- calendar.gotoDate(selected_date);
+ GLPIPlanning.calendar.gotoDate(selected_date);
}
}).next('.ui-datepicker-trigger').addClass('pointer');
diff --git a/lib/bundles/cytoscape.js b/lib/bundles/cytoscape.js
new file mode 100644
index 00000000000..7bffa8fe28c
--- /dev/null
+++ b/lib/bundles/cytoscape.js
@@ -0,0 +1,38 @@
+/**
+ * ---------------------------------------------------------------------
+ * GLPI - Gestionnaire Libre de Parc Informatique
+ * Copyright (C) 2015-2018 Teclib' and contributors.
+ *
+ * http://glpi-project.org
+ *
+ * based on GLPI - Gestionnaire Libre de Parc Informatique
+ * Copyright (C) 2003-2014 by the INDEPNET Development Team.
+ *
+ * ---------------------------------------------------------------------
+ *
+ * LICENSE
+ *
+ * This file is part of GLPI.
+ *
+ * GLPI is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * GLPI is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with GLPI. If not, see .
+ * ---------------------------------------------------------------------
+ */
+
+// Cytoscape.js lib
+window.cytoscape = require('cytoscape');
+window._ = require('lodash');
+require('cytoscape-context-menus');
+require('cytoscape-context-menus/cytoscape-context-menus.css');
+require('cytoscape-grid-guide');
+require('cytoscape-dagre');
diff --git a/lib/bundles/fullcalendar.js b/lib/bundles/fullcalendar.js
index 7d8ba8a4e51..8c33fb0eb3e 100644
--- a/lib/bundles/fullcalendar.js
+++ b/lib/bundles/fullcalendar.js
@@ -28,6 +28,8 @@
* along with GLPI. If not, see .
* ---------------------------------------------------------------------
*/
+// RRule dependency
+window.rrule = require('rrule');
// Fullcalendar library
require('@fullcalendar/core');
@@ -35,6 +37,7 @@ require('@fullcalendar/daygrid');
require('@fullcalendar/interaction');
require('@fullcalendar/list');
require('@fullcalendar/timegrid');
+require('@fullcalendar/rrule');
require('@fullcalendar/core/main.css');
require('@fullcalendar/daygrid/main.css');
diff --git a/package-lock.json b/package-lock.json
index 77a8d8dca6b..74bb95c3464 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -48,6 +48,11 @@
"resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-4.3.0.tgz",
"integrity": "sha512-Al+XaKynC4RsJpXyWOWJgrVQXFOSC+fg8DI4HGAt/mrrwazZ1L0pTQwCLeU8auZp53VFazCiNxV+jkAbuDdbDQ=="
},
+ "@fullcalendar/rrule": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@fullcalendar/rrule/-/rrule-4.3.0.tgz",
+ "integrity": "sha512-80nPrIW0hA/vtT2VcDzSzk2yQ/bqdJB4zCiVhrGAg3+DtVxnormElTQQg84Iw1q1WeouT9lo2FYzG9B4SIDQEw=="
+ },
"@fullcalendar/timegrid": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-4.3.0.tgz",
@@ -1251,6 +1256,45 @@
"integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=",
"dev": true
},
+ "cytoscape": {
+ "version": "3.7.3",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.7.3.tgz",
+ "integrity": "sha512-70WU7ZITYnxPFjorGe3Oa1SPY9Qa4ZZyi1k2F67VeSxx8X4ReoWBfIMt5ogZkVUC5dMxOV7hbFyrcJJ86wR7LA==",
+ "requires": {
+ "heap": "^0.2.6",
+ "lodash.debounce": "^4.0.8"
+ }
+ },
+ "cytoscape-context-menus": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/cytoscape-context-menus/-/cytoscape-context-menus-3.0.6.tgz",
+ "integrity": "sha512-eRsAIg/HYOZOUfTvBK4L0t9ikdRuQULoB8ilB8AZ9D7zGfRjQip/quDH/H20n3IFMe6pb9XYAwVMHWZtlMjR1w=="
+ },
+ "cytoscape-dagre": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/cytoscape-dagre/-/cytoscape-dagre-2.2.2.tgz",
+ "integrity": "sha512-zsg36qNwua/L2stJSWkcbSDcvW3E6VZf6KRe6aLnQJxuXuz89tMqI5EVYVKEcNBgzTEzFMFv0PE3T0nD4m6VDw==",
+ "requires": {
+ "dagre": "^0.8.2"
+ }
+ },
+ "cytoscape-grid-guide": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/cytoscape-grid-guide/-/cytoscape-grid-guide-2.2.1.tgz",
+ "integrity": "sha512-zJFImEcvd6ubHBlKdAQwmP5qOvYg2u2brwbvadSc9fM2N7WDcYol6d/wKvsLm+g+ixgIhglKKC2X1yEGvQiPeA==",
+ "requires": {
+ "functional-red-black-tree": "^1.0.1"
+ }
+ },
+ "dagre": {
+ "version": "0.8.4",
+ "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.4.tgz",
+ "integrity": "sha512-Dj0csFDrWYKdavwROb9FccHfTC4fJbyF/oJdL9LNZJ8WUvl968P6PAKEriGqfbdArVJEmmfA+UyumgWEwcHU6A==",
+ "requires": {
+ "graphlib": "^2.1.7",
+ "lodash": "^4.17.4"
+ }
+ },
"date-now": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
@@ -2605,8 +2649,7 @@
"functional-red-black-tree": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
- "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
- "dev": true
+ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc="
},
"fuzzy": {
"version": "0.1.3",
@@ -2747,6 +2790,14 @@
"integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==",
"dev": true
},
+ "graphlib": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.7.tgz",
+ "integrity": "sha512-TyI9jIy2J4j0qgPmOOrHTCtpPqJGN/aurBwc6ZT+bRii+di1I+Wv3obRhVrmBEXet+qkMaEX67dXrwsd3QQM6w==",
+ "requires": {
+ "lodash": "^4.17.5"
+ }
+ },
"gridstack": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/gridstack/-/gridstack-0.4.0.tgz",
@@ -2822,6 +2873,11 @@
"minimalistic-assert": "^1.0.1"
}
},
+ "heap": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.6.tgz",
+ "integrity": "sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw="
+ },
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@@ -3369,6 +3425,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
+ "lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
+ },
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -3378,6 +3439,12 @@
"yallist": "^3.0.2"
}
},
+ "luxon": {
+ "version": "1.17.2",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.17.2.tgz",
+ "integrity": "sha512-qELKtIj3HD41N+MvgoxArk8DZGUb4Gpiijs91oi+ZmKJzRlxY6CoyTwNoUwnogCVs4p8HuxVJDik9JbnYgrCng==",
+ "optional": true
+ },
"make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
@@ -4447,6 +4514,15 @@
"inherits": "^2.0.1"
}
},
+ "rrule": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.6.2.tgz",
+ "integrity": "sha512-xL38CM1zOYOIp4OO8hdD6zHH5UdR9siHMvPiv+CCSh7o0LYJ0owg87QcFW7GXJ0PfpLBHjanEMvvBjJxbRhAcQ==",
+ "requires": {
+ "luxon": "^1.3.3",
+ "tslib": "^1.9.0"
+ }
+ },
"run-async": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
@@ -5196,8 +5272,7 @@
"tslib": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
- "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
- "dev": true
+ "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
},
"tty-browserify": {
"version": "0.0.0",
diff --git a/package.json b/package.json
index 0b5387975d0..a97bc72edd6 100644
--- a/package.json
+++ b/package.json
@@ -11,10 +11,15 @@
"@fullcalendar/interaction": "^4.3.0",
"@fullcalendar/list": "^4.3.0",
"@fullcalendar/timegrid": "^4.3.0",
+ "@fullcalendar/rrule": "^4.3.0",
"chartist": "^0.10.1",
"chartist-plugin-legend": "^0.6.2",
"chartist-plugin-tooltips": "^0.0.17",
"codemirror": "^5.48.4",
+ "cytoscape": "^3.7.3",
+ "cytoscape-dagre": "^2.2.1",
+ "cytoscape-context-menus": "^3.0.6",
+ "cytoscape-grid-guide": "^2.2.1",
"diff-match-patch": "^1.0.4",
"fuzzy": "^0.1.3",
"gridstack": "^0.4.0",
@@ -36,6 +41,7 @@
"photoswipe": "^4.1.3",
"prismjs": "^1.17.1",
"qtip2": "^3.0.3",
+ "rrule": "^2.6.0",
"select2": "^4.0.10",
"spectrum-colorpicker": "^1.8.0",
"spin.js": "^2.3.2",
diff --git a/pics/impact/README.txt b/pics/impact/README.txt
new file mode 100644
index 00000000000..56eab151ef1
--- /dev/null
+++ b/pics/impact/README.txt
@@ -0,0 +1,2 @@
+Icons made by Smashicons : https://www.flaticon.com/authors/smashicons
+www.flaticon.com is licensed by "CC 3.0 BY" : http://creativecommons.org/licenses/by/3.0/
diff --git a/pics/impact/computer.png b/pics/impact/computer.png
new file mode 100644
index 00000000000..570303090cf
Binary files /dev/null and b/pics/impact/computer.png differ
diff --git a/pics/impact/dcroom.png b/pics/impact/dcroom.png
new file mode 100644
index 00000000000..50aad2f5ba4
Binary files /dev/null and b/pics/impact/dcroom.png differ
diff --git a/pics/impact/default.png b/pics/impact/default.png
new file mode 100644
index 00000000000..18a7cfa1ed8
Binary files /dev/null and b/pics/impact/default.png differ
diff --git a/pics/impact/enclosure.png b/pics/impact/enclosure.png
new file mode 100644
index 00000000000..23991ca798a
Binary files /dev/null and b/pics/impact/enclosure.png differ
diff --git a/pics/impact/monitor.png b/pics/impact/monitor.png
new file mode 100644
index 00000000000..90e9bd729f2
Binary files /dev/null and b/pics/impact/monitor.png differ
diff --git a/pics/impact/networkequipment.png b/pics/impact/networkequipment.png
new file mode 100644
index 00000000000..33658037ba0
Binary files /dev/null and b/pics/impact/networkequipment.png differ
diff --git a/pics/impact/peripheral.png b/pics/impact/peripheral.png
new file mode 100644
index 00000000000..ccd108c9682
Binary files /dev/null and b/pics/impact/peripheral.png differ
diff --git a/pics/impact/phone.png b/pics/impact/phone.png
new file mode 100644
index 00000000000..5a8a2fdc084
Binary files /dev/null and b/pics/impact/phone.png differ
diff --git a/pics/impact/printer.png b/pics/impact/printer.png
new file mode 100644
index 00000000000..6e218993225
Binary files /dev/null and b/pics/impact/printer.png differ
diff --git a/pics/impact/rack.png b/pics/impact/rack.png
new file mode 100644
index 00000000000..350107b816e
Binary files /dev/null and b/pics/impact/rack.png differ
diff --git a/pics/impact/software.png b/pics/impact/software.png
new file mode 100644
index 00000000000..3e6371ae0af
Binary files /dev/null and b/pics/impact/software.png differ
diff --git a/tests/DbTestCase.php b/tests/DbTestCase.php
index 029c0c79ec7..f662e0d2dd5 100644
--- a/tests/DbTestCase.php
+++ b/tests/DbTestCase.php
@@ -81,7 +81,7 @@ protected function setEntity($entityname, $subtree) {
* @param int $id The id of added object
* @param array $input the input used for add object (optionnal)
*
- * @return nothing (do tests)
+ * @return void
*/
protected function checkInput(CommonDBTM $object, $id = 0, $input = []) {
$this->integer((int)$id)->isGreaterThan(0);
diff --git a/tests/abstracts/AbstractPlanningEvent.php b/tests/abstracts/AbstractPlanningEvent.php
new file mode 100644
index 00000000000..43836867055
--- /dev/null
+++ b/tests/abstracts/AbstractPlanningEvent.php
@@ -0,0 +1,139 @@
+.
+ * ---------------------------------------------------------------------
+*/
+
+abstract class AbstractPlanningEvent extends \DbTestCase {
+ protected $myclass = "";
+ protected $input = [];
+
+ private $begin = "";
+ private $end = "";
+ private $duration = "";
+
+ public function beforeTestMethod($method) {
+ parent::beforeTestMethod($method);
+
+ $now = time();
+ $this->duration = 2 * \HOUR_TIMESTAMP;
+ $this->begin = date('Y-m-d H:i:s', $now);
+ $this->end = date('Y-m-d H:i:s', $now + $this->duration);
+
+ $this->input = [
+ 'name' => 'test add external event',
+ 'test' => 'comment for external event',
+ 'plan' => [
+ 'begin' => $this->begin,
+ '_duration' => $this->duration,
+ ],
+ 'rrule' => [
+ 'freq' => 'daily',
+ 'interval' => 1,
+ 'byweekday' => 'MO',
+ 'bymonth' => 1,
+ ],
+ 'state' => \Planning::TODO,
+ 'background' => 1,
+ ];
+ }
+
+ public function testAdd() {
+ $event = new $this->myclass;
+ $id = $event->add($this->input);
+
+ $this->integer((int) $id)->isGreaterThan(0);
+ $this->boolean($event->getFromDB($id))->isTrue();
+
+ // check end date
+ if (isset($event->fields['end'])) {
+ $this->string($event->fields['end'])->isEqualTo($this->end);
+ }
+
+ // check rrule encoding
+ $this->string($event->fields['rrule'])
+ ->isEqualTo('{"freq":"daily","interval":1,"byweekday":"MO","bymonth":1}');
+
+ return $event;
+ }
+
+
+ public function testUpdate() {
+ $this->login();
+
+ $event = new $this->myclass;
+ $id = $event->add($this->input);
+
+ $new_begin = date("Y-m-d H:i:s", strtotime($this->begin) + $this->duration);
+ $new_end = date("Y-m-d H:i:s", strtotime($this->end) + $this->duration);
+
+ $update = array_merge($this->input, [
+ 'id' => $id,
+ 'name' => 'updated external event',
+ 'test' => 'updated comment for external event',
+ 'plan' => [
+ 'begin' => $new_begin,
+ '_duration' => $this->duration,
+ ],
+ 'rrule' => [
+ 'freq' => 'monthly',
+ 'interval' => 2,
+ 'byweekday' => 'TU',
+ 'bymonth' => 2,
+ ],
+ 'state' => \Planning::INFO,
+ 'background' => 0,
+ ]);
+ $this->boolean($event->update($update))->isTrue();
+
+ // check dates (we added duration to both dates on update)
+ if (isset($event->fields['begin'])) {
+ $this->string($event->fields['begin'])
+ ->isEqualTo($new_begin);
+ }
+ if (isset($event->fields['end'])) {
+ $this->string($event->fields['end'])
+ ->isEqualTo($new_end);
+ }
+
+ // check rrule encoding
+ $this->string($event->fields['rrule'])
+ ->isEqualTo('{"freq":"monthly","interval":2,"byweekday":"TU","bymonth":2}');
+ }
+
+
+ public function testDelete() {
+ $event = new $this->myclass;
+ $id = $event->add($this->input);
+
+ $this->boolean($event->delete([
+ 'id' => $id,
+ ]))->isTrue();
+ }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 69df7362899..9e09791ae1f 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -30,6 +30,7 @@
* ---------------------------------------------------------------------
*/
+ini_set('display_errors', 'On');
error_reporting(E_ALL);
define('GLPI_CACHE_DIR', __DIR__ . '/files/_cache');
@@ -475,7 +476,7 @@ function loadDataset() {
], 'AuthLdap' => [
[
'name' => '_local_ldap',
- 'host' => '127.0.0.1',
+ 'host' => 'openldap',
'basedn' => 'dc=glpi,dc=org',
'rootdn' => 'cn=Manager,dc=glpi,dc=org',
'port' => '3890',
@@ -497,8 +498,7 @@ function loadDataset() {
'group_condition' => '(objectclass=groupOfNames)',
'group_member_field' => 'member'
]
- ]
-
+ ],
];
// To bypass various right checks
@@ -576,7 +576,7 @@ function loadDataset() {
* @param string $type
* @param string $name
* @param boolean $onlyid
- * @return the item, or its id
+ * @return CommonGLPI|false the item, or its id
*/
function getItemByTypeName($type, $name, $onlyid = false) {
diff --git a/tests/functionnal/Impact.class.php b/tests/functionnal/Impact.class.php
new file mode 100644
index 00000000000..e5fdd4ae0c6
--- /dev/null
+++ b/tests/functionnal/Impact.class.php
@@ -0,0 +1,283 @@
+.
+ * ---------------------------------------------------------------------
+*/
+
+namespace tests\units;
+
+class Impact extends \DbTestCase {
+
+ public function testGetTabNameForItem_notCommonDBTM() {
+ $impact = new \Impact();
+
+ $this->exception(function() use ($impact) {
+ $notCommonDBTM = new \Impact();
+ $impact->getTabNameForItem($notCommonDBTM);
+ })->isInstanceOf(\InvalidArgumentException::class);
+ }
+
+ public function testGetTabNameForItem_notEnabledOrITIL() {
+ $impact = new \Impact();
+
+ $this->exception(function() use ($impact) {
+ $notEnabledOrITIL = new \ImpactCompound();
+ $impact->getTabNameForItem($notEnabledOrITIL);
+ })->isInstanceOf(\InvalidArgumentException::class);
+ }
+
+ public function testGetTabNameForItem_tabCountDisabled() {
+ $oldSession = $_SESSION['glpishow_count_on_tabs'];
+ $_SESSION['glpishow_count_on_tabs'] = false;
+
+ $impact = new \Impact();
+ $computer = new \Computer();
+ $tab_name = $impact->getTabNameForItem($computer);
+ $_SESSION['glpishow_count_on_tabs'] = $oldSession;
+
+ $this->string($tab_name)->isEqualTo("Impact analysis");
+ }
+
+ public function testGetTabNameForItem_enabledAsset() {
+ $oldSession = $_SESSION['glpishow_count_on_tabs'];
+ $_SESSION['glpishow_count_on_tabs'] = true;
+
+ $impact = new \Impact();
+ $impactRelationManager = new \ImpactRelation();
+
+ // Get computers
+ $computer1 = getItemByTypeName('Computer', '_test_pc01');
+ $computer2 = getItemByTypeName('Computer', '_test_pc02');
+ $computer3 = getItemByTypeName('Computer', '_test_pc03');
+
+ // Create an impact graph
+ $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer1->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer2->fields['id'],
+ ]);
+ $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer2->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer3->fields['id'],
+ ]);
+ $tab_name = $impact->getTabNameForItem($computer2);
+ $_SESSION['glpishow_count_on_tabs'] = $oldSession;
+
+ $this->string($tab_name)->isEqualTo("Impact analysis 2");
+ }
+
+ public function testGetTabNameForItem_ITILObject() {
+ $oldSession = $_SESSION['glpishow_count_on_tabs'];
+ $_SESSION['glpishow_count_on_tabs'] = true;
+
+ $impact = new \Impact();
+ $impactRelationManager = new \ImpactRelation();
+ $ticketManager = new \Ticket();
+ $itemTicketManger = new \Item_Ticket();
+
+ // Get computers
+ $computer1 = getItemByTypeName('Computer', '_test_pc01');
+ $computer2 = getItemByTypeName('Computer', '_test_pc02');
+ $computer3 = getItemByTypeName('Computer', '_test_pc03');
+
+ // Create an impact graph
+ $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer1->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer2->fields['id'],
+ ]);
+ $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer2->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer3->fields['id'],
+ ]);
+
+ // Create a ticket and link it to the computer
+ $ticketId = $ticketManager->add(['name' => "test", 'content' => "test"]);
+ $itemTicketManger->add([
+ 'itemtype' => "Computer",
+ 'items_id' => $computer2->fields['id'],
+ 'tickets_id' => $ticketId,
+ ]);
+
+ // Get the actual ticket
+ $ticket = new \Ticket;
+ $ticket->getFromDB($ticketId);
+
+ $tab_name = $impact->getTabNameForItem($ticket);
+ $_SESSION['glpishow_count_on_tabs'] = $oldSession;
+
+ $this->string($tab_name)->isEqualTo("Impact analysis");
+ }
+
+ public function testBuildGraph_empty() {
+ $computer = getItemByTypeName('Computer', '_test_pc01');
+ $graph = \Impact::buildGraph($computer);
+
+ $this->array($graph)->hasKeys(["nodes", "edges"]);
+
+ // Nodes should contain only _test_pc01
+ $id = $computer->fields['id'];
+ $this->array($graph["nodes"])->hasSize(1);
+ $this->string($graph["nodes"]["Computer::$id"]['label'])->isEqualTo("_test_pc01");
+
+ // Edges should be empty
+ $this->array($graph["edges"])->hasSize(0);
+ }
+
+ public function testBuildGraph_complex() {
+ $impactRelationManager = new \ImpactRelation();
+ $impactItemManager = new \ImpactItem();
+ $impactCompoundManager = new \ImpactCompound();
+
+ $computer1 = getItemByTypeName('Computer', '_test_pc01');
+ $computer2 = getItemByTypeName('Computer', '_test_pc02');
+ $computer3 = getItemByTypeName('Computer', '_test_pc03');
+ $computer4 = getItemByTypeName('Computer', '_test_pc11');
+ $computer5 = getItemByTypeName('Computer', '_test_pc12');
+ $computer6 = getItemByTypeName('Computer', '_test_pc13');
+
+ // Set compounds
+ $compound01Id = $impactCompoundManager->add([
+ 'name' => "_test_compound01",
+ 'color' => "#000011",
+ ]);
+ $compound02Id = $impactCompoundManager->add([
+ 'name' => "_test_compound02",
+ 'color' => "#110000",
+ ]);
+
+ // Set impact items
+ $impactItemManager->add([
+ 'itemtype' => "Computer",
+ 'items_id' => $computer1->fields['id'],
+ 'parent_id' => 0,
+ ]);
+ $impactItemManager->add([
+ 'itemtype' => "Computer",
+ 'items_id' => $computer2->fields['id'],
+ 'parent_id' => $compound01Id,
+ ]);
+ $impactItemManager->add([
+ 'itemtype' => "Computer",
+ 'items_id' => $computer3->fields['id'],
+ 'parent_id' => $compound01Id,
+ ]);
+ $impactItemManager->add([
+ 'itemtype' => "Computer",
+ 'items_id' => $computer4->fields['id'],
+ 'parent_id' => $compound02Id,
+ ]);
+ $impactItemManager->add([
+ 'itemtype' => "Computer",
+ 'items_id' => $computer5->fields['id'],
+ 'parent_id' => $compound02Id,
+ ]);
+ $impactItemManager->add([
+ 'itemtype' => "Computer",
+ 'items_id' => $computer6->fields['id'],
+ 'parent_id' => $compound02Id,
+ ]);
+
+ // Set relations
+ $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer1->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer2->fields['id'],
+ ]);
+ $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer2->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer3->fields['id'],
+ ]);
+ $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer3->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer4->fields['id'],
+ ]);
+ $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer4->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer5->fields['id'],
+ ]);
+ $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer2->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer6->fields['id'],
+ ]);
+ $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer6->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer2->fields['id'],
+ ]);
+
+ // Build graph from pc02
+ $computer = getItemByTypeName('Computer', '_test_pc02');
+ $graph = \Impact::buildGraph($computer);
+ // var_dump($graph);
+ $this->array($graph)->hasKeys(["nodes", "edges"]);
+
+ // Nodes should contain 8 elements (6 nodes + 2 compounds)
+ $this->array($graph["nodes"])->hasSize(8);
+ $nodes = array_filter($graph["nodes"], function($elem) {
+ return !isset($elem['color']);
+ });
+ $this->array($nodes)->hasSize(6);
+ $compounds = array_filter($graph["nodes"], function($elem) {
+ return isset($elem['color']);
+ });
+ $this->array($compounds)->hasSize(2);
+
+ // Edges should contain 6 elements (3 forward, 1 backward, 2 both)
+ $this->array($graph["edges"])->hasSize(6);
+ $backward = array_filter($graph["edges"], function($elem) {
+ return $elem["flag"] == \Impact::DIRECTION_BACKWARD;
+ });
+ $this->array($backward)->hasSize(1);
+ $forward = array_filter($graph["edges"], function($elem) {
+ return $elem["flag"] == \Impact::DIRECTION_FORWARD;
+ });
+ $this->array($forward)->hasSize(3);
+ $both = array_filter($graph["edges"], function($elem) {
+ return $elem["flag"] == (\Impact::DIRECTION_FORWARD | \Impact::DIRECTION_BACKWARD);
+ });
+ $this->array($both)->hasSize(2);
+ }
+}
\ No newline at end of file
diff --git a/tests/functionnal/ImpactItem.class.php b/tests/functionnal/ImpactItem.class.php
new file mode 100644
index 00000000000..a58030ccdbd
--- /dev/null
+++ b/tests/functionnal/ImpactItem.class.php
@@ -0,0 +1,56 @@
+.
+ * ---------------------------------------------------------------------
+*/
+
+namespace tests\units;
+
+class ImpactItem extends \DbTestCase {
+
+ public function testFindForItem_inexistent() {
+ $computer = getItemByTypeName('Computer', '_test_pc02');
+
+ $this->boolean(\ImpactItem::findForItem($computer))->isFalse();
+ }
+
+ public function testFindForItem_exist() {
+ $impactItemManager = new \ImpactItem();
+ $computer = getItemByTypeName('Computer', '_test_pc02');
+
+ $id = $impactItemManager->add([
+ 'itemtype' => "Computer",
+ 'items_id' => $computer->fields['id'],
+ 'parent_id' => 0,
+ ]);
+
+ $impactItem = \ImpactItem::findForItem($computer);
+ $this->integer((int) $impactItem->fields['id'])->isEqualTo($id);
+ }
+}
diff --git a/tests/functionnal/ImpactRelation.class.php b/tests/functionnal/ImpactRelation.class.php
new file mode 100644
index 00000000000..4d9a65a24fb
--- /dev/null
+++ b/tests/functionnal/ImpactRelation.class.php
@@ -0,0 +1,144 @@
+.
+ * ---------------------------------------------------------------------
+*/
+
+namespace tests\units;
+
+class ImpactRelation extends \DbTestCase {
+
+ public function testPrepareInputForAdd_requiredFields() {
+ $impactRelationManager = new \ImpactRelation();
+ $res = $impactRelationManager->add([]);
+
+ $this->boolean($res)->isFalse();
+ }
+
+ public function testPrepareInputForAdd_differentItems() {
+ $computer = getItemByTypeName('Computer', '_test_pc02');
+ $impactRelationManager = new \ImpactRelation();
+ $res = $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer->fields['id'],
+ ]);
+
+ $this->boolean($res)->isFalse();
+ }
+
+ public function testPrepareInputForAdd_duplicate() {
+ $computer1 = getItemByTypeName('Computer', '_test_pc01');
+ $computer2 = getItemByTypeName('Computer', '_test_pc02');
+ $impactRelationManager = new \ImpactRelation();
+
+ $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer1->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer2->fields['id'],
+ ]);
+
+ $res = $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer1->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer2->fields['id'],
+ ]);
+
+ $this->boolean($res)->isFalse();
+ }
+
+ public function testPrepareInputForAdd_assetExist() {
+ $impactRelationManager = new \ImpactRelation();
+
+ $res = $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => -40,
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => -78,
+ ]);
+
+ $this->boolean($res)->isFalse();
+ }
+
+ public function testPrepareInputForAdd_valid() {
+ $computer1 = getItemByTypeName('Computer', '_test_pc01');
+ $computer2 = getItemByTypeName('Computer', '_test_pc02');
+ $impactRelationManager = new \ImpactRelation();
+
+ $res = $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer1->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer2->fields['id'],
+ ]);
+
+ $this->integer($res);
+ }
+
+ public function testGetIDFromInput_invalid() {
+ $computer1 = getItemByTypeName('Computer', '_test_pc01');
+ $computer2 = getItemByTypeName('Computer', '_test_pc02');
+
+ $input = [
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer1->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer2->fields['id'],
+ ];
+
+ $id = \ImpactRelation::getIDFromInput($input);
+ $this->boolean($id)->isFalse();
+ }
+
+ public function testGetIDFromInput_valid() {
+ $computer1 = getItemByTypeName('Computer', '_test_pc01');
+ $computer2 = getItemByTypeName('Computer', '_test_pc02');
+ $impactRelationManager = new \ImpactRelation();
+
+ $id1 = $impactRelationManager->add([
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer1->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer2->fields['id'],
+ ]);
+
+ $input = [
+ 'itemtype_source' => "Computer",
+ 'items_id_source' => $computer1->fields['id'],
+ 'itemtype_impacted' => "Computer",
+ 'items_id_impacted' => $computer2->fields['id'],
+ ];
+
+ $id2 = \ImpactRelation::getIDFromInput($input);
+ $this->integer($id1)->isEqualTo($id2);
+ }
+}
diff --git a/tests/functionnal/NotificationMailing.php b/tests/functionnal/NotificationMailing.php
index 966edcca09f..a4ab1345028 100644
--- a/tests/functionnal/NotificationMailing.php
+++ b/tests/functionnal/NotificationMailing.php
@@ -38,6 +38,10 @@
class NotificationMailing extends DbTestCase {
+ /**
+ * @ignore
+ * @see https://gitlab.alpinelinux.org/alpine/aports/issues/7392
+ */
public function testCheck() {
$instance = new \NotificationMailing();
diff --git a/tests/functionnal/PlanningExternalEvent.php b/tests/functionnal/PlanningExternalEvent.php
new file mode 100644
index 00000000000..891a39d8255
--- /dev/null
+++ b/tests/functionnal/PlanningExternalEvent.php
@@ -0,0 +1,40 @@
+.
+ * ---------------------------------------------------------------------
+*/
+
+namespace tests\units;
+
+include_once __DIR__ . '/../abstracts/AbstractPlanningEvent.php';
+
+class PlanningExternalEvent extends \AbstractPlanningEvent {
+ public $myclass = "\PlanningExternalEvent";
+
+}
\ No newline at end of file
diff --git a/tests/functionnal/PlanningExternalEventTemplate.php b/tests/functionnal/PlanningExternalEventTemplate.php
new file mode 100644
index 00000000000..6d057bdfde3
--- /dev/null
+++ b/tests/functionnal/PlanningExternalEventTemplate.php
@@ -0,0 +1,58 @@
+.
+ * ---------------------------------------------------------------------
+*/
+
+namespace tests\units;
+
+include_once __DIR__ . '/../abstracts/AbstractPlanningEvent.php';
+
+class PlanningExternalEventTemplate extends \AbstractPlanningEvent {
+ protected $myclass = "\PlanningExternalEventTemplate";
+
+ public function beforeTestMethod($method) {
+ parent::beforeTestMethod($method);
+
+ $this->input = array_merge($this->input, [
+ '_planningrecall' => [
+ 'before_time' => 2 * \HOUR_TIMESTAMP,
+ ],
+ ]);
+ }
+
+ public function testAdd() {
+ $event = parent::testAdd();
+
+ $this->integer((int) $event->fields['before_time'])
+ ->isEqualTo(2 * \HOUR_TIMESTAMP);
+ $this->integer((int) $event->fields['duration'])
+ ->isEqualTo(2 * \HOUR_TIMESTAMP);
+ }
+}
diff --git a/tests/functionnal/Search.php b/tests/functionnal/Search.php
index 19171ef5fea..79871b18600 100644
--- a/tests/functionnal/Search.php
+++ b/tests/functionnal/Search.php
@@ -88,7 +88,7 @@ private function doSearch($itemtype, $params, array $forcedisplay = []) {
private function getClasses($function = false, array $excludes = []) {
$classes = [];
foreach (new \DirectoryIterator('inc/') as $fileInfo) {
- if ($fileInfo->isDot()) {
+ if (!$fileInfo->isFile()) {
continue;
}
diff --git a/tests/units/Html.php b/tests/units/Html.php
index 08136890414..c785e8bb80a 100644
--- a/tests/units/Html.php
+++ b/tests/units/Html.php
@@ -365,7 +365,8 @@ public function testGetMenuInfos() {
'ReservationItem',
'Report',
'MigrationCleaner',
- 'SavedSearch'
+ 'SavedSearch',
+ 'Impact'
];
$this->string($menu['tools']['title'])->isIdenticalTo('Tools');
$this->array($menu['tools']['types'])->isIdenticalTo($expected);
diff --git a/tests/web/APIRest.php b/tests/web/APIRest.php
index 7e69dc889e7..3b0e1901cd7 100644
--- a/tests/web/APIRest.php
+++ b/tests/web/APIRest.php
@@ -215,6 +215,8 @@ public function testInitSessionUserToken() {
$data = json_decode($body, true);
$this->variable($data)->isNotFalse();
$this->array($data)->hasKey('session_token');
+ $this->array($data)->hasKey('users_id');
+ $this->integer((int) $data['users_id'])->isEqualTo($uid);
}
/**
diff --git a/tests/web/APIXmlrpc.php b/tests/web/APIXmlrpc.php
index 4b9c8f63499..12b649f7b13 100644
--- a/tests/web/APIXmlrpc.php
+++ b/tests/web/APIXmlrpc.php
@@ -106,6 +106,7 @@ protected function query($resource = "", $params = [], $expected_code = 200, $ex
* @covers API::initSession
*/
public function initSessionCredentials() {
+ $uid = getItemByTypeName('User', TU_USER, true);
$data = $this->query('initSession',
['query' => [
'login' => TU_USER,
@@ -114,6 +115,8 @@ public function initSessionCredentials() {
$this->variable($data)->isNotFalse();
$this->array($data)->hasKey('session_token');
$this->session_token = $data['session_token'];
+ $this->array($data)->hasKey('users_id');
+ $this->integer((int) $data['users_id'])->isEqualTo($uid);
}
/**
diff --git a/webpack.config.js b/webpack.config.js
index 75e0f58d0b7..d2754bf95dd 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -75,12 +75,14 @@ var libsConfig = {
rules: [
{
// Load scripts with no compilation for packages that are directly providing "dist" files.
- // This prevents useless compilation pass and can also
+ // This prevents useless compilation pass and can also
// prevents incompatibility issues with the webpack require feature.
test: /\.js$/,
include: [
path.resolve(__dirname, 'node_modules/@fullcalendar'),
path.resolve(__dirname, 'node_modules/codemirror'),
+ path.resolve(__dirname, 'node_modules/cystoscape'),
+ path.resolve(__dirname, 'node_modules/cytoscape-context-menus'),
path.resolve(__dirname, 'node_modules/gridstack'),
path.resolve(__dirname, 'node_modules/jstree'),
path.resolve(__dirname, 'node_modules/photoswipe'),
|