瀏覽代碼

Merge pull request #5936 from weseek/imprv/94757audit-log-ui-ux-improvement

imprv: Audit log  UI / UX improvement
Yuki Takei 3 年之前
父節點
當前提交
5d890fcb63

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

@@ -514,5 +514,11 @@
       "force_update_parents_label": "Forcibly add missing users",
       "force_update_parents_label": "Forcibly add missing users",
       "force_update_parents_description": "Enable this option to force the addition of missing users to the ancestor groups if they exist after changing a parent group."
       "force_update_parents_description": "Enable this option to force the addition of missing users to the ancestor groups if they exist after changing a parent group."
     }
     }
+  },
+  "audit_log_management": {
+    "username": "Username",
+    "target_model": "Target Model",
+    "action": "Action",
+    "date": "Date"
   }
   }
 }
 }

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

@@ -513,5 +513,11 @@
       "force_update_parents_label": "強制的に足りないユーザーを追加する",
       "force_update_parents_label": "強制的に足りないユーザーを追加する",
       "force_update_parents_description": "このオプションを有効化すると、親グループ変更後に祖先グループに足りないユーザーが存在した場合にそれらのユーザーを強制的に追加することができます"
       "force_update_parents_description": "このオプションを有効化すると、親グループ変更後に祖先グループに足りないユーザーが存在した場合にそれらのユーザーを強制的に追加することができます"
     }
     }
+  },
+  "audit_log_management": {
+    "username": "ユーザー名",
+    "target_model": "ターゲットモデル",
+    "action": "アクション",
+    "date": "日付"
   }
   }
 }
 }

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

@@ -523,5 +523,11 @@
       "force_update_parents_label": "强行添加失踪的用户",
       "force_update_parents_label": "强行添加失踪的用户",
       "force_update_parents_description": "激活这个选项,如果在父组改变后,在祖先组中有缺失的用户,可以强制添加这些用户"
       "force_update_parents_description": "激活这个选项,如果在父组改变后,在祖先组中有缺失的用户,可以强制添加这些用户"
     }
     }
+  },
+  "audit_log_management": {
+    "username": "帐号",
+    "target_model": "目标模型",
+    "action": "行动",
+    "date": "日期"
   }
   }
 }
 }

+ 7 - 4
packages/app/src/components/Admin/AuditLog/ActivityTable.tsx

@@ -1,6 +1,7 @@
 import React, { FC } from 'react';
 import React, { FC } from 'react';
 
 
 import { format } from 'date-fns';
 import { format } from 'date-fns';
+import { useTranslation } from 'react-i18next';
 
 
 import { IActivityHasId } from '~/interfaces/activity';
 import { IActivityHasId } from '~/interfaces/activity';
 
 
