Răsfoiți Sursa

Merge pull request #5853 from weseek/feat/94792-find-activities-between-two-dates

feat: Find activities between two dates
Shun Miyazawa 3 ani în urmă
părinte
comite
41649d02ea

+ 63 - 0
packages/app/src/components/Admin/AuditLog/DateRangePicker.tsx

@@ -0,0 +1,63 @@
+import React, {
+  FC, useRef, forwardRef, useCallback,
+} from 'react';
+
+import DatePicker from 'react-datepicker';
+import 'react-datepicker/dist/react-datepicker.css';
+
+
+type CustomInputProps = {
+  buttonRef: React.Ref<HTMLButtonElement>
+  onClick?: () => void;
+}
+
+const CustomInput = forwardRef<HTMLButtonElement, CustomInputProps>((props: CustomInputProps) => {
+  return (
+    <button
+      type="button"
+      className="btn btn-outline-secondary dropdown-toggle"
+      ref={props.buttonRef}
+      onClick={props.onClick}
+    >
+      <i className="fa fa-fw fa-calendar" /> Date
+    </button>
+  );
+});
+
+
+type DateRangePickerProps = {
+  startDate: Date | null
+  endDate: Date | null
+  onChangeDatePicker: (dateList: Date[] | null[]) => void
+}
+
+export const DateRangePicker: FC<DateRangePickerProps> = (props: DateRangePickerProps) => {
+  const { startDate, endDate } = props;
+
+  const buttonRef = useRef(null);
+
+  const datePickerChangedHandler = useCallback((dateList: Date[] | null[]) => {
+    if (props.onChangeDatePicker != null) {
+      const [start, end] = dateList;
+      const isSameTime = (start != null && end != null) && (start.getTime() === end.getTime());
+      if (isSameTime) {
+        props.onChangeDatePicker([null, null]);
+      }
+      else {
+        props.onChangeDatePicker(dateList);
+      }
+    }
+  }, []);
+
+  return (
+    <div className="btn-group mr-2 mb-3">
+      <DatePicker
+        selectsRange
+        startDate={startDate}
+        endDate={endDate}
+        onChange={datePickerChangedHandler}
+        customInput={<CustomInput buttonRef={buttonRef} />}
+      />
+    </div>
+  );
+};

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

@@ -5,26 +5,26 @@ import { SupportedActionType } from '~/interfaces/activity';
 type Props = {
   dropdownItems: Array<{actionCategory: string, actionNames: SupportedActionType[]}>
   actionMap: Map<SupportedActionType, boolean>
-  onSelectAction: (action: SupportedActionType) => void
-  onSelectMultipleAction: (actions: SupportedActionType[], isChecked: boolean) => void
+  onChangeAction: (action: SupportedActionType) => void
+  onChangeMultipleAction: (actions: SupportedActionType[], isChecked: boolean) => void
 }
 
 export const SelectActionDropdown: FC<Props> = (props: Props) => {
   const {
-    dropdownItems, actionMap, onSelectAction, onSelectMultipleAction,
+    dropdownItems, actionMap, onChangeAction, onChangeMultipleAction,
   } = props;
 
-  const selectActionCheckboxChangedHandler = useCallback((action) => {
-    if (onSelectAction != null) {
-      onSelectAction(action);
+  const actionCheckboxChangedHandler = useCallback((action) => {
+    if (onChangeAction != null) {
+      onChangeAction(action);
     }
-  }, [onSelectAction]);
+  }, [onChangeAction]);
 
-  const selectMultipleActionCheckboxChangedHandler = useCallback((actions, isChecked) => {
-    if (onSelectMultipleAction != null) {
-      onSelectMultipleAction(actions, isChecked);
+  const multipleActionCheckboxChangedHandler = useCallback((actions, isChecked) => {
+    if (onChangeMultipleAction != null) {
+      onChangeMultipleAction(actions, isChecked);
     }
-  }, [onSelectMultipleAction]);
+  }, [onChangeMultipleAction]);
 
   return (
     <div className="btn-group mr-2 mb-3">
@@ -40,7 +40,7 @@ export const SelectActionDropdown: FC<Props> = (props: Props) => {
                   type="checkbox"
                   className="form-check-input"
                   defaultChecked
-                  onChange={(e) => { selectMultipleActionCheckboxChangedHandler(item.actionNames, e.target.checked) }}
+                  onChange={(e) => { multipleActionCheckboxChangedHandler(item.actionNames, e.target.checked) }}
                 />
                 <label className="form-check-label">{item.actionCategory}</label>
               </div>
@@ -53,7 +53,7 @@ export const SelectActionDropdown: FC<Props> = (props: Props) => {
                       type="checkbox"
                       className="form-check-input"
                       id={`checkbox${action}`}
-                      onChange={() => { selectActionCheckboxChangedHandler(action) }}
+                      onChange={() => { actionCheckboxChangedHandler(action) }}
                       checked={actionMap.get(action)}
                     />
                     <label

+ 32 - 5
packages/app/src/components/Admin/AuditLogManagement.tsx

@@ -1,5 +1,6 @@
 import React, { FC, useState, useCallback } from 'react';
 
+import { format } from 'date-fns';
 import { useTranslation } from 'react-i18next';
 
 import {
@@ -10,8 +11,17 @@ import { useSWRxActivityList } from '~/stores/activity';
 import PaginationWrapper from '../PaginationWrapper';
 
 import { ActivityTable } from './AuditLog/ActivityTable';
+import { DateRangePicker } from './AuditLog/DateRangePicker';
 import { SelectActionDropdown } from './AuditLog/SelectActionDropdown';
 
+
+const formatDate = (date: Date | null) => {
+  if (date == null) {
+    return '';
+  }
+  return format(new Date(date), 'yyyy/MM/dd');
+};
+
 const PAGING_LIMIT = 10;
 
 export const AuditLogManagement: FC = () => {
@@ -22,6 +32,8 @@ export const AuditLogManagement: FC = () => {
    */
   const [activePage, setActivePage] = useState<number>(1);
   const offset = (activePage - 1) * PAGING_LIMIT;
+  const [startDate, setStartDate] = useState<Date | null>(null);
+  const [endDate, setEndDate] = useState<Date | null>(null);
   const [actionMap, setActionMap] = useState(
     new Map<SupportedActionType, boolean>(AllSupportedActionType.map(action => [action, true])),
   );
@@ -29,8 +41,9 @@ export const AuditLogManagement: FC = () => {
   /*
    * Fetch
    */
+  const selectedDate = { startDate: formatDate(startDate), endDate: formatDate(endDate) };
   const selectedActionList = Array.from(actionMap.entries()).filter(v => v[1]).map(v => v[0]);
-  const searchFilter = { action: selectedActionList };
+  const searchFilter = { action: selectedActionList, date: selectedDate };
 
   const { data: activityListData, error } = useSWRxActivityList(PAGING_LIMIT, offset, searchFilter);
   const activityList = activityListData?.docs != null ? activityListData.docs : [];
@@ -44,13 +57,20 @@ export const AuditLogManagement: FC = () => {
     setActivePage(selectedPageNum);
   }, []);
 
-  const selectActionCheckboxChangedHandler = useCallback((action: SupportedActionType) => {
+  const datePickerChangedHandler = useCallback((dateList: Date[] | null[]) => {
+    console.log(dateList);
+    setActivePage(1);
+    setStartDate(dateList[0]);
+    setEndDate(dateList[1]);
+  }, []);
+
+  const actionCheckboxChangedHandler = useCallback((action: SupportedActionType) => {
     setActivePage(1);
     actionMap.set(action, !actionMap.get(action));
     setActionMap(new Map(actionMap.entries()));
   }, [actionMap, setActionMap]);
 
-  const selectMultipleActionCheckboxChangedHandler = useCallback((actions: SupportedActionType[], isChecked) => {
+  const multipleActionCheckboxChangedHandler = useCallback((actions: SupportedActionType[], isChecked) => {
     setActivePage(1);
     actions.forEach(action => actionMap.set(action, isChecked));
     setActionMap(new Map(actionMap.entries()));
@@ -62,14 +82,21 @@ export const AuditLogManagement: FC = () => {
   return (
     <div data-testid="admin-auditlog">
       <h2 className="admin-setting-header mb-3">{t('AuditLog')}</h2>
+
+      <DateRangePicker
+        startDate={startDate}
+        endDate={endDate}
+        onChangeDatePicker={datePickerChangedHandler}
+      />
+
       <SelectActionDropdown
         dropdownItems={[
           { actionCategory: 'Page', actionNames: PageActions },
           { actionCategory: 'Comment', actionNames: CommentActions },
         ]}
         actionMap={actionMap}
-        onSelectAction={selectActionCheckboxChangedHandler}
-        onSelectMultipleAction={selectMultipleActionCheckboxChangedHandler}
+        onChangeAction={actionCheckboxChangedHandler}
+        onChangeMultipleAction={multipleActionCheckboxChangedHandler}
       />
 
       { isLoading

+ 38 - 5
packages/app/src/server/routes/apiv3/activity.ts

@@ -1,3 +1,4 @@
+import { parse, addMinutes, isValid } from 'date-fns';
 import express, { Request, Router } from 'express';
 import rateLimit from 'express-rate-limit';
 import { query } from 'express-validator';
@@ -43,13 +44,45 @@ module.exports = (crowi: Crowi): Router => {
     const limit = req.query.limit || await crowi.configManager?.getConfig('crowi', 'customize:showPageLimitationS') || 10;
     const offset = req.query.offset || 1;
 
+    const query = {};
+
     try {
-      const parsedSearchFilter = JSON.parse(req.query.searchFilter as string || '');
+      const parsedSearchFilter = JSON.parse(req.query.searchFilter as string);
+
+      // add action to query
       const canContainActionFilterToQuery = parsedSearchFilter.action.every(a => AllSupportedActionType.includes(a));
-      const query = {
-        action: canContainActionFilterToQuery ? parsedSearchFilter.action : [],
-      };
+      if (canContainActionFilterToQuery) {
+        Object.assign(query, { action: parsedSearchFilter.action });
+      }
+
+      // add date to query
+      const startDate = parse(parsedSearchFilter.date.startDate, 'yyyy/MM/dd', new Date());
+      const endDate = parse(parsedSearchFilter.date.endDate, 'yyyy/MM/dd', new Date());
+      if (isValid(startDate) && isValid(endDate)) {
+        Object.assign(query, {
+          createdAt: {
+            $gte: startDate,
+            // + 23 hours 59 minutes
+            $lt: addMinutes(endDate, 1439),
+          },
+        });
+      }
+      else if (isValid(startDate) && !isValid(endDate)) {
+        Object.assign(query, {
+          createdAt: {
+            $gte: startDate,
+            // + 23 hours 59 minutes
+            $lt: addMinutes(startDate, 1439),
+          },
+        });
+      }
+    }
+    catch (err) {
+      logger.error('Invalid value', err);
+      return res.apiv3Err(err, 400);
+    }
 
+    try {
       const paginationResult = await Activity.getPaginatedActivity(limit, offset, query);
 
       const User = crowi.model('User');
@@ -69,7 +102,7 @@ module.exports = (crowi: Crowi): Router => {
     }
     catch (err) {
       logger.error('Failed to get paginated activity', err);
-      return res.apiv3Err(err);
+      return res.apiv3Err(err, 500);
     }
   });
 

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

@@ -7,6 +7,7 @@ import { PaginateResult } from '../interfaces/mongoose-utils';
 
 
 type ISearchFilter = {
+  date?: {startDate: string | null, endDate: string | null}
   action?: SupportedActionType[]
 }