diff --git a/application/Espo/Classes/Cleanup/AppLog.php b/application/Espo/Classes/Cleanup/AppLog.php new file mode 100644 index 0000000000..a5ec3c8b87 --- /dev/null +++ b/application/Espo/Classes/Cleanup/AppLog.php @@ -0,0 +1,65 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\Cleanup; + +use Espo\Core\Cleanup\Cleanup; +use Espo\Core\Field\DateTime; +use Espo\Core\Utils\Config; +use Espo\Entities\AppLogRecord; +use Espo\ORM\EntityManager; +use Espo\ORM\Query\DeleteBuilder; + +class AppLog implements Cleanup +{ + private const PERIOD = '30 days'; + + public function __construct( + private EntityManager $entityManager, + private Config $config + ) {} + + public function process(): void + { + $query = DeleteBuilder::create() + ->from(AppLogRecord::ENTITY_TYPE) + ->where(['createdAt<' => $this->getBefore()->toString()]) + ->build(); + + $this->entityManager->getQueryExecutor()->execute($query); + } + + private function getBefore(): DateTime + { + /** @var string $period */ + $period = $this->config->get('cleanupAppLogPeriod') ?? self::PERIOD; + + return DateTime::createNow()->modify('-' . $period); + } +} diff --git a/application/Espo/Classes/Select/AppLogRecord/PrimaryFilters/Errors.php b/application/Espo/Classes/Select/AppLogRecord/PrimaryFilters/Errors.php new file mode 100644 index 0000000000..f586495b59 --- /dev/null +++ b/application/Espo/Classes/Select/AppLogRecord/PrimaryFilters/Errors.php @@ -0,0 +1,49 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\Select\AppLogRecord\PrimaryFilters; + +use Espo\Core\Select\Primary\Filter; +use Espo\ORM\Query\SelectBuilder as QueryBuilder; +use Psr\Log\LogLevel; + +class Errors implements Filter +{ + public function apply(QueryBuilder $queryBuilder): void + { + $queryBuilder->where([ + 'level' => [ + ucfirst(LogLevel::ERROR), + ucfirst(LogLevel::EMERGENCY), + ucfirst(LogLevel::CRITICAL), + ucfirst(LogLevel::ALERT), + ] + ]); + } +} diff --git a/application/Espo/Controllers/AppLogRecord.php b/application/Espo/Controllers/AppLogRecord.php new file mode 100644 index 0000000000..86c28c5944 --- /dev/null +++ b/application/Espo/Controllers/AppLogRecord.php @@ -0,0 +1,76 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Controllers; + +use Espo\Core\Exceptions\Forbidden; +use Espo\Core\Controllers\Record; +use Espo\Core\Api\Request; +use Espo\Core\Api\Response; +use stdClass; + +class AppLogRecord extends Record +{ + protected function checkAccess(): bool + { + if (!$this->user->isAdmin()) { + return false; + } + + if (!$this->config->get('restrictedMode')) { + return true; + } + + if ($this->config->get('appLogAdminAllowed')) { + return true; + } + + return $this->user->isSuperAdmin(); + } + + public function postActionCreate(Request $request, Response $response): stdClass + { + throw new Forbidden(); + } + + public function putActionUpdate(Request $request, Response $response): stdClass + { + throw new Forbidden(); + } + + public function postActionCreateLink(Request $request): bool + { + throw new Forbidden(); + } + + public function deleteActionRemoveLink(Request $request): bool + { + throw new Forbidden(); + } +} diff --git a/application/Espo/Core/Container.php b/application/Espo/Core/Container.php index c9fb9567cd..35e6d94ed5 100644 --- a/application/Espo/Core/Container.php +++ b/application/Espo/Core/Container.php @@ -308,7 +308,9 @@ class Container implements ContainerInterface return $loader; } - assert($this->configuration !== null); + if ($this->configuration === null) { + throw new RuntimeException("Container configuration is not ready."); + } return $this->configuration->getLoaderClassName($id); } diff --git a/application/Espo/Core/Container/ContainerBuilder.php b/application/Espo/Core/Container/ContainerBuilder.php index 66349d39f3..c02cb068f6 100644 --- a/application/Espo/Core/Container/ContainerBuilder.php +++ b/application/Espo/Core/Container/ContainerBuilder.php @@ -36,6 +36,7 @@ use Espo\Core\Binding\BindingContainer; use Espo\Core\Binding\BindingLoader; use Espo\Core\Binding\EspoBindingLoader; +use Espo\Core\Loaders\ApplicationState as ApplicationStateLoader; use Espo\Core\Utils\File\Manager as FileManager; use Espo\Core\Utils\Config\ConfigFileManager; use Espo\Core\Utils\Config; @@ -71,6 +72,7 @@ class ContainerBuilder 'log' => LogLoader::class, 'dataManager' => DataManagerLoader::class, 'metadata' => MetadataLoader::class, + 'applicationState' => ApplicationStateLoader::class, ]; public function withBindingLoader(BindingLoader $bindingLoader): self diff --git a/application/Espo/Core/Log/Handler/DatabaseHandler.php b/application/Espo/Core/Log/Handler/DatabaseHandler.php new file mode 100644 index 0000000000..19a58929e1 --- /dev/null +++ b/application/Espo/Core/Log/Handler/DatabaseHandler.php @@ -0,0 +1,145 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Log\Handler; + +use Espo\Core\Api\Request; +use Espo\Core\ApplicationState; +use Espo\Core\ORM\EntityManagerProxy; +use Espo\Entities\AppLogRecord; +use Monolog\Handler\HandlerInterface; +use Monolog\Level; +use Monolog\LogRecord; +use Throwable; + +class DatabaseHandler implements HandlerInterface +{ + public function __construct( + private Level $level, + private EntityManagerProxy $entityManager, + private ApplicationState $applicationState + ) {} + + public function isHandling(LogRecord $record): bool + { + if (!$this->applicationState->hasUser()) { + return false; + } + + if ($record->context['isSql'] ?? false) { + return false; + } + + return $record->level->value >= $this->level->value; + } + + public function handle(LogRecord $record): bool + { + if (!$this->isHandling($record)) { + return false; + } + + try { + $logRecord = $this->entityManager->getRDBRepositoryByClass(AppLogRecord::class)->getNew(); + + $level = ucfirst($record->level->toPsrLogLevel()); + $message = $this->interpolate($record, $record->message); + + $logRecord + ->setLevel($level) + ->setMessage($message); + + $this->setException($record, $logRecord); + $this->setRequest($record, $logRecord); + + $this->entityManager->saveEntity($logRecord); + } + catch (Throwable) { + // Nowhere to log. + + return false; + } + + return true; + } + + private function interpolate(LogRecord $record, string $line): string + { + $replace = []; + + foreach ($record->context as $key => $val) { + if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { + $replace['{' . $key . '}'] = $val; + } + } + + return strtr($line, $replace); + } + + /** + * @param LogRecord[] $records + */ + public function handleBatch(array $records): void + { + foreach ($records as $record) { + $this->handle($record); + } + } + + public function close(): void + {} + + private function setException(LogRecord $record, AppLogRecord $logRecord): void + { + $exception = $record->context['exception'] ?? null; + + if (!$exception instanceof Throwable) { + return; + } + + $logRecord + ->setExceptionClass(get_class($exception)) + ->setFile($exception->getFile()) + ->setLine($exception->getLine()) + ->setCode($exception->getCode()); + } + + private function setRequest(LogRecord $record, AppLogRecord $logRecord): void + { + $request = $record->context['request'] ?? null; + + if (!$request instanceof Request) { + return; + } + + $logRecord + ->setRequestMethod($request->getMethod()) + ->setRequestResourcePath($request->getResourcePath()); + } +} diff --git a/application/Espo/Core/Log/LogLoader.php b/application/Espo/Core/Log/LogLoader.php index 7330ff9fa1..281d9c0378 100644 --- a/application/Espo/Core/Log/LogLoader.php +++ b/application/Espo/Core/Log/LogLoader.php @@ -29,8 +29,11 @@ namespace Espo\Core\Log; +use Espo\Core\ApplicationState; +use Espo\Core\Log\Handler\DatabaseHandler; use Espo\Core\Log\Handler\EspoFileHandler; use Espo\Core\Log\Handler\EspoRotatingFileHandler; +use Espo\Core\ORM\EntityManagerProxy; use Espo\Core\Utils\Config; use Espo\Core\Utils\Log; @@ -47,7 +50,9 @@ class LogLoader public function __construct( private readonly Config $config, - private readonly HandlerListLoader $handlerListLoader + private readonly HandlerListLoader $handlerListLoader, + private readonly EntityManagerProxy $entityManagerProxy, + private readonly ApplicationState $applicationState ) {} public function load(): Log @@ -65,6 +70,10 @@ class LogLoader $handlerList = [$this->createDefaultHandler()]; } + if ($this->config->get('logger.databaseHandler')) { + $handlerList[] = $this->createDatabaseHandler(); + } + foreach ($handlerList as $handler) { $log->pushHandler($handler); } @@ -105,4 +114,15 @@ class LogLoader { return (bool) $this->config->get('logger.printTrace'); } + + private function createDatabaseHandler(): HandlerInterface + { + $rawLevel = $this->config->get('logger.databaseHandlerLevel') ?? + $this->config->get('logger.level') ?? + self::DEFAULT_LEVEL; + + $level = Logger::toMonologLevel($rawLevel); + + return new DatabaseHandler($level, $this->entityManagerProxy, $this->applicationState); + } } diff --git a/application/Espo/Entities/AppLogRecord.php b/application/Espo/Entities/AppLogRecord.php new file mode 100644 index 0000000000..bd2e5fca0c --- /dev/null +++ b/application/Espo/Entities/AppLogRecord.php @@ -0,0 +1,111 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Entities; + +use Espo\Core\ORM\Entity; + +class AppLogRecord extends Entity +{ + public const ENTITY_TYPE = 'AppLogRecord'; + + public function setMessage(string $message): self + { + $this->set('message', $message); + + return $this; + } + + public function setLevel(string $level): self + { + $this->set('level', $level); + + return $this; + } + + public function setCode(?int $code): self + { + $this->set('code', $code); + + return $this; + } + + public function setExceptionClass(?string $exceptionClass): self + { + $len = $this->getAttributeParam('exceptionClass', 'len'); + + if ($exceptionClass && strlen($exceptionClass) > $len) { + $exceptionClass = substr($exceptionClass, $len); + } + + $this->set('exceptionClass', $exceptionClass); + + return $this; + } + + public function setFile(?string $file): self + { + $len = $this->getAttributeParam('file', 'len'); + + if ($file && strlen($file) > $len) { + $file = substr($file, $len); + } + + $this->set('file', $file); + + return $this; + } + + public function setLine(?int $code): self + { + $this->set('line', $code); + + return $this; + } + + public function setRequestMethod(?string $requestMethod): self + { + $this->set('requestMethod', $requestMethod); + + return $this; + } + + public function setRequestResourcePath(?string $requestResourcePath): self + { + $len = $this->getAttributeParam('requestResourcePath', 'len'); + + if ($requestResourcePath && strlen($requestResourcePath) > $len) { + $requestResourcePath = substr($requestResourcePath, $len); + } + + $this->set('requestResourcePath', $requestResourcePath); + + return $this; + } +} diff --git a/application/Espo/ORM/Executor/DefaultSqlExecutor.php b/application/Espo/ORM/Executor/DefaultSqlExecutor.php index a28711a966..af8a720bfc 100644 --- a/application/Espo/ORM/Executor/DefaultSqlExecutor.php +++ b/application/Espo/ORM/Executor/DefaultSqlExecutor.php @@ -59,7 +59,7 @@ class DefaultSqlExecutor implements SqlExecutor public function execute(string $sql, bool $rerunIfDeadlock = false): PDOStatement { if ($this->logAll) { - $this->logger?->info("SQL: " . $sql); + $this->logger?->info("SQL: " . $sql, ['isSql' => true]); } if (!$rerunIfDeadlock) { @@ -81,7 +81,7 @@ class DefaultSqlExecutor implements SqlExecutor if ($counter === 0 || !$this->isExceptionIsDeadlock($e)) { if ($this->logFailed) { - $this->logger?->error("SQL failed: " . $sql); + $this->logger?->error("SQL failed: " . $sql, ['isSql' => true]); } /** @var PDOException $e */ diff --git a/application/Espo/Resources/defaults/config.php b/application/Espo/Resources/defaults/config.php index 00d679ef0d..4a44ce044d 100644 --- a/application/Espo/Resources/defaults/config.php +++ b/application/Espo/Resources/defaults/config.php @@ -236,6 +236,9 @@ return [ 'cleanupSubscribers' => true, 'cleanupAudit' => true, 'cleanupAuditPeriod' => '3 months', + 'cleanupAppLog' => true, + 'cleanupAppLogPeriod' => '30 days', + 'appLogAdminAllowed' => false, 'currencyFormat' => 2, 'currencyDecimalPlaces' => 2, 'aclAllowDeleteCreated' => false, diff --git a/application/Espo/Resources/defaults/systemConfig.php b/application/Espo/Resources/defaults/systemConfig.php index bce77e79ba..e04793c937 100644 --- a/application/Espo/Resources/defaults/systemConfig.php +++ b/application/Espo/Resources/defaults/systemConfig.php @@ -111,6 +111,8 @@ return [ 'passwordChangeRequestNewUserLifetime', 'passwordChangeRequestExistingUserLifetime', 'passwordRecoveryInternalIntervalPeriod', + 'cleanupAppLog', + 'cleanupAppLogPeriod', ], 'adminItems' => [ 'devMode', @@ -219,6 +221,7 @@ return [ 'jobRunInParallel', 'jobPoolConcurrencyNumber', 'jobPeriodForActiveProcess', + 'appLogAdminAllowed', 'cronMinInterval', 'daemonInterval', 'daemonProcessTimeout', diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json index 1b9a416d12..1c9225732d 100644 --- a/application/Espo/Resources/i18n/en_US/Admin.json +++ b/application/Espo/Resources/i18n/en_US/Admin.json @@ -37,6 +37,7 @@ "User Interface": "User Interface", "Auth Tokens": "Auth Tokens", "Auth Log": "Auth Log", + "App Log": "App Log", "Authentication": "Authentication", "Currency": "Currency", "Integrations": "Integrations", @@ -293,6 +294,7 @@ "labelManager": "Customize application labels.", "templateManager": "Customize message templates.", "authLog": "Login history.", + "appLog": "Application log.", "leadCapture": "API entry points for Web-to-Lead.", "attachments": "All file attachments stored in the system.", "systemRequirements": "System Requirements for EspoCRM.", diff --git a/application/Espo/Resources/i18n/en_US/AppLogRecord.json b/application/Espo/Resources/i18n/en_US/AppLogRecord.json new file mode 100644 index 0000000000..e17fca5920 --- /dev/null +++ b/application/Espo/Resources/i18n/en_US/AppLogRecord.json @@ -0,0 +1,15 @@ +{ + "fields": { + "message": "Message", + "code": "Code", + "level": "Level", + "exceptionClass": "Exception Class", + "file": "File", + "line": "Line", + "requestMethod": "Request Method", + "requestResourcePath": "Request Resource Path" + }, + "presetFilters": { + "errors": "Errors" + } +} diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index 569798285d..b4970b445a 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -47,6 +47,7 @@ "Preferences": "Preferences", "EmailAddress": "Email Address", "PhoneNumber": "Phone Number", + "AppLogRecord": "App Log Record", "AuthLogRecord": "Auth Log Record", "AuthFailLogRecord": "Auth Fail Log Record", "LeadCapture": "Lead Capture Entry Point", @@ -100,6 +101,7 @@ "AuthToken": "Auth Tokens", "UniqueId": "Unique IDs", "LastViewed": "Last Viewed", + "AppLogRecord": "App Log", "AuthLogRecord": "Auth Log", "AuthFailLogRecord": "Auth Fail Log", "LeadCapture": "Lead Capture", diff --git a/application/Espo/Resources/layouts/AppLogRecord/detail.json b/application/Espo/Resources/layouts/AppLogRecord/detail.json new file mode 100644 index 0000000000..1eb06731b0 --- /dev/null +++ b/application/Espo/Resources/layouts/AppLogRecord/detail.json @@ -0,0 +1,21 @@ +[ + { + "rows": [ + [{"name": "level"}, false], + [{"name": "message"}] + ] + }, + { + "rows": [ + [{"name": "code"}, false], + [{"name": "exceptionClass"}], + [{"name": "file"}], + [{"name": "line"}, false] + ] + }, + { + "rows": [ + [{"name": "requestMethod"}, {"name": "requestResourcePath"}] + ] + } +] diff --git a/application/Espo/Resources/layouts/AppLogRecord/detailSmall.json b/application/Espo/Resources/layouts/AppLogRecord/detailSmall.json new file mode 100644 index 0000000000..1eb06731b0 --- /dev/null +++ b/application/Espo/Resources/layouts/AppLogRecord/detailSmall.json @@ -0,0 +1,21 @@ +[ + { + "rows": [ + [{"name": "level"}, false], + [{"name": "message"}] + ] + }, + { + "rows": [ + [{"name": "code"}, false], + [{"name": "exceptionClass"}], + [{"name": "file"}], + [{"name": "line"}, false] + ] + }, + { + "rows": [ + [{"name": "requestMethod"}, {"name": "requestResourcePath"}] + ] + } +] diff --git a/application/Espo/Resources/layouts/AppLogRecord/filters.json b/application/Espo/Resources/layouts/AppLogRecord/filters.json new file mode 100644 index 0000000000..b62c075f4c --- /dev/null +++ b/application/Espo/Resources/layouts/AppLogRecord/filters.json @@ -0,0 +1,10 @@ +[ + "createdAt", + "level", + "code", + "exceptionClass", + "file", + "line", + "requestMethod", + "requestResourcePath" +] diff --git a/application/Espo/Resources/layouts/AppLogRecord/list.json b/application/Espo/Resources/layouts/AppLogRecord/list.json new file mode 100644 index 0000000000..628e1e5eb7 --- /dev/null +++ b/application/Espo/Resources/layouts/AppLogRecord/list.json @@ -0,0 +1,26 @@ +[ + {"name": "level", "widthPx": 100}, + {"name": "code", "widthPx": 80}, + {"name": "message"}, + {"name": "createdAt", "width": 13}, + { + "name": "exceptionClass", + "width": 20, + "hidden": true + }, + { + "name": "file", + "width": 20, + "hidden": true + }, + { + "name": "requestMethod", + "width": 10, + "hidden": true + }, + { + "name": "requestResourcePath", + "width": 20, + "hidden": true + } +] diff --git a/application/Espo/Resources/metadata/app/adminPanel.json b/application/Espo/Resources/metadata/app/adminPanel.json index 2bfeb93d34..34eebf13b2 100644 --- a/application/Espo/Resources/metadata/app/adminPanel.json +++ b/application/Espo/Resources/metadata/app/adminPanel.json @@ -331,6 +331,12 @@ "label": "Phone Numbers", "iconClass": "fas fa-phone", "description": "phoneNumbers" + }, + { + "url": "#Admin/appLog", + "label": "App Log", + "iconClass": "fas fa-list", + "description": "appLog" } ], "order": 25 diff --git a/application/Espo/Resources/metadata/app/cleanup.json b/application/Espo/Resources/metadata/app/cleanup.json index ed90e1cc5c..99ae71a734 100644 --- a/application/Espo/Resources/metadata/app/cleanup.json +++ b/application/Espo/Resources/metadata/app/cleanup.json @@ -27,5 +27,8 @@ }, "stars": { "className": "Espo\\Classes\\Cleanup\\Stars" + }, + "appLog": { + "className": "Espo\\Classes\\Cleanup\\AppLog" } } diff --git a/application/Espo/Resources/metadata/clientDefs/AppLogRecord.json b/application/Espo/Resources/metadata/clientDefs/AppLogRecord.json new file mode 100644 index 0000000000..60c563c7f2 --- /dev/null +++ b/application/Espo/Resources/metadata/clientDefs/AppLogRecord.json @@ -0,0 +1,14 @@ +{ + "controller": "controllers/record", + "createDisabled": true, + "editDisabled": true, + "mergeDisabled": true, + "recordViews": { + "list": "views/admin/app-log-record/record/list" + }, + "filterList": [ + { + "name": "errors" + } + ] +} diff --git a/application/Espo/Resources/metadata/entityDefs/AppLogRecord.json b/application/Espo/Resources/metadata/entityDefs/AppLogRecord.json new file mode 100644 index 0000000000..85502c3582 --- /dev/null +++ b/application/Espo/Resources/metadata/entityDefs/AppLogRecord.json @@ -0,0 +1,94 @@ +{ + "fields": { + "number": { + "type": "autoincrement", + "dbType": "bigint" + }, + "createdAt": { + "type": "datetime", + "readOnly": true + }, + "message": { + "type": "text", + "readOnly": true, + "orderDisabled": true + }, + "level": { + "type": "enum", + "options": [ + "Debug", + "Info", + "Notice", + "Warning", + "Error", + "Critical", + "Alert", + "Emergency" + ], + "style": { + "Info": "info", + "Notice": "primary", + "Warning": "warning", + "Error": "danger", + "Critical": "danger", + "Alert": "danger", + "Emergency": "danger" + }, + "maxLength": 9, + "readOnly": true, + "index": true + }, + "code": { + "type": "int", + "readOnly": true + }, + "exceptionClass": { + "type": "varchar", + "maxLength": 512, + "readOnly": true + }, + "file": { + "type": "varchar", + "maxLength": 512, + "readOnly": true + }, + "line": { + "type": "int", + "readOnly": true + }, + "requestMethod": { + "type": "enum", + "maxLength": 7, + "options": [ + "GET", + "POST", + "PUT", + "UPDATE", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS", + "TRACE" + ], + "readOnly": true + }, + "requestResourcePath": { + "type": "varchar", + "maxLength": 255, + "readOnly": true + }, + "requestUrl": { + "type": "varchar", + "maxLength": 512, + "readOnly": true + } + }, + "links": {}, + "collection": { + "orderBy": "number", + "order": "desc", + "textFilterFields": ["message"] + }, + "indexes": {}, + "hooksDisabled": true +} diff --git a/application/Espo/Resources/metadata/recordDefs/AppLogRecord.json b/application/Espo/Resources/metadata/recordDefs/AppLogRecord.json new file mode 100644 index 0000000000..c2bbb32733 --- /dev/null +++ b/application/Espo/Resources/metadata/recordDefs/AppLogRecord.json @@ -0,0 +1,8 @@ +{ + "massActions": { + "delete": { + "allowed": true + } + }, + "actionHistoryDisabled": true +} diff --git a/application/Espo/Resources/metadata/scopes/AppLogRecord.json b/application/Espo/Resources/metadata/scopes/AppLogRecord.json new file mode 100644 index 0000000000..a5280171da --- /dev/null +++ b/application/Espo/Resources/metadata/scopes/AppLogRecord.json @@ -0,0 +1,7 @@ +{ + "entity": true, + "layouts": false, + "tab": false, + "acl": false, + "customizable": false +} diff --git a/application/Espo/Resources/metadata/selectDefs/AppLogRecord.json b/application/Espo/Resources/metadata/selectDefs/AppLogRecord.json new file mode 100644 index 0000000000..ba0a68b965 --- /dev/null +++ b/application/Espo/Resources/metadata/selectDefs/AppLogRecord.json @@ -0,0 +1,5 @@ +{ + "primaryFilterClassNameMap": { + "errors": "Espo\\Classes\\Select\\AppLogRecord\\PrimaryFilters\\Errors" + } +} diff --git a/client/src/controllers/admin.js b/client/src/controllers/admin.js index a675dee0eb..8ec31097b4 100644 --- a/client/src/controllers/admin.js +++ b/client/src/controllers/admin.js @@ -412,6 +412,29 @@ class AdminController extends Controller { }); } + // noinspection JSUnusedGlobalSymbols + actionAppLog() { + this.collectionFactory.create('AppLogRecord', collection => { + const searchManager = new SearchManager( + collection, + 'list', + this.getStorage(), + this.getDateTime() + ); + + searchManager.loadStored(); + + collection.where = searchManager.getWhere(); + collection.maxSize = this.getConfig().get('recordsPerPage') || collection.maxSize; + + this.main('views/list', { + scope: 'AppLogRecord', + collection: collection, + searchManager: searchManager + }); + }); + } + // noinspection JSUnusedGlobalSymbols actionIntegrations(options) { const integration = options.name || null; diff --git a/client/src/views/admin/app-log-record/record/list.js b/client/src/views/admin/app-log-record/record/list.js new file mode 100644 index 0000000000..0851cb4227 --- /dev/null +++ b/client/src/views/admin/app-log-record/record/list.js @@ -0,0 +1,35 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Website: https://www.espocrm.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +import ListRecordView from 'views/record/list'; + +export default class extends ListRecordView { + + forceSettings = true +} + diff --git a/client/src/views/record/panels/default-side.js b/client/src/views/record/panels/default-side.js index e3fd6143a3..97c84f3008 100644 --- a/client/src/views/record/panels/default-side.js +++ b/client/src/views/record/panels/default-side.js @@ -55,11 +55,11 @@ class DefaultSidePanelView extends SidePanelView { const allFieldList = this.getFieldManager().getEntityTypeFieldList(this.model.entityType); this.hasComplexCreated = - allFieldList.includes('createdAt') && + allFieldList.includes('createdAt') || allFieldList.includes('createdBy'); this.hasComplexModified = - allFieldList.includes('modifiedAt') && + allFieldList.includes('modifiedAt') || allFieldList.includes('modifiedBy'); super.setup();