Просмотр исходного кода

Merge pull request #10781 from growilabs/feat/94970-178340-audit-log-bulk-export-in-app-notifications

feat: audit log bulk export in app notifications
Yuki Takei 1 месяц назад
Родитель
Сommit
6336a45cff

+ 5 - 0
apps/app/public/static/locales/en_US/translation.json

@@ -861,6 +861,11 @@
     "started_on": "Started on",
     "file_upload_not_configured": "File upload settings are not configured"
   },
+  "audit_log_bulk_export": {
+    "download_expired": "Download period has expired",
+    "job_expired": "Export process was canceled because it took too long",
+    "no_results": "No audit logs matched the specified filters"
+  },
   "message": {
     "successfully_connected": "Successfully Connected!",
     "fail_to_save_access_token": "Failed to save access_token. Please try again.",

+ 5 - 0
apps/app/public/static/locales/fr_FR/translation.json

@@ -855,6 +855,11 @@
     "started_on": "Commencé le",
     "file_upload_not_configured": "Les paramètres de téléchargement de fichiers ne sont pas configurés"
   },
+  "audit_log_bulk_export": {
+    "download_expired": "La période de téléchargement a expiré",
+    "job_expired": "Le processus d'exportation a été annulé car il a pris trop de temps",
+    "no_results": "Aucun journal d'audit ne correspondait aux filtres spécifiés"
+  },
   "message": {
     "successfully_connected": "Connecté!",
     "fail_to_save_access_token": "Échec de la sauvegarde de access_token.",

+ 5 - 0
apps/app/public/static/locales/ja_JP/translation.json

@@ -894,6 +894,11 @@
     "started_on": "開始日時",
     "file_upload_not_configured": "ファイルアップロード設定が完了していません"
   },
+  "audit_log_bulk_export": {
+    "download_expired": "ダウンロード期限が切れました",
+    "job_expired": "エクスポート時間が長すぎるため、処理が中断されました",
+    "no_results": "指定されたフィルターに一致する監査ログはありませんでした"
+  },
   "message": {
     "successfully_connected": "接続に成功しました!",
     "fail_to_save_access_token": "アクセストークンの保存に失敗しました、再度お試しください。",

+ 5 - 0
apps/app/public/static/locales/ko_KR/translation.json

@@ -821,6 +821,11 @@
     "started_on": "시작일",
     "file_upload_not_configured": "파일 업로드 설정이 구성되지 않았습니다."
   },
+  "audit_log_bulk_export": {
+    "download_expired": "다운로드 기간이 만료되었습니다",
+    "job_expired": "수출 프로세스가 너무 오래 걸려 취소되었습니다",
+    "no_results": "지정된 필터에 일치하는 감사 로그가 없습니다"
+  },
   "message": {
     "successfully_connected": "성공적으로 연결되었습니다!",
     "fail_to_save_access_token": "액세스 토큰 저장 실패. 다시 시도하십시오.",

+ 5 - 0
apps/app/public/static/locales/zh_CN/translation.json

@@ -866,6 +866,11 @@
     "started_on": "开始于",
     "file_upload_not_configured": "未配置文件上传设置"
   },
+  "audit_log_bulk_export": {
+    "download_expired": "下载期限已过期",
+    "job_expired": "导出过程因耗时过长被取消",
+    "no_results": "没有审计日志符合指定筛选条件"
+  },
   "message": {
     "successfully_connected": "连接成功!",
     "fail_to_save_access_token": "无法保存访问令牌。请再试一次。",

+ 3 - 6
apps/app/src/client/components/Admin/AuditLog/AuditLogExportModal.tsx

@@ -4,6 +4,7 @@ import { useAtomValue } from 'jotai';
 import { useTranslation } from 'react-i18next';
 import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 
+import type { IAuditLogBulkExportFilters } from '~/features/audit-log-bulk-export/interfaces/audit-log-bulk-export';
 import type { SupportedActionType } from '~/interfaces/activity';
 import { auditLogAvailableActionsAtom } from '~/states/server-configurations';
 
@@ -77,12 +78,8 @@ const AuditLogExportModalSubstance = ({
       .filter((v) => v[1])
       .map((v) => v[0]);
 
-    const filters: {
-      actions?: SupportedActionType[];
-      dateFrom?: Date;
-      dateTo?: Date;
-      // TODO: Add users filter after implementing username-to-userId conversion
-    } = {};
+    const filters: IAuditLogBulkExportFilters = {};
+    // TODO: Add users filter after implementing username-to-userId conversion
 
     if (selectedActionList.length > 0) {
       filters.actions = selectedActionList;

+ 98 - 0
apps/app/src/client/components/InAppNotification/ModelNotification/AuditLogBulkExportJobModelNotification.tsx

@@ -0,0 +1,98 @@
+import React from 'react';
+import { type HasObjectId, isPopulated } from '@growi/core';
+import { useTranslation } from 'react-i18next';
+
+import type { IAuditLogBulkExportJobHasId } from '~/features/audit-log-bulk-export/interfaces/audit-log-bulk-export';
+import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
+import type { IInAppNotification } from '~/interfaces/in-app-notification';
+
+import type { ModelNotificationUtils } from '.';
+import { ModelNotification } from './ModelNotification';
+import { useActionMsgAndIconForModelNotification } from './useActionAndMsg';
+
+export const useAuditLogBulkExportJobModelNotification = (
+  notification: IInAppNotification & HasObjectId,
+): ModelNotificationUtils | null => {
+  const { t } = useTranslation();
+  const { actionMsg, actionIcon } =
+    useActionMsgAndIconForModelNotification(notification);
+
+  const isAuditLogBulkExportJobModelNotification = (
+    notification: IInAppNotification & HasObjectId,
+  ): notification is IInAppNotification<IAuditLogBulkExportJobHasId> &
+    HasObjectId => {
+    return (
+      notification.targetModel ===
+      SupportedTargetModel.MODEL_AUDIT_LOG_BULK_EXPORT_JOB
+    );
+  };
+
+  if (!isAuditLogBulkExportJobModelNotification(notification)) {
+    return null;
+  }
+
+  const actionUsers = notification.user.username;
+
+  const getSubMsg = (): JSX.Element => {
+    if (
+      notification.action ===
+        SupportedAction.ACTION_AUDIT_LOG_BULK_EXPORT_COMPLETED &&
+      notification.target == null
+    ) {
+      return (
+        <div className="text-danger">
+          <small>{t('audit_log_bulk_export.download_expired')}</small>
+        </div>
+      );
+    }
+    if (
+      notification.action ===
+      SupportedAction.ACTION_AUDIT_LOG_BULK_EXPORT_JOB_EXPIRED
+    ) {
+      return (
+        <div className="text-danger">
+          <small>{t('audit_log_bulk_export.job_expired')}</small>
+        </div>
+      );
+    }
+    if (
+      notification.action ===
+      SupportedAction.ACTION_AUDIT_LOG_BULK_EXPORT_NO_RESULTS
+    ) {
+      return (
+        <div className="text-danger">
+          <small>{t('audit_log_bulk_export.no_results')}</small>
+        </div>
+      );
+    }
+    return <></>;
+  };
+
+  const Notification = () => {
+    return (
+      <ModelNotification
+        notification={notification}
+        actionMsg={actionMsg}
+        actionIcon={actionIcon}
+        actionUsers={actionUsers}
+        hideActionUsers
+        hidePath
+        subMsg={getSubMsg()}
+      />
+    );
+  };
+
+  const clickLink =
+    notification.action ===
+      SupportedAction.ACTION_AUDIT_LOG_BULK_EXPORT_COMPLETED &&
+    notification.target?.attachment != null &&
+    isPopulated(notification.target?.attachment)
+      ? notification.target.attachment.downloadPathProxied
+      : undefined;
+
+  return {
+    Notification,
+    clickLink,
+    isDisabled: notification.target == null,
+  };
+};

+ 5 - 1
apps/app/src/client/components/InAppNotification/ModelNotification/ModelNotification.tsx

@@ -15,6 +15,7 @@ type Props = {
   actionIcon: string;
   actionUsers: string;
   hideActionUsers?: boolean;
+  hidePath?: boolean;
   subMsg?: JSX.Element;
 };
 
@@ -24,6 +25,7 @@ export const ModelNotification: FC<Props> = ({
   actionIcon,
   actionUsers,
   hideActionUsers = false,
+  hidePath = false,
   subMsg,
 }: Props) => {
   return (
@@ -31,7 +33,9 @@ export const ModelNotification: FC<Props> = ({
       <div className="text-truncate page-title">
         {hideActionUsers ? <></> : <b>{actionUsers}</b>}
         {` ${actionMsg}`}
-        <PagePathLabel path={notification.parsedSnapshot?.path ?? ''} />
+        {!hidePath && (
+          <PagePathLabel path={notification.parsedSnapshot?.path ?? ''} />
+        )}
       </div>
       {subMsg}
       <span className="material-symbols-outlined me-2">{actionIcon}</span>

+ 5 - 1
apps/app/src/client/components/InAppNotification/ModelNotification/index.tsx

@@ -3,6 +3,7 @@ import type { HasObjectId } from '@growi/core';
 
 import type { IInAppNotification } from '~/interfaces/in-app-notification';
 
+import { useAuditLogBulkExportJobModelNotification } from './AuditLogBulkExportJobModelNotification';
 import { usePageBulkExportJobModelNotification } from './PageBulkExportJobModelNotification';
 import { usePageModelNotification } from './PageModelNotification';
 import { useUserModelNotification } from './UserModelNotification';
@@ -23,11 +24,14 @@ export const useModelNotification = (
   const userModelNotificationUtils = useUserModelNotification(notification);
   const pageBulkExportResultModelNotificationUtils =
     usePageBulkExportJobModelNotification(notification);
+  const auditLogBulkExportJobModelNotificationUtils =
+    useAuditLogBulkExportJobModelNotification(notification);
 
   const modelNotificationUtils =
     pageModelNotificationUtils ??
     userModelNotificationUtils ??
-    pageBulkExportResultModelNotificationUtils;
+    pageBulkExportResultModelNotificationUtils ??
+    auditLogBulkExportJobModelNotificationUtils;
 
   return modelNotificationUtils;
 };

+ 13 - 0
apps/app/src/client/components/InAppNotification/ModelNotification/useActionAndMsg.ts

@@ -81,6 +81,19 @@ export const useActionMsgAndIconForModelNotification = (
       actionMsg = 'export failed for';
       actionIcon = 'error';
       break;
+    case SupportedAction.ACTION_AUDIT_LOG_BULK_EXPORT_COMPLETED:
+      actionMsg = 'audit log export completed';
+      actionIcon = 'download';
+      break;
+    case SupportedAction.ACTION_AUDIT_LOG_BULK_EXPORT_FAILED:
+    case SupportedAction.ACTION_AUDIT_LOG_BULK_EXPORT_JOB_EXPIRED:
+      actionMsg = 'audit log export failed';
+      actionIcon = 'error';
+      break;
+    case SupportedAction.ACTION_AUDIT_LOG_BULK_EXPORT_NO_RESULTS:
+      actionMsg = 'audit log export had no results';
+      actionIcon = 'error';
+      break;
     default:
       actionMsg = '';
       actionIcon = '';

+ 3 - 2
apps/app/src/server/service/in-app-notification.ts

@@ -3,6 +3,7 @@ import { SubscriptionStatusType } from '@growi/core';
 import { subDays } from 'date-fns/subDays';
 import type { FilterQuery, Types, UpdateQuery } from 'mongoose';
 
+import type { IAuditLogBulkExportJob } from '~/features/audit-log-bulk-export/interfaces/audit-log-bulk-export';
 import type { IPageBulkExportJob } from '~/features/page-bulk-export/interfaces/page-bulk-export';
 import { AllEssentialActions } from '~/interfaces/activity';
 import type { PaginateResult } from '~/interfaces/in-app-notification';
@@ -48,7 +49,7 @@ export default class InAppNotificationService {
       'updated',
       async (
         activity: ActivityDocument,
-        target: IUser | IPage | IPageBulkExportJob,
+        target: IUser | IPage | IPageBulkExportJob | IAuditLogBulkExportJob,
         preNotify: PreNotify,
       ) => {
         try {
@@ -224,7 +225,7 @@ export default class InAppNotificationService {
 
   createInAppNotification = async function (
     activity: ActivityDocument,
-    target: IUser | IPage | IPageBulkExportJob,
+    target: IUser | IPage | IPageBulkExportJob | IAuditLogBulkExportJob,
     preNotify: PreNotify,
   ): Promise<void> {
     const shouldNotification =

+ 4 - 3
apps/app/src/server/service/in-app-notification/in-app-notification-utils.ts

@@ -1,5 +1,6 @@
 import type { IPage, IUser } from '@growi/core';
 
+import type { IAuditLogBulkExportJob } from '~/features/audit-log-bulk-export/interfaces/audit-log-bulk-export';
 import type { IPageBulkExportJob } from '~/features/page-bulk-export/interfaces/page-bulk-export';
 import { SupportedTargetModel } from '~/interfaces/activity';
 import * as pageSerializers from '~/models/serializers/in-app-notification-snapshot/page';
@@ -7,14 +8,14 @@ import * as pageBulkExportJobSerializers from '~/models/serializers/in-app-notif
 
 const isIPage = (
   targetModel: string,
-  target: IUser | IPage | IPageBulkExportJob,
+  target: IUser | IPage | IPageBulkExportJob | IAuditLogBulkExportJob,
 ): target is IPage => {
   return targetModel === SupportedTargetModel.MODEL_PAGE;
 };
 
 const isIPageBulkExportJob = (
   targetModel: string,
-  target: IUser | IPage | IPageBulkExportJob,
+  target: IUser | IPage | IPageBulkExportJob | IAuditLogBulkExportJob,
 ): target is IPageBulkExportJob => {
   return targetModel === SupportedTargetModel.MODEL_PAGE_BULK_EXPORT_JOB;
 };
@@ -22,7 +23,7 @@ const isIPageBulkExportJob = (
 // snapshots are infos about the target that are displayed in the notification, which should not change on target update/deletion
 export const generateSnapshot = async (
   targetModel: string,
-  target: IUser | IPage | IPageBulkExportJob,
+  target: IUser | IPage | IPageBulkExportJob | IAuditLogBulkExportJob,
 ): Promise<string | undefined> => {
   let snapshot: string | undefined;