@@ -13,15 +14,17 @@ const formatDate = (date) => {
 };
 };
 
 
 export const ActivityTable : FC<Props> = (props: Props) => {
 export const ActivityTable : FC<Props> = (props: Props) => {
+  const { t } = useTranslation();
+
   return (
   return (
     <div className="table-responsive text-nowrap h-100">
     <div className="table-responsive text-nowrap h-100">
       <table className="table table-default table-bordered table-user-list">
       <table className="table table-default table-bordered table-user-list">
         <thead>
         <thead>
           <tr>
           <tr>
-            <th scope="col">username</th>
-            <th scope="col">targetModel</th>
-            <th scope="col">action</th>
-            <th scope="col">createdAt</th>
+            <th scope="col">{t('admin:audit_log_management.username')}</th>
+            <th scope="col">{t('admin:audit_log_management.target_model')}</th>
+            <th scope="col">{t('admin:audit_log_management.action')}</th>
+            <th scope="col">{t('admin:audit_log_management.date')}</th>
           </tr>
           </tr>
         </thead>
         </thead>
         <tbody>
         <tbody>

+ 12 - 9
packages/app/src/components/Admin/AuditLog/DateRangePicker.tsx

@@ -5,6 +5,8 @@ import React, {
 import DatePicker from 'react-datepicker';
 import DatePicker from 'react-datepicker';
 import 'react-datepicker/dist/react-datepicker.css';
 import 'react-datepicker/dist/react-datepicker.css';
 
 
+import { useTranslation } from 'react-i18next';
+
 
 
 type CustomInputProps = {
 type CustomInputProps = {
   buttonRef: React.Ref<HTMLButtonElement>
   buttonRef: React.Ref<HTMLButtonElement>
@@ -12,6 +14,7 @@ type CustomInputProps = {
 }
 }
 
 
 const CustomInput = forwardRef<HTMLButtonElement, CustomInputProps>((props: CustomInputProps) => {
 const CustomInput = forwardRef<HTMLButtonElement, CustomInputProps>((props: CustomInputProps) => {
+  const { t } = useTranslation();
   return (
   return (
     <button
     <button
       type="button"
       type="button"
@@ -19,7 +22,7 @@ const CustomInput = forwardRef<HTMLButtonElement, CustomInputProps>((props: Cust
       ref={props.buttonRef}
       ref={props.buttonRef}
       onClick={props.onClick}
       onClick={props.onClick}
     >
     >
-      <i className="fa fa-fw fa-calendar" /> Date
+      <i className="fa fa-fw fa-calendar" /> {t('admin:audit_log_management.date')}
     </button>
     </button>
   );
   );
 });
 });
@@ -28,26 +31,26 @@ const CustomInput = forwardRef<HTMLButtonElement, CustomInputProps>((props: Cust
 type DateRangePickerProps = {
 type DateRangePickerProps = {
   startDate: Date | null
   startDate: Date | null
   endDate: Date | null
   endDate: Date | null
-  onChangeDatePicker: (dateList: Date[] | null[]) => void
+  onChange: (dateList: Date[] | null[]) => void
 }
 }
 
 
 export const DateRangePicker: FC<DateRangePickerProps> = (props: DateRangePickerProps) => {
 export const DateRangePicker: FC<DateRangePickerProps> = (props: DateRangePickerProps) => {
-  const { startDate, endDate } = props;
+  const { startDate, endDate, onChange } = props;
 
 
   const buttonRef = useRef(null);
   const buttonRef = useRef(null);
 
 
-  const datePickerChangedHandler = useCallback((dateList: Date[] | null[]) => {
-    if (props.onChangeDatePicker != null) {
+  const changeHandler = useCallback((dateList: Date[] | null[]) => {
+    if (onChange != null) {
       const [start, end] = dateList;
       const [start, end] = dateList;
       const isSameTime = (start != null && end != null) && (start.getTime() === end.getTime());
       const isSameTime = (start != null && end != null) && (start.getTime() === end.getTime());
       if (isSameTime) {
       if (isSameTime) {
-        props.onChangeDatePicker([null, null]);
+        onChange([null, null]);
       }
       }
       else {
       else {
-        props.onChangeDatePicker(dateList);
+        onChange(dateList);
       }
       }
     }
     }
-  }, []);
+  }, [onChange]);
 
 
   return (
   return (
     <div className="btn-group mr-2">
     <div className="btn-group mr-2">
@@ -55,7 +58,7 @@ export const DateRangePicker: FC<DateRangePickerProps> = (props: DateRangePicker
         selectsRange
         selectsRange
         startDate={startDate}
         startDate={startDate}
         endDate={endDate}
         endDate={endDate}
-        onChange={datePickerChangedHandler}
+        onChange={changeHandler}
         customInput={<CustomInput buttonRef={buttonRef} />}
         customInput={<CustomInput buttonRef={buttonRef} />}
       />
       />
     </div>
     </div>

+ 7 - 5
packages/app/src/components/Admin/AuditLog/SearchUsernameTypeahead.tsx

@@ -3,6 +3,7 @@ import React, {
 } from 'react';
 } from 'react';
 
 
 import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
+import { useTranslation } from 'react-i18next';
 
 
 import { useSWRxUsernames } from '~/stores/user';
 import { useSWRxUsernames } from '~/stores/user';
 
 
@@ -13,11 +14,11 @@ const Categories = {
   activitySnapshotUser: 'Activity Snapshot User',
   activitySnapshotUser: 'Activity Snapshot User',
 } as const;
 } as const;
 
 
-type CategorieType = typeof Categories[keyof typeof Categories]
+type CategoryType = typeof Categories[keyof typeof Categories]
 
 
 type UserDataType = {
 type UserDataType = {
   username: string
   username: string
-  category: CategorieType
+  category: CategoryType
 }
 }
 
 
 type Props = {
 type Props = {
@@ -26,6 +27,7 @@ type Props = {
 
 
 export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
 export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
   const { onChange } = props;
   const { onChange } = props;
+  const { t } = useTranslation();
 
 
   /*
   /*
    * State
    * State
@@ -43,7 +45,7 @@ export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
   const isLoading = usernameData === undefined && error == null;
   const isLoading = usernameData === undefined && error == null;
 
 
   const allUser: UserDataType[] = [];
   const allUser: UserDataType[] = [];
-  const pushToAllUser = (usernames: string[], category: CategorieType) => {
+  const pushToAllUser = (usernames: string[], category: CategoryType) => {
     usernames.forEach(username => allUser.push({ username, category }));
     usernames.forEach(username => allUser.push({ username, category }));
   };
   };
   pushToAllUser(activeUsernames, Categories.activeUser);
   pushToAllUser(activeUsernames, Categories.activeUser);
@@ -102,11 +104,11 @@ export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
         </span>
         </span>
       </div>
       </div>
       <AsyncTypeahead
       <AsyncTypeahead
-        id="auditlog-username-typeahead-asynctypeahead"
+        id="search-username-typeahead-asynctypeahead"
         multiple
         multiple
         delay={400}
         delay={400}
         minLength={0}
         minLength={0}
-        placeholder="username"
+        placeholder={t('admin:audit_log_management.username')}
         caseSensitive={false}
         caseSensitive={false}
         isLoading={isLoading}
         isLoading={isLoading}
         options={allUser}
         options={allUser}

+ 4 - 1
packages/app/src/components/Admin/AuditLog/SelectActionDropdown.tsx

@@ -1,5 +1,7 @@
 import React, { FC, useCallback } from 'react';
 import React, { FC, useCallback } from 'react';
 
 
+import { useTranslation } from 'react-i18next';
+
 import { SupportedActionType } from '~/interfaces/activity';
 import { SupportedActionType } from '~/interfaces/activity';
 
 
 type Props = {
 type Props = {
@@ -10,6 +12,7 @@ type Props = {
 }
 }
 
 
 export const SelectActionDropdown: FC<Props> = (props: Props) => {
 export const SelectActionDropdown: FC<Props> = (props: Props) => {
+  const { t } = useTranslation();
   const {
   const {
     dropdownItems, actionMap, onChangeAction, onChangeMultipleAction,
     dropdownItems, actionMap, onChangeAction, onChangeMultipleAction,
   } = props;
   } = props;
@@ -29,7 +32,7 @@ export const SelectActionDropdown: FC<Props> = (props: Props) => {
   return (
   return (
     <div className="btn-group mr-2">
     <div className="btn-group mr-2">
       <button className="btn btn-outline-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown">
       <button className="btn btn-outline-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown">
-        <i className="fa fa-fw fa-bolt" />Action
+        <i className="fa fa-fw fa-bolt" />{t('admin:audit_log_management.action')}
       </button>
       </button>
       <ul className="dropdown-menu" aria-labelledby="dropdownMenuButton">
       <ul className="dropdown-menu" aria-labelledby="dropdownMenuButton">
         {dropdownItems.map(item => (
         {dropdownItems.map(item => (

+ 33 - 23
packages/app/src/components/Admin/AuditLogManagement.tsx

@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
 import {
 import {
   SupportedActionType, AllSupportedActionType, PageActions, CommentActions,
   SupportedActionType, AllSupportedActionType, PageActions, CommentActions,
 } from '~/interfaces/activity';
 } from '~/interfaces/activity';
-import { useSWRxActivityList } from '~/stores/activity';
+import { useSWRxActivity } from '~/stores/activity';
 
 
 import PaginationWrapper from '../PaginationWrapper';
 import PaginationWrapper from '../PaginationWrapper';
 
 
@@ -47,10 +47,10 @@ export const AuditLogManagement: FC = () => {
   const selectedActionList = Array.from(actionMap.entries()).filter(v => v[1]).map(v => v[0]);
   const selectedActionList = Array.from(actionMap.entries()).filter(v => v[1]).map(v => v[0]);
   const searchFilter = { actions: selectedActionList, dates: selectedDate, usernames: selectedUsernames };
   const searchFilter = { actions: selectedActionList, dates: selectedDate, usernames: selectedUsernames };
 
 
-  const { data: activityListData, error } = useSWRxActivityList(PAGING_LIMIT, offset, searchFilter);
-  const activityList = activityListData?.docs != null ? activityListData.docs : [];
-  const totalActivityNum = activityListData?.totalDocs != null ? activityListData.totalDocs : 0;
-  const isLoading = activityListData === undefined && error == null;
+  const { data: activityData, mutate: mutateActivity, error } = useSWRxActivity(PAGING_LIMIT, offset, searchFilter);
+  const activityList = activityData?.docs != null ? activityData.docs : [];
+  const totalActivityNum = activityData?.totalDocs != null ? activityData.totalDocs : 0;
+  const isLoading = activityData === undefined && error == null;
 
 
   /*
   /*
    * Functions
    * Functions
@@ -78,9 +78,15 @@ export const AuditLogManagement: FC = () => {
   }, [actionMap, setActionMap]);
   }, [actionMap, setActionMap]);
 
 
   const setUsernamesHandler = useCallback((usernames: string[]) => {
   const setUsernamesHandler = useCallback((usernames: string[]) => {
+    setActivePage(1);
     setSelectedUsernames(usernames);
     setSelectedUsernames(usernames);
   }, []);
   }, []);
 
 
+  const reloadButtonPushedHandler = useCallback(() => {
+    setActivePage(1);
+    mutateActivity();
+  }, [mutateActivity]);
+
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
   const activityCounter = `<b>${activityList.length === 0 ? 0 : offset + 1}</b> - <b>${(PAGING_LIMIT * activePage) - (PAGING_LIMIT - activityList.length)}</b> of <b>${totalActivityNum}<b/>`;
   const activityCounter = `<b>${activityList.length === 0 ? 0 : offset + 1}</b> - <b>${(PAGING_LIMIT * activePage) - (PAGING_LIMIT - activityList.length)}</b> of <b>${totalActivityNum}<b/>`;
 
 
@@ -96,7 +102,7 @@ export const AuditLogManagement: FC = () => {
         <DateRangePicker
         <DateRangePicker
           startDate={startDate}
           startDate={startDate}
           endDate={endDate}
           endDate={endDate}
-          onChangeDatePicker={datePickerChangedHandler}
+          onChange={datePickerChangedHandler}
         />
         />
 
 
         <SelectActionDropdown
         <SelectActionDropdown
@@ -108,33 +114,37 @@ export const AuditLogManagement: FC = () => {
           onChangeAction={actionCheckboxChangedHandler}
           onChangeAction={actionCheckboxChangedHandler}
           onChangeMultipleAction={multipleActionCheckboxChangedHandler}
           onChangeMultipleAction={multipleActionCheckboxChangedHandler}
         />
         />
+
+        <button type="button" className="btn ml-auto grw-btn-reload" onClick={reloadButtonPushedHandler}>
+          <i className="icon icon-reload" />
+        </button>
       </div>
       </div>
 
 
+      <p
+        className="ml-2"
+        // eslint-disable-next-line react/no-danger
+        dangerouslySetInnerHTML={{ __html: activityCounter }}
+      />
+
       { isLoading
       { isLoading
         ? (
         ? (
           <div className="text-muted text-center mb-5">
           <div className="text-muted text-center mb-5">
-            <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
+            <i className="fa fa-2x fa-spinner fa-pulse mr-1" />
           </div>
           </div>
         )
         )
         : (
         : (
-          <>
-            <p
-              className="ml-2"
-              // eslint-disable-next-line react/no-danger
-              dangerouslySetInnerHTML={{ __html: activityCounter }}
-            />
-            <ActivityTable activityList={activityList} />
-            <PaginationWrapper
-              activePage={activePage}
-              changePage={setActivePageHandler}
-              totalItemsCount={totalActivityNum}
-              pagingLimit={PAGING_LIMIT}
-              align="center"
-              size="sm"
-            />
-          </>
+          <ActivityTable activityList={activityList} />
         )
         )
       }
       }
+
+      <PaginationWrapper
+        activePage={activePage}
+        changePage={setActivePageHandler}
+        totalItemsCount={totalActivityNum}
+        pagingLimit={PAGING_LIMIT}
+        align="center"
+        size="sm"
+      />
     </div>
     </div>
   );
   );
 };
 };

+ 0 - 2
packages/app/src/server/routes/apiv3/activity.ts

@@ -48,8 +48,6 @@ module.exports = (crowi: Crowi): Router => {
     try {
     try {
       const parsedSearchFilter = JSON.parse(req.query.searchFilter as string);
       const parsedSearchFilter = JSON.parse(req.query.searchFilter as string);
 
 
-      console.log(parsedSearchFilter);
-
       // add username to query
       // add username to query
       const canContainUsernameFilterToQuery = parsedSearchFilter.usernames.every(u => typeof u === 'string');
       const canContainUsernameFilterToQuery = parsedSearchFilter.usernames.every(u => typeof u === 'string');
       if (canContainUsernameFilterToQuery && parsedSearchFilter.usernames.length > 0) {
       if (canContainUsernameFilterToQuery && parsedSearchFilter.usernames.length > 0) {

+ 1 - 1
packages/app/src/stores/activity.ts

@@ -12,7 +12,7 @@ type ISearchFilter = {
   actions?: SupportedActionType[]
   actions?: SupportedActionType[]
 }
 }
 
 
-export const useSWRxActivityList = (limit?: number, offset?: number, searchFilter?: ISearchFilter): SWRResponse<PaginateResult<IActivityHasId>, Error> => {
+export const useSWRxActivity = (limit?: number, offset?: number, searchFilter?: ISearchFilter): SWRResponse<PaginateResult<IActivityHasId>, Error> => {
   const stringifiedSearchFilter = JSON.stringify(searchFilter);
   const stringifiedSearchFilter = JSON.stringify(searchFilter);
   return useSWRImmutable(
   return useSWRImmutable(
     ['/activity', limit, offset, stringifiedSearchFilter],
     ['/activity', limit, offset, stringifiedSearchFilter],