Преглед изворни кода

Merge pull request #5775 from weseek/feat/93691-93694-swrize-activity-list-and-show-on-table

feat: SWRize Activity list and show on table
Yuki Takei пре 4 година
родитељ
комит
6f3c2fe4c4

+ 2 - 2
packages/app/src/client/admin.jsx

@@ -31,7 +31,7 @@ import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
 import AdminHome from '../components/Admin/AdminHome/AdminHome';
 import AppSettingsPage from '../components/Admin/App/AppSettingsPage';
-import AuditLog from '../components/Admin/AuditLog';
+import { AuditLogManagement } from '../components/Admin/AuditLogManagement';
 import AdminNavigation from '../components/Admin/Common/AdminNavigation';
 import Customize from '../components/Admin/Customize/Customize';
 import ExportArchiveDataPage from '../components/Admin/ExportArchiveDataPage';
@@ -109,7 +109,7 @@ Object.assign(componentMappings, {
   'admin-user-group-detail': <UserGroupDetailPage />,
   'admin-full-text-search-management': <FullTextSearchManagement />,
   'admin-user-group-page': <UserGroupPage />,
-  'admin-audit-log': <AuditLog />,
+  'admin-audit-log': <AuditLogManagement />,
   'admin-navigation': <AdminNavigation />,
 });
 

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

@@ -1,9 +0,0 @@
-import React, { FC } from 'react';
-
-const AuditLog: FC = () => {
-  return (
-    <>Hello, AuditLog</>
-  );
-};
-
-export default AuditLog;

+ 42 - 0
packages/app/src/components/Admin/AuditLog/ActivityTable.tsx

@@ -0,0 +1,42 @@
+import React, { FC } from 'react';
+
+import { format } from 'date-fns';
+
+import { IActivityHasId } from '~/interfaces/activity';
+
+type Props = {
+  activityList: IActivityHasId[]
+}
+
+const formatDate = (date) => {
+  return format(new Date(date), 'yyyy/MM/dd HH:mm:ss');
+};
+
+export const ActivityTable : FC<Props> = (props: Props) => {
+  return (
+    <div className="table-responsive text-nowrap h-100">
+      <table className="table table-default table-bordered table-user-list">
+        <thead>
+          <tr>
+            <th scope="col">username</th>
+            <th scope="col">targetModel</th>
+            <th scope="col">action</th>
+            <th scope="col">createdAt</th>
+          </tr>
+        </thead>
+        <tbody>
+          {props.activityList.map((activity) => {
+            return (
+              <tr data-testid="activity-table" key={activity._id}>
+                <td>{activity.user?.username}</td>
+                <td>{activity.targetModel}</td>
+                <td>{activity.action}</td>
+                <td>{formatDate(activity.createdAt)}</td>
+              </tr>
+            );
+          })}
+        </tbody>
+      </table>
+    </div>
+  );
+};

+ 21 - 0
packages/app/src/components/Admin/AuditLogManagement.tsx

@@ -0,0 +1,21 @@
+import React, { FC } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { useSWRxActivityList } from '~/stores/activity';
+
+import { ActivityTable } from './AuditLog/ActivityTable';
+
+export const AuditLogManagement: FC = () => {
+  const { t } = useTranslation();
+
+  const { data: activityListData } = useSWRxActivityList(10, 0);
+  const activityList = activityListData?.docs != null ? activityListData.docs : [];
+
+  return (
+    <div data-testid="admin-auditlog">
+      <h2>{t('AuditLog')}</h2>
+      <ActivityTable activityList={activityList} />
+    </div>
+  );
+};

+ 16 - 3
packages/app/src/interfaces/activity.ts

@@ -1,3 +1,6 @@
+import { HasObjectId } from './has-object-id';
+import { IUser } from './user';
+
 // Model
 const MODEL_PAGE = 'Page';
 const MODEL_COMMENT = 'Comment';
@@ -43,6 +46,16 @@ export const AllSupportedTargetModelType = Object.values(SUPPORTED_TARGET_MODEL_
 export const AllSupportedEventModelType = Object.values(SUPPORTED_EVENT_MODEL_TYPE);
 export const AllSupportedActionType = Object.values(SUPPORTED_ACTION_TYPE);
 
-// type supportedTargetModelType = typeof SUPPORTED_TARGET_MODEL_NAMES[keyof typeof SUPPORTED_TARGET_MODEL_NAMES];
-// type supportedEventModelType = typeof SUPPORTED_EVENT_MODEL_NAMES[keyof typeof SUPPORTED_EVENT_MODEL_NAMES];
-// type supportedActionType = typeof SUPPORTED_ACTION_NAMES[keyof typeof SUPPORTED_ACTION_NAMES];
+type supportedTargetModelType = typeof SUPPORTED_TARGET_MODEL_TYPE[keyof typeof SUPPORTED_TARGET_MODEL_TYPE];
+// type supportedEventModelType = typeof SUPPORTED_EVENT_MODEL_TYPE[keyof typeof SUPPORTED_EVENT_MODEL_TYPE];
+type supportedActionType = typeof SUPPORTED_ACTION_TYPE[keyof typeof SUPPORTED_ACTION_TYPE];
+
+export type IActivity = {
+  user?: IUser
+  targetModel: supportedTargetModelType
+  targe: string
+  action: supportedActionType
+  createdAt: Date
+}
+
+export type IActivityHasId = IActivity & HasObjectId;

+ 13 - 0
packages/app/src/interfaces/mongoose-utils.ts

@@ -0,0 +1,13 @@
+export interface PaginateResult<T> {
+  docs: T[];
+  hasNextPage: boolean;
+  hasPrevPage: boolean;
+  limit: number;
+  nextPage: number | null;
+  offset: number;
+  page: number;
+  pagingCounter: number;
+  prevPage: number | null;
+  totalDocs: number;
+  totalPages: number;
+}

+ 1 - 0
packages/app/src/server/models/activity.ts

@@ -111,6 +111,7 @@ activitySchema.statics.getPaginatedActivity = async function(limit: number, offs
       limit,
       offset,
       sort: { createdAt: -1 },
+      populate: 'user',
     },
   );
   return paginateResult;

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

@@ -2,14 +2,17 @@ import express, { Request, Router } from 'express';
 import rateLimit from 'express-rate-limit';
 import { query } from 'express-validator';
 
+import { IActivity } from '~/interfaces/activity';
 import Activity from '~/server/models/activity';
 import loggerFactory from '~/utils/logger';
 
 import Crowi from '../../crowi';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
+import { serializeUserSecurely } from '../../models/serializers/user-serializer';
 
 import { ApiV3Response } from './interfaces/apiv3-response';
 
+
 const logger = loggerFactory('growi:routes:apiv3:activity');
 
 
@@ -24,7 +27,7 @@ const apiLimiter = rateLimit({
   windowMs: 15 * 60 * 1000, // 15 minutes
   max: 10, // limit each IP to 10 requests per windowMs
   message:
-    'Too many requests were sent from this IP. Please try a password reset request again on the password reset request form',
+    'Too many requests sent from this IP, please try again after 15 minutes.',
 });
 
 module.exports = (crowi: Crowi): Router => {
@@ -40,8 +43,22 @@ module.exports = (crowi: Crowi): Router => {
     const offset = req.query.offset || 1;
 
     try {
-      const paginatedActivity = await Activity.getPaginatedActivity(limit, offset);
-      return res.apiv3({ paginatedActivity });
+      const paginationResult = await Activity.getPaginatedActivity(limit, offset);
+
+      const User = crowi.model('User');
+      const serializedDocs = paginationResult.docs.map((doc: IActivity) => {
+        if (doc.user != null && doc.user instanceof User) {
+          doc.user = serializeUserSecurely(doc.user);
+        }
+        return doc;
+      });
+
+      const serializedPaginationResult = {
+        ...paginationResult,
+        docs: serializedDocs,
+      };
+
+      return res.apiv3({ serializedPaginationResult });
     }
     catch (err) {
       logger.error('Failed to get paginated activity', err);

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

@@ -0,0 +1,13 @@
+import { SWRResponse } from 'swr';
+import useSWRImmutable from 'swr/immutable';
+
+import { apiv3Get } from '../client/util/apiv3-client';
+import { IActivityHasId } from '../interfaces/activity';
+import { PaginateResult } from '../interfaces/mongoose-utils';
+
+export const useSWRxActivityList = (limit?: number, offset?: number): SWRResponse<PaginateResult<IActivityHasId>, Error> => {
+  return useSWRImmutable(
+    ['/activity', limit, offset],
+    (endpoint, limit, offset) => apiv3Get(endpoint, { limit, offset }).then(result => result.data.serializedPaginationResult),
+  );
+};