mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-24 02:58:43 +02:00
N°3531 - Activity panel: Restore possibility to load extra history entries asynchroniously
This commit is contained in:
@@ -71,9 +71,13 @@ class ActivityPanel extends UIBlock
|
||||
protected $aEntries;
|
||||
/** @var bool $bAreEntriesSorted True if the entries have been sorted by date */
|
||||
protected $bAreEntriesSorted;
|
||||
/** @var bool True if there are more entries to load asynchroniously */
|
||||
protected $bHasMoreEntriesToLoad;
|
||||
/** @var array IDs of the last loaded entries of each type, makes it easier to load the next entries asynchronioulsy */
|
||||
protected $aLastLoadedEntriesIds;
|
||||
/**
|
||||
* @var bool True if the host object has states (but not necessary a lifecycle)
|
||||
* @see MetaModel::HasStateAttributeCode()
|
||||
* @var bool True if the host object has states (but not necessary a lifecycle)
|
||||
*/
|
||||
protected $bHasStates;
|
||||
/** @var \Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\CaseLogEntryForm\CaseLogEntryForm[] $aCaseLogTabsEntryForms */
|
||||
@@ -101,6 +105,8 @@ class ActivityPanel extends UIBlock
|
||||
$this->SetObject($oObject);
|
||||
$this->SetEntries($aEntries);
|
||||
$this->bAreEntriesSorted = false;
|
||||
$this->bHasMoreEntriesToLoad = false;
|
||||
$this->aLastLoadedEntriesIds = [];
|
||||
$this->ComputedShowMultipleEntriesSubmitConfirmation();
|
||||
}
|
||||
|
||||
@@ -453,6 +459,52 @@ class ActivityPanel extends UIBlock
|
||||
return !empty($this->aEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see static::$bHasMoreEntriesToLoad
|
||||
*
|
||||
* @param bool $bHasMoreEntriesToLoad
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function SetHasMoreEntriesToLoad(bool $bHasMoreEntriesToLoad)
|
||||
{
|
||||
$this->bHasMoreEntriesToLoad = $bHasMoreEntriesToLoad;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see static::$bHasMoreEntriesToLoad
|
||||
* @return bool
|
||||
*/
|
||||
public function HasMoreEntriesToLoad(): bool
|
||||
{
|
||||
return $this->bHasMoreEntriesToLoad;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sEntryType Type of entry (eg. cmdbchangeop, caselog, notification)
|
||||
* @param string $sEntryId ID of the last loaded entry
|
||||
*
|
||||
* @return $this
|
||||
* @uses static::$aLastLoadedEntriesIds
|
||||
*/
|
||||
public function SetLastEntryId(string $sEntryType, string $sEntryId)
|
||||
{
|
||||
$this->aLastLoadedEntriesIds[$sEntryType] = $sEntryId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array Hash array of the last loaded entries
|
||||
* @uses static::$aLastLoadedEntriesIds
|
||||
*/
|
||||
public function GetLastEntryIds(): array
|
||||
{
|
||||
return $this->aLastLoadedEntriesIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the case log tabs metadata, not their entries
|
||||
*
|
||||
@@ -710,7 +762,7 @@ class ActivityPanel extends UIBlock
|
||||
* @return string The endpoint for all "lock" related operations
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function GetLockEndpointForJSWidget(): string
|
||||
public function GetLockEndpoint(): string
|
||||
{
|
||||
return utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
|
||||
}
|
||||
@@ -724,6 +776,15 @@ class ActivityPanel extends UIBlock
|
||||
return utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The endpoint to load the remaining entries
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function GetLoadMoreEntriesEndpoint(): string
|
||||
{
|
||||
return utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
||||
@@ -21,9 +21,7 @@ namespace Combodo\iTop\Application\UI\Base\Layout\ActivityPanel;
|
||||
|
||||
|
||||
use cmdbAbstractObject;
|
||||
use CMDBChangeOpSetAttributeCaseLog;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\ActivityEntryFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\EditsEntry;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\CaseLogEntryFormFactory\CaseLogEntryFormFactory;
|
||||
use DBObject;
|
||||
use DBObjectSearch;
|
||||
@@ -61,7 +59,7 @@ class ActivityPanelFactory
|
||||
public static function MakeForObjectDetails(DBObject $oObject, string $sMode = cmdbAbstractObject::DEFAULT_OBJECT_MODE)
|
||||
{
|
||||
$sObjClass = get_class($oObject);
|
||||
$iObjId = $oObject->GetKey();
|
||||
$sObjId = $oObject->GetKey();
|
||||
|
||||
if ($sMode == cmdbAbstractObject::ENUM_OBJECT_MODE_PRINT) {
|
||||
$oActivityPanel = new ActivityPanelPrint($oObject, [], ActivityPanel::BLOCK_CODE);
|
||||
@@ -90,48 +88,19 @@ class ActivityPanelFactory
|
||||
}
|
||||
|
||||
// Retrieve history changes (excluding case logs entries)
|
||||
// - Prepare query to retrieve changes
|
||||
$oChangesSearch = DBObjectSearch::FromOQL('SELECT CMDBChangeOp WHERE objclass = :obj_class AND objkey = :obj_key AND finalclass NOT IN (:excluded_optypes)');
|
||||
// Note: We can't order by date (only) as something multiple CMDBChangeOp rows are inserted at the same time (eg. Delivery model of the "Demo" Organization in the sample data).
|
||||
// As the DB returns rows "chronologically", we get the older first and it messes with the processing. Ordering by the ID is way much simpler and less DB CPU consuming.
|
||||
$oChangesSet = new DBObjectSet($oChangesSearch, ['id' => false], ['obj_class' => $sObjClass, 'obj_key' => $iObjId, 'excluded_optypes' => ['CMDBChangeOpSetAttributeCaseLog']]);
|
||||
$oChangesSet->SetLimit(MetaModel::GetConfig()->Get('max_history_length'));
|
||||
$aChangesData = ActivityPanelHelper::GetCMDBChangeOpEditsEntriesForObject($sObjClass, $sObjId);
|
||||
|
||||
// Prepare previous values to group edits within a same CMDBChange
|
||||
$iPreviousChangeId = 0;
|
||||
$oPreviousEditsEntry = null;
|
||||
|
||||
/** @var \CMDBChangeOp $oChangeOp */
|
||||
while ($oChangeOp = $oChangesSet->Fetch()) {
|
||||
// Skip case log changes as they are handled directly from the attributes themselves (most of them should have been excluded by the OQL above, but some derivated classes could still be retrieved)
|
||||
if ($oChangeOp instanceof CMDBChangeOpSetAttributeCaseLog) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make entry from CMDBChangeOp
|
||||
$iChangeId = $oChangeOp->Get('change');
|
||||
try {
|
||||
$oEntry = ActivityEntryFactory::MakeFromCmdbChangeOp($oChangeOp);
|
||||
}
|
||||
catch (Exception $oException) {
|
||||
IssueLog::Debug(static::class.': Could not create entry from CMDBChangeOp #'.$oChangeOp->GetKey().' related to '.$oChangeOp->Get('objclass').'::'.$oChangeOp->Get('objkey').': '.$oException->getMessage());
|
||||
continue;
|
||||
}
|
||||
// If same CMDBChange and mergeable edits entry from the same author, we merge them
|
||||
if (($iChangeId == $iPreviousChangeId) && ($oPreviousEditsEntry instanceof EditsEntry) && ($oEntry instanceof EditsEntry) && ($oPreviousEditsEntry->GetAuthorLogin() === $oEntry->GetAuthorLogin())) {
|
||||
$oPreviousEditsEntry->Merge($oEntry);
|
||||
} else {
|
||||
$oActivityPanel->AddEntry($oEntry);
|
||||
|
||||
// Set previous edits entry
|
||||
if ($oEntry instanceof EditsEntry) {
|
||||
$oPreviousEditsEntry = $oEntry;
|
||||
}
|
||||
}
|
||||
|
||||
$iPreviousChangeId = $iChangeId;
|
||||
// - Set metadata for pagination
|
||||
if (true === $aChangesData['more_entries_to_load']) {
|
||||
$oActivityPanel->SetHasMoreEntriesToLoad(true);
|
||||
$oActivityPanel->SetLastEntryId('cmdbchangeop', $aChangesData['last_loaded_entry_id']);
|
||||
}
|
||||
|
||||
// - Add history entries
|
||||
/** @var \Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\EditsEntry $oEntry */
|
||||
foreach ($aChangesData['entries'] as $oEntry) {
|
||||
$oActivityPanel->AddEntry($oEntry);
|
||||
}
|
||||
unset($oChangesSet);
|
||||
|
||||
// Retrieving notification events for cmdbAbstractObject only
|
||||
if ($oObject instanceof cmdbAbstractObject) {
|
||||
@@ -141,7 +110,7 @@ class ActivityPanelFactory
|
||||
if (false === empty($aRelatedTriggersIDs)) {
|
||||
// - Prepare query to retrieve events
|
||||
$oNotifEventsSearch = DBObjectSearch::FromOQL('SELECT EN FROM EventNotification AS EN JOIN Action AS A ON EN.action_id = A.id WHERE EN.trigger_id IN (:triggers_ids) AND EN.object_id = :object_id');
|
||||
$oNotifEventsSet = new DBObjectSet($oNotifEventsSearch, ['id' => false], ['triggers_ids' => $aRelatedTriggersIDs, 'object_id' => $iObjId]);
|
||||
$oNotifEventsSet = new DBObjectSet($oNotifEventsSearch, ['id' => false], ['triggers_ids' => $aRelatedTriggersIDs, 'object_id' => $sObjId]);
|
||||
$oNotifEventsSet->SetLimit(MetaModel::GetConfig()->Get('max_history_length'));
|
||||
|
||||
/** @var \EventNotification $oNotifEvent */
|
||||
|
||||
@@ -8,9 +8,17 @@ namespace Combodo\iTop\Application\UI\Base\Layout\ActivityPanel;
|
||||
|
||||
|
||||
use appUserPreferences;
|
||||
use BinaryExpression;
|
||||
use cmdbAbstractObject;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\ActivityEntryFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityEntry\EditsEntry;
|
||||
use DBObjectSearch;
|
||||
use DBObjectSet;
|
||||
use Exception;
|
||||
use FieldExpression;
|
||||
use IssueLog;
|
||||
use MetaModel;
|
||||
use VariableExpression;
|
||||
|
||||
/**
|
||||
* Class ActivityPanelHelper
|
||||
@@ -75,4 +83,99 @@ class ActivityPanelHelper
|
||||
$aStates[$sObjectClass.'::'.$sObjectMode] = $bIsClosed;
|
||||
appUserPreferences::SetPref('activity_panel.is_closed', $aStates);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sObjectClass
|
||||
* @param string $sObjectId
|
||||
* @param string|null $sChangeOpIdToOffsetFrom Entries will be retrieved after this CMDBChangeOp ID. Typically used for pagination.
|
||||
*
|
||||
* @return array The 'max_history_length' edits entries from the CMDBChangeOp of the object, starting from $sChangeOpIdToOffsetFrom. Flag to know if more entries are available and the ID of the last returned entry are also provided.
|
||||
*
|
||||
* [
|
||||
* 'entries' => EditsEntry[],
|
||||
* 'last_loaded_entry_id' => null|int,
|
||||
* 'more_entries_to_load' => bool,
|
||||
* ]
|
||||
*
|
||||
* @throws \ArchivedObjectException
|
||||
* @throws \CoreException
|
||||
*/
|
||||
public static function GetCMDBChangeOpEditsEntriesForObject(string $sObjectClass, string $sObjectId, ?string $sChangeOpIdToOffsetFrom = null): array
|
||||
{
|
||||
$iMaxHistoryLength = MetaModel::GetConfig()->Get('max_history_length');
|
||||
$aResults = [
|
||||
'entries' => [],
|
||||
'last_loaded_entry_id' => null,
|
||||
'more_entries_to_load' => false,
|
||||
];
|
||||
|
||||
// - Prepare query to retrieve changes
|
||||
$oSearch = DBObjectSearch::FromOQL('SELECT CO FROM CMDBChangeOp AS CO WHERE CO.objclass = :obj_class AND CO.objkey = :obj_key AND CO.finalclass NOT IN (:excluded_optypes)');
|
||||
$aArgs = ['obj_class' => $sObjectClass, 'obj_key' => $sObjectId, 'excluded_optypes' => ['CMDBChangeOpSetAttributeCaseLog']];
|
||||
|
||||
// - Optional offset condition
|
||||
if (null !== $sChangeOpIdToOffsetFrom) {
|
||||
$oSearch->AddConditionExpression(
|
||||
new BinaryExpression(
|
||||
new FieldExpression('id', 'CO'), '<', new VariableExpression('id')
|
||||
)
|
||||
);
|
||||
$aArgs['id'] = $sChangeOpIdToOffsetFrom;
|
||||
}
|
||||
|
||||
// Note: We can't order by date (only) as something multiple CMDBChangeOp rows are inserted at the same time (eg. Delivery model of the "Demo" Organization in the sample data).
|
||||
// As the DB returns rows "chronologically", we get the older first and it messes with the processing. Ordering by the ID is way much simpler and less DB CPU consuming.
|
||||
$oSet = new DBObjectSet($oSearch, ['id' => false], $aArgs);
|
||||
|
||||
// - Limit history entries to display
|
||||
$bMoreEntriesToLoad = $oSet->CountExceeds($iMaxHistoryLength);
|
||||
$oSet->SetLimit($iMaxHistoryLength);
|
||||
|
||||
// Prepare previous values to group edits within a same CMDBChange
|
||||
$iPreviousChangeId = 0;
|
||||
/** @var string|int $iPreviousChangeOpId Only used for pagination */
|
||||
$iPreviousChangeOpId = 0;
|
||||
$oPreviousEditsEntry = null;
|
||||
|
||||
/** @var \CMDBChangeOp $oChangeOp */
|
||||
while ($oChangeOp = $oSet->Fetch()) {
|
||||
// Skip case log changes as they are handled directly from the attributes themselves (most of them should have been excluded by the OQL above, but some derivated classes could still be retrieved)
|
||||
if ($oChangeOp instanceof CMDBChangeOpSetAttributeCaseLog) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make entry from CMDBChangeOp
|
||||
$iChangeId = $oChangeOp->Get('change');
|
||||
try {
|
||||
$oEntry = ActivityEntryFactory::MakeFromCmdbChangeOp($oChangeOp);
|
||||
}
|
||||
catch (Exception $oException) {
|
||||
IssueLog::Debug(static::class.': Could not create entry from CMDBChangeOp #'.$oChangeOp->GetKey().' related to '.$oChangeOp->Get('objclass').'::'.$oChangeOp->Get('objkey').': '.$oException->getMessage());
|
||||
continue;
|
||||
}
|
||||
// If same CMDBChange and mergeable edits entry from the same author, we merge them
|
||||
if (($iChangeId == $iPreviousChangeId) && ($oPreviousEditsEntry instanceof EditsEntry) && ($oEntry instanceof EditsEntry) && ($oPreviousEditsEntry->GetAuthorLogin() === $oEntry->GetAuthorLogin())) {
|
||||
$oPreviousEditsEntry->Merge($oEntry);
|
||||
} else {
|
||||
$aResults['entries'][] = $oEntry;
|
||||
|
||||
// Set previous edits entry
|
||||
if ($oEntry instanceof EditsEntry) {
|
||||
$oPreviousEditsEntry = $oEntry;
|
||||
}
|
||||
}
|
||||
|
||||
$iPreviousChangeId = $iChangeId;
|
||||
$iPreviousChangeOpId = $oChangeOp->GetKey();
|
||||
}
|
||||
unset($oSet);
|
||||
|
||||
// - Set last entry ID so the other can be loaded later
|
||||
if ((true === $bMoreEntriesToLoad) && (0 !== $iPreviousChangeOpId)) {
|
||||
$aResults['last_loaded_entry_id'] = $iPreviousChangeOpId;
|
||||
$aResults['more_entries_to_load'] = true;
|
||||
}
|
||||
|
||||
return $aResults;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user