Shun Miyazawa 3 лет назад
Родитель
Сommit
dc475870c2

+ 23 - 7
packages/app/src/components/Admin/AuditLog/SearchUsernameInput.tsx

@@ -10,18 +10,24 @@ import { useSWRxUsernames } from '~/stores/user';
 const Categories = {
   activeUser: 'Active User',
   inactiveUser: 'Inactive User',
-  activitySnapshotUser: 'ActivitySnapshot User',
+  activitySnapshotUser: 'Activity Snapshot User',
 } as const;
 
 type CategorieType = typeof Categories[keyof typeof Categories]
 
-type AllUser = {
+type UserDataType = {
   username: string
   category: CategorieType
 }
 
-export const SearchUsernameInput: FC = () => {
-  const [searchKeyword, setSearchKeyword] = useState('');
+type Props = {
+  onChange: (text) => void
+}
+
+export const SearchUsernameInput: FC<Props> = (props: Props) => {
+  const { onChange } = props;
+
+  const [searchKeyword, setSearchKeyword] = useState<string>('');
 
   const includeUserOptions = {
     isIncludeActiveUser: true,
@@ -34,7 +40,7 @@ export const SearchUsernameInput: FC = () => {
   const activitySnapshotUsernames = usernameData?.activitySnapshotUser?.usernames != null ? usernameData.activitySnapshotUser.usernames : [];
   const isLoading = usernameData === undefined && error == null;
 
-  const allUser: AllUser[] = [];
+  const allUser: UserDataType[] = [];
   const pushToAllUser = (usernames: string[], category: CategorieType) => {
     usernames.forEach(username => allUser.push({ username, category }));
   };
@@ -42,11 +48,18 @@ export const SearchUsernameInput: FC = () => {
   pushToAllUser(inactiveUsernames, Categories.inactiveUser);
   pushToAllUser(activitySnapshotUsernames, Categories.activitySnapshotUser);
 
-  const searchHandler = useCallback(async(text) => {
+  const changeHandler = useCallback((userData: UserDataType[]) => {
+    const usernames = userData.map(user => user.username);
+    if (onChange != null) {
+      onChange(usernames);
+    }
+  }, [onChange]);
+
+  const searchHandler = useCallback((text) => {
     setSearchKeyword(text);
   }, []);
 
-  const renderMenu = useCallback((allUser: AllUser[], menuProps) => {
+  const renderMenu = useCallback((allUser: UserDataType[], menuProps) => {
     if (allUser == null || allUser.length === 0) {
       return <></>;
     }
@@ -85,6 +98,8 @@ export const SearchUsernameInput: FC = () => {
       </div>
       <AsyncTypeahead
         id="auditlog-username-typeahead-asynctypeahead"
+        multiple
+        delay={1000}
         placeholder="username"
         labelKey={option => `${option.username}`}
         caseSensitive={false}
@@ -93,6 +108,7 @@ export const SearchUsernameInput: FC = () => {
         newSelectionPrefix=""
         options={allUser}
         onSearch={searchHandler}
+        onChange={changeHandler}
         renderMenu={renderMenu}
       />
     </div>

+ 9 - 2
packages/app/src/components/Admin/AuditLogManagement.tsx

@@ -35,6 +35,7 @@ export const AuditLogManagement: FC = () => {
   const offset = (activePage - 1) * PAGING_LIMIT;
   const [startDate, setStartDate] = useState<Date | null>(null);
   const [endDate, setEndDate] = useState<Date | null>(null);
+  const [selectedUsernames, setSelectedUsernames] = useState<string[]>([]);
   const [actionMap, setActionMap] = useState(
     new Map<SupportedActionType, boolean>(AllSupportedActionType.map(action => [action, true])),
   );
@@ -44,7 +45,7 @@ export const AuditLogManagement: FC = () => {
    */
   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, date: selectedDate };
+  const searchFilter = { action: selectedActionList, date: selectedDate, username: selectedUsernames };
 
   const { data: activityListData, error } = useSWRxActivityList(PAGING_LIMIT, offset, searchFilter);
   const activityList = activityListData?.docs != null ? activityListData.docs : [];
@@ -76,6 +77,10 @@ export const AuditLogManagement: FC = () => {
     setActionMap(new Map(actionMap.entries()));
   }, [actionMap, setActionMap]);
 
+  const setUsernamesHandler = useCallback((usernames: string[]) => {
+    setSelectedUsernames(usernames);
+  }, []);
+
   // 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/>`;
 
@@ -84,7 +89,9 @@ export const AuditLogManagement: FC = () => {
       <h2 className="admin-setting-header mb-3">{t('AuditLog')}</h2>
 
       <div className="form-inline mb-3">
-        <SearchUsernameInput />
+        <SearchUsernameInput
+          onChange={setUsernamesHandler}
+        />
 
         <DateRangePicker
           startDate={startDate}

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

@@ -48,8 +48,9 @@ module.exports = (crowi: Crowi): Router => {
     try {
       const parsedSearchFilter = JSON.parse(req.query.searchFilter as string);
 
-      // add username to query
-      if (typeof parsedSearchFilter.username === 'string') {
+      // amdd username to query
+      const canContainUsernameFilterToQuery = parsedSearchFilter.username.every(u => typeof u === 'string');
+      if (canContainUsernameFilterToQuery && parsedSearchFilter.username.length !== 0) {
         Object.assign(query, { 'snapshot.username': parsedSearchFilter.username });
       }
 

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

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