Răsfoiți Sursa

Merge pull request #6037 from weseek/feat/97396-auditlog-settings

feat: AuditLog Settings
Yuki Takei 3 ani în urmă
părinte
comite
acf3fc04d8

+ 6 - 1
packages/app/resource/locales/en_US/admin/admin.json

@@ -520,6 +520,11 @@
     "date": "Date",
     "action": "Action",
     "ip": "IP Address",
-    "url": "URL"
+    "url": "URL",
+    "settings": "Settings",
+    "return": "Return",
+    "activity_expiration_date": "Audit Log expiration date",
+    "activity_expiration_date_explain": "Created Audit Log are automatically deleted after the number of seconds set in the environment variable from the creation time",
+    "fixed_by_env_var": "This is fixed by the env var <code>{{key}}={{value}}</code>."
   }
 }

+ 2 - 1
packages/app/resource/locales/en_US/translation.json

@@ -125,7 +125,8 @@
   "UserGroup": "UserGroup",
   "ChildUserGroup": "ChildUserGroup",
   "UserGroup Management": "UserGroup Management",
-  "AuditLog": "AuditLog",
+  "AuditLog": "Audit Log",
+  "AuditLog Settings": "Audit Log Settings",
   "Full Text Search Management": "Full Text Search Management",
   "Import Data": "Import Data",
   "Export Archive Data": "Export Archive Data",

+ 6 - 1
packages/app/resource/locales/ja_JP/admin/admin.json

@@ -519,6 +519,11 @@
     "date": "日付",
     "action": "アクション",
     "ip": "IPアドレス",
-    "url": "URL"
+    "url": "URL",
+    "settings": "設定",
+    "return": "戻る",
+    "activity_expiration_date": "監査ログの有効期限",
+    "activity_expiration_date_explain": "作成された監査ログは、作成時間から環境変数に設定した秒数後に自動的に削除されます",
+    "fixed_by_env_var": "環境変数により固定されています <code>{{key}}={{value}}</code>."
   }
 }

+ 1 - 0
packages/app/resource/locales/ja_JP/translation.json

@@ -126,6 +126,7 @@
   "ChildUserGroup": "子グループ",
   "UserGroup Management": "グループ管理",
   "AuditLog": "監査ログ",
+  "AuditLog Settings": "監査ログ設定",
   "Full Text Search Management": "全文検索管理",
   "Import Data": "データインポート",
   "Export Archive Data": "データアーカイブ",

+ 6 - 1
packages/app/resource/locales/zh_CN/admin/admin.json

@@ -529,6 +529,11 @@
     "date": "日期",
     "action": "行动",
     "ip": "IP地址",
-    "url": "URL"
+    "url": "URL",
+    "settings": "设置",
+    "return": "返回",
+    "activity_expiration_date": "审计日志的到期日",
+    "activity_expiration_date_explain": "创建的审计日志会在环境变量中设置的从创建时间算起的秒数后自动删除",
+    "fixed_by_env_var": "这是由env var 修复的 <code>{{key}}={{value}}</code>."
   }
 }

+ 1 - 0
packages/app/resource/locales/zh_CN/translation.json

@@ -134,6 +134,7 @@
   "ChildUserGroup": "儿童用户组",
 	"UserGroup Management": "用户组管理",
   "AuditLog": "审计日志",
+  "AuditLog Settings": "审计日志设置",
 	"Full Text Search Management": "全文搜索管理",
 	"Import Data": "导入数据",
 	"Export Archive Data": "导出主题数据",

+ 2 - 1
packages/app/src/client/services/ContextExtractor.tsx

@@ -18,7 +18,7 @@ import {
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
   useNotFoundTargetPathOrId, useIsSearchPage, useIsForbidden, useIsIdenticalPath, useHasParent,
   useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader, useIsNotFoundPermalink,
-  useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken,
+  useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken, useActivityExpirationSeconds,
 } from '../../stores/context';
 
 const { isTrashPage: _isTrashPage } = pagePathUtils;
@@ -119,6 +119,7 @@ const ContextExtractorOnce: FC = () => {
   useIsEnabledAttachTitleHeader(configByContextHydrate.isEnabledAttachTitleHeader);
   useIsIndentSizeForced(configByContextHydrate.isIndentSizeForced);
   useDefaultIndentSize(configByContextHydrate.adminPreferredIndentSize);
+  useActivityExpirationSeconds(configByContextHydrate.activityExpirationSeconds);
 
 
   // Page

+ 32 - 0
packages/app/src/components/Admin/AuditLog/AuditLogSettings.tsx

@@ -0,0 +1,32 @@
+import React, { FC } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { useActivityExpirationSeconds } from '~/stores/context';
+
+export const AuditLogSettings: FC = () => {
+  const { t } = useTranslation();
+
+  const { data: activityExpirationSecondsData } = useActivityExpirationSeconds();
+  const activityExpirationSeconds = activityExpirationSecondsData != null ? activityExpirationSecondsData : 2592000;
+
+  return (
+    <>
+      <h4>{t('admin:audit_log_management.activity_expiration_date')}</h4>
+      <p className="form-text text-muted">
+        {t('admin:audit_log_management.activity_expiration_date_explain')}
+      </p>
+      <p className="alert alert-warning col-6">
+        <i className="icon-exclamation icon-fw">
+        </i><b>FIXED</b><br />
+        <b
+          // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{
+            __html: t('admin:audit_log_management.fixed_by_env_var',
+              { key: 'ACTIVITY_EXPIRATION_SECONDS', value: activityExpirationSeconds }),
+          }}
+        />
+      </p>
+    </>
+  );
+};

+ 72 - 52
packages/app/src/components/Admin/AuditLogManagement.tsx

@@ -11,6 +11,7 @@ import { useSWRxActivity } from '~/stores/activity';
 import PaginationWrapper from '../PaginationWrapper';
 
 import { ActivityTable } from './AuditLog/ActivityTable';
+import { AuditLogSettings } from './AuditLog/AuditLogSettings';
 import { DateRangePicker } from './AuditLog/DateRangePicker';
 import { SearchUsernameTypeahead } from './AuditLog/SearchUsernameTypeahead';
 import { SelectActionDropdown } from './AuditLog/SelectActionDropdown';
@@ -31,6 +32,7 @@ export const AuditLogManagement: FC = () => {
   /*
    * State
    */
+  const [isSettingPage, setIsSettingPage] = useState<boolean>(false);
   const [activePage, setActivePage] = useState<number>(1);
   const offset = (activePage - 1) * PAGING_LIMIT;
   const [startDate, setStartDate] = useState<Date | null>(null);
@@ -92,59 +94,77 @@ export const AuditLogManagement: FC = () => {
 
   return (
     <div data-testid="admin-auditlog">
-      <h2 className="admin-setting-header mb-3">{t('AuditLog')}</h2>
-
-      <div className="form-inline mb-3">
-        <SearchUsernameTypeahead
-          onChange={setUsernamesHandler}
-        />
-
-        <DateRangePicker
-          startDate={startDate}
-          endDate={endDate}
-          onChange={datePickerChangedHandler}
-        />
-
-        <SelectActionDropdown
-          dropdownItems={[
-            { actionCategory: 'Page', actionNames: PageActions },
-            { actionCategory: 'Comment', actionNames: CommentActions },
-          ]}
-          actionMap={actionMap}
-          onChangeAction={actionCheckboxChangedHandler}
-          onChangeMultipleAction={multipleActionCheckboxChangedHandler}
-        />
-
-        <button type="button" className="btn ml-auto grw-btn-reload" onClick={reloadButtonPushedHandler}>
-          <i className="icon icon-reload" />
-        </button>
-      </div>
-
-      <p
-        className="ml-2"
-        // eslint-disable-next-line react/no-danger
-        dangerouslySetInnerHTML={{ __html: activityCounter }}
-      />
-
-      { isLoading
-        ? (
-          <div className="text-muted text-center mb-5">
-            <i className="fa fa-2x fa-spinner fa-pulse mr-1" />
+      <button type="button" className="btn btn-outline-secondary mb-4" onClick={() => setIsSettingPage(!isSettingPage)}>
+        {
+          isSettingPage
+            ? <><i className="fa fa-hand-o-left mr-1" />{t('admin:audit_log_management.return')}</>
+            : <><i className="fa icon-settings mr-1" />{t('admin:audit_log_management.settings')}</>
+        }
+      </button>
+
+      <h2 className="admin-setting-header mb-3">
+        <span>
+          {isSettingPage ? t('AuditLog Settings') : t('AuditLog')}
+        </span>
+      </h2>
+
+      {isSettingPage ? (
+        <AuditLogSettings />
+      ) : (
+        <>
+          <div className="form-inline mb-3">
+            <SearchUsernameTypeahead
+              onChange={setUsernamesHandler}
+            />
+
+            <DateRangePicker
+              startDate={startDate}
+              endDate={endDate}
+              onChange={datePickerChangedHandler}
+            />
+
+            <SelectActionDropdown
+              dropdownItems={[
+                { actionCategory: 'Page', actionNames: PageActions },
+                { actionCategory: 'Comment', actionNames: CommentActions },
+              ]}
+              actionMap={actionMap}
+              onChangeAction={actionCheckboxChangedHandler}
+              onChangeMultipleAction={multipleActionCheckboxChangedHandler}
+            />
+
+            <button type="button" className="btn ml-auto grw-btn-reload" onClick={reloadButtonPushedHandler}>
+              <i className="icon icon-reload" />
+            </button>
           </div>
-        )
-        : (
-          <ActivityTable activityList={activityList} />
-        )
-      }
-
-      <PaginationWrapper
-        activePage={activePage}
-        changePage={setActivePageHandler}
-        totalItemsCount={totalActivityNum}
-        pagingLimit={PAGING_LIMIT}
-        align="center"
-        size="sm"
-      />
+
+          <p
+            className="ml-2"
+            // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{ __html: activityCounter }}
+          />
+
+          { isLoading
+            ? (
+              <div className="text-muted text-center mb-5">
+                <i className="fa fa-2x fa-spinner fa-pulse mr-1" />
+              </div>
+            )
+            : (
+              <ActivityTable activityList={activityList} />
+            )
+          }
+
+          <PaginationWrapper
+            activePage={activePage}
+            changePage={setActivePageHandler}
+            totalItemsCount={totalActivityNum}
+            pagingLimit={PAGING_LIMIT}
+            align="center"
+            size="sm"
+          />
+        </>
+      )}
     </div>
   );
 };

+ 2 - 1
packages/app/src/server/models/config.ts

@@ -1,7 +1,7 @@
+import { getOrCreateModel } from '@growi/core';
 import { Types, Schema } from 'mongoose';
 import uniqueValidator from 'mongoose-unique-validator';
 
-import { getOrCreateModel } from '@growi/core';
 
 export interface Config {
   _id: Types.ObjectId;
@@ -242,6 +242,7 @@ schema.statics.getLocalconfig = function(crowi) {
     globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
     pageLimitationL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationL'),
     pageLimitationXL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL'),
+    activityExpirationSeconds: crowi.configManager.getConfig('crowi', 'app:activityExpirationSeconds'),
   };
 
   return localConfig;

+ 1 - 1
packages/app/src/server/service/config-loader.ts

@@ -626,7 +626,7 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     ns: 'crowi',
     key: 'app:activityExpirationSeconds',
     type: ValueType.NUMBER,
-    default: 2592000,
+    default: 2592000, // 30 days
   },
 };
 

+ 4 - 0
packages/app/src/stores/context.tsx

@@ -168,6 +168,10 @@ export const useDefaultIndentSize = (initialData?: number) : SWRResponse<number,
   return useStaticSWR<number, Error>('defaultIndentSize', initialData, { fallbackData: 4 });
 };
 
+export const useActivityExpirationSeconds = (initialData?: number) : SWRResponse<number, Error> => {
+  return useStaticSWR<number, Error>('activityExpirationSeconds', initialData);
+};
+
 
 /** **********************************************************
  *                     Computed contexts