diff --git a/packages/plugins/@hera/plugin-audit-logs/.npmignore b/packages/plugins/@hera/plugin-audit-logs/.npmignore new file mode 100644 index 000000000..65f5e8779 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/.npmignore @@ -0,0 +1,2 @@ +/node_modules +/src diff --git a/packages/plugins/@hera/plugin-audit-logs/README.md b/packages/plugins/@hera/plugin-audit-logs/README.md new file mode 100644 index 000000000..6f6185630 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/README.md @@ -0,0 +1 @@ +# @hera/plugin-audit-logs diff --git a/packages/plugins/@hera/plugin-audit-logs/client.d.ts b/packages/plugins/@hera/plugin-audit-logs/client.d.ts new file mode 100644 index 000000000..6c459cbac --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/client.d.ts @@ -0,0 +1,2 @@ +export * from './dist/client'; +export { default } from './dist/client'; diff --git a/packages/plugins/@hera/plugin-audit-logs/client.js b/packages/plugins/@hera/plugin-audit-logs/client.js new file mode 100644 index 000000000..b6e3be70e --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/client.js @@ -0,0 +1 @@ +module.exports = require('./dist/client/index.js'); diff --git a/packages/plugins/@hera/plugin-audit-logs/package.json b/packages/plugins/@hera/plugin-audit-logs/package.json new file mode 100644 index 000000000..529e2768f --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/package.json @@ -0,0 +1,24 @@ +{ + "name": "@hera/plugin-audit-logs", + "displayName": "Audit logs", + "displayName.zh-CN": "审计日志", + "description": "Audit logs.", + "description.zh-CN": "审计日志。", + "version": "1.0.0", + "main": "dist/server/index.js", + "devDependencies": { + "@ant-design/icons": "5.x", + "@formily/antd-v5": "1.x", + "@formily/react": "2.x", + "@formily/shared": "2.x", + "react": "^18.2.0", + "react-i18next": "^11.15.1" + }, + "peerDependencies": { + "@nocobase/client": "0.x", + "@nocobase/database": "0.x", + "@nocobase/server": "0.x", + "@nocobase/test": "0.x" + }, + "dependencies": {} +} diff --git a/packages/plugins/@hera/plugin-audit-logs/server.d.ts b/packages/plugins/@hera/plugin-audit-logs/server.d.ts new file mode 100644 index 000000000..c41081ddc --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/server.d.ts @@ -0,0 +1,2 @@ +export * from './dist/server'; +export { default } from './dist/server'; diff --git a/packages/plugins/@hera/plugin-audit-logs/server.js b/packages/plugins/@hera/plugin-audit-logs/server.js new file mode 100644 index 000000000..972842039 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/server.js @@ -0,0 +1 @@ +module.exports = require('./dist/server/index.js'); diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/AuditLogsBlockInitializer.tsx b/packages/plugins/@hera/plugin-audit-logs/src/client/AuditLogsBlockInitializer.tsx new file mode 100644 index 000000000..6752dcf10 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/AuditLogsBlockInitializer.tsx @@ -0,0 +1,37 @@ +import { TableOutlined } from '@ant-design/icons'; +import { ISchema } from '@formily/react'; +import { + createTableBlockSchema, + SchemaInitializerItem, + useSchemaInitializer, + useSchemaInitializerItem, +} from '@nocobase/client'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export const AuditLogsBlockInitializer = () => { + const { insert } = useSchemaInitializer(); + const { t } = useTranslation(); + const itemConfig = useSchemaInitializerItem(); + + const schema = createTableBlockSchema({ + collection: 'auditLogs', + rowKey: 'id', + tableActionInitializers: 'auditLogsTable:configureActions', + tableColumnInitializers: 'auditLogsTable:configureColumns', + tableActionColumnInitializers: 'auditLogsTable:configureItemActions', + tableBlockProvider: 'AuditLogsBlockProvider', + disableTemplate: true, + }); + + return ( + } + onClick={() => { + insert(schema as ISchema); + }} + title={t('Audit Logs')} + {...itemConfig} + /> + ); +}; diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/AuditLogsBlockProvider.tsx b/packages/plugins/@hera/plugin-audit-logs/src/client/AuditLogsBlockProvider.tsx new file mode 100644 index 000000000..9569b2c10 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/AuditLogsBlockProvider.tsx @@ -0,0 +1,17 @@ +import { ExtendCollectionsProvider, TableBlockProvider } from '@nocobase/client'; +import React from 'react'; +import { useAuditChangesCollection, useAuditLogsCollection, useCollectionsCollection } from './collections'; + +export const AuditLogsBlockProvider: React.FC = ({ children, ...restProps }) => { + const auditChangesCollection = useAuditChangesCollection(); + const auditLogsCollection = useAuditLogsCollection(); + const collectionsCollection = useCollectionsCollection(); + + return ( + + + {children} + + + ); +}; diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/AuditLogsProvider.tsx b/packages/plugins/@hera/plugin-audit-logs/src/client/AuditLogsProvider.tsx new file mode 100644 index 000000000..df1959f4a --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/AuditLogsProvider.tsx @@ -0,0 +1,27 @@ +import { SchemaComponentOptions } from '@nocobase/client'; +import React from 'react'; +import { AuditLogsBlockInitializer } from './AuditLogsBlockInitializer'; +import { AuditLogsBlockProvider } from './AuditLogsBlockProvider'; +import { AuditLogsField } from './components/AuditLogsField'; +import { AuditLogsValue } from './components/AuditLogsValue'; +import { AuditLogsViewActionInitializer } from './components/AuditLogsViewActionInitializer'; +import { AuditLogs } from './deplicated/AuditLogs'; +import { AuditLogsTableActionColumnInitializer } from './initializers/AuditLogsTableActionColumnInitializer'; + +export const AuditLogsProvider = (props: any) => { + return ( + + {props.children} + + ); +}; diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/collections.tsx b/packages/plugins/@hera/plugin-audit-logs/src/client/collections.tsx new file mode 100644 index 000000000..c1d4757ef --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/collections.tsx @@ -0,0 +1,166 @@ +import { useAuditLogsTranslation } from './locale'; + +export const useAuditLogsCollection = () => { + return { + name: 'auditLogs', + title: '{{t("Audit logs")}}', + fields: [ + { + name: 'createdAt', + type: 'date', + interface: 'createdAt', + uiSchema: { + type: 'datetime', + title: '{{t("Created at")}}', + 'x-component': 'DatePicker', + 'x-component-props': { + showTime: true, + ellipsis: true, + }, + 'x-read-pretty': true, + }, + }, + { + name: 'type', + type: 'string', + interface: 'select', + uiSchema: { + type: 'string', + title: '{{t("Action type")}}', + 'x-component': 'Select', + 'x-component-props': { ellipsis: true }, + 'x-read-pretty': true, + enum: [ + { label: '{{t("Create record")}}', value: 'create', color: 'lime' }, + { label: '{{t("Update record")}}', value: 'update', color: 'gold' }, + { label: '{{t("Delete record")}}', value: 'destroy', color: 'magenta' }, + ], + }, + }, + { + name: 'recordId', + type: 'string', + interface: 'input', + uiSchema: { + title: '{{t("Record ID")}}', + type: 'string', + 'x-component': 'Input', + 'x-component-props': { ellipsis: true }, + }, + }, + { + collectionName: 'auditLogs', + name: 'collection', + type: 'belongsTo', + interface: 'm2o', + target: 'collections', + targetKey: 'name', + sourceKey: 'id', + foreignKey: 'collectionName', + uiSchema: { + type: 'object', + title: '{{t("Collection")}}', + 'x-component': 'AssociationField', + 'x-component-props': { fieldNames: { value: 'name', label: 'title' }, ellipsis: true }, + 'x-read-pretty': true, + }, + }, + { + name: 'user', + collectionName: 'auditLogs', + type: 'belongsTo', + interface: 'createdBy', + targetKey: 'id', + foreignKey: 'createdById', + target: 'users', + uiSchema: { + type: 'object', + title: '{{t("User")}}', + 'x-component': 'AssociationField', + 'x-component-props': { fieldNames: { value: 'id', label: 'nickname' }, ellipsis: true }, + 'x-read-pretty': true, + }, + }, + { + name: 'changes', + collectionName: 'auditLogs', + type: 'hasMany', + interface: 'subTable', + target: 'auditChanges', + foreignKey: 'auditLogId', + targetKey: 'id', + uiSchema: { + type: 'object', + title: '{{t("Details of changes", { ns: "audit-logs" })}}', + }, + }, + ], + }; +}; + +export const useAuditChangesCollection = () => { + return { + name: 'auditChanges', + title: '{{t("Audit Changes")}}', + fields: [ + { + name: 'field', + type: 'json', + interface: 'input', + uiSchema: { + title: '{{t("Field")}}', + 'x-component': 'AuditLogsField', + }, + }, + { + type: 'json', + name: 'before', + interface: 'input', + uiSchema: { + title: '{{t("Before change")}}', + 'x-component': 'AuditLogsValue', + }, + }, + { + type: 'json', + name: 'after', + interface: 'input', + uiSchema: { + title: '{{t("After change")}}', + 'x-component': 'AuditLogsValue', + }, + }, + ], + }; +}; + +export const useCollectionsCollection = () => { + return { + name: 'collections', + title: '{{t("Collections")}}', + fields: [ + { + name: 'name', + type: 'string', + interface: 'input', + uiSchema: { + title: '{{t("Collection name")}}', + type: 'string', + 'x-component': 'Input', + 'x-component-props': { ellipsis: true }, + }, + }, + { + name: 'title', + type: 'string', + interface: 'input', + uiSchema: { + title: '{{t("Collection display name")}}', + type: 'string', + 'x-component': 'Input', + 'x-component-props': { ellipsis: true }, + }, + }, + ], + }; +}; diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/components/AuditLogsField.tsx b/packages/plugins/@hera/plugin-audit-logs/src/client/components/AuditLogsField.tsx new file mode 100644 index 000000000..314d1b048 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/components/AuditLogsField.tsx @@ -0,0 +1,19 @@ +import { EllipsisWithTooltip, useCompile } from '@nocobase/client'; +import React from 'react'; +import { observer, useField } from '@formily/react'; + +export const AuditLogsField = observer( + () => { + const field = useField(); + const compile = useCompile(); + if (!field.value) { + return null; + } + return ( + + {field.value?.uiSchema?.title ? compile(field.value?.uiSchema?.title) : field.value.name} + + ); + }, + { displayName: 'AuditLogsField' }, +); diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/components/AuditLogsValue.tsx b/packages/plugins/@hera/plugin-audit-logs/src/client/components/AuditLogsValue.tsx new file mode 100644 index 000000000..1e2c1ac08 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/components/AuditLogsValue.tsx @@ -0,0 +1,26 @@ +import { EllipsisWithTooltip, FormProvider, SchemaComponent, useRecord } from '@nocobase/client'; +import React from 'react'; +import { observer, useField } from '@formily/react'; + +export const AuditLogsValue = observer( + () => { + const field = useField(); + const record = useRecord(); + if (record.field?.uiSchema) { + return ( + + + + ); + } + return {field.value ? JSON.stringify(field.value) : null}; + }, + { displayName: 'AuditLogsValue' }, +); diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/components/AuditLogsViewActionInitializer.tsx b/packages/plugins/@hera/plugin-audit-logs/src/client/components/AuditLogsViewActionInitializer.tsx new file mode 100644 index 000000000..2ab850ef3 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/components/AuditLogsViewActionInitializer.tsx @@ -0,0 +1,343 @@ +import { ActionInitializer } from '@nocobase/client'; +import React from 'react'; + +export const AuditLogsViewActionInitializer = () => { + const schema = { + type: 'void', + title: '{{ t("View") }}', + 'x-action': 'view', + // 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:view', + 'x-component': 'Action', + 'x-component-props': { + openMode: 'drawer', + }, + properties: { + drawer: { + type: 'void', + title: '{{ t("View record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + properties: { + tab1: { + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + type: 'void', + 'x-component': 'Grid', + properties: { + '7ypqrxaqysp': { + type: 'void', + 'x-component': 'Grid.Row', + properties: { + '1fc2l8dwe7m': { + type: 'void', + 'x-component': 'Grid.Col', + properties: { + '5ley6xifrsb': { + type: 'void', + 'x-acl-action': 'auditLogs:get', + 'x-decorator': 'FormBlockProvider', + 'x-decorator-props': { + resource: 'auditLogs', + collection: 'auditLogs', + readPretty: true, + action: 'get', + params: { + appends: ['collection', 'user', 'changes'], + }, + useParams: '{{ useParamsFromRecord }}', + useSourceId: '{{ useSourceIdFromParentRecord }}', + }, + 'x-component': 'CardItem', + properties: { + bv710pbf9w6: { + type: 'void', + 'x-component': 'FormV2', + 'x-read-pretty': true, + 'x-component-props': { + useProps: '{{ useFormBlockProps }}', + }, + properties: { + grid: { + type: 'void', + 'x-component': 'Grid', + properties: { + g4c24abnbd9: { + type: 'void', + 'x-component': 'Grid.Row', + properties: { + fkt9dj5lu1k: { + type: 'void', + 'x-component': 'Grid.Col', + properties: { + createdAt: { + type: 'string', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'auditLogs.createdAt', + 'x-component-props': {}, + 'x-read-pretty': true, + }, + }, + }, + }, + }, + l267bkv423v: { + type: 'void', + 'x-component': 'Grid.Row', + properties: { + tehaepm5xqy: { + type: 'void', + 'x-component': 'Grid.Col', + properties: { + type: { + type: 'string', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'auditLogs.type', + 'x-component-props': {}, + 'x-read-pretty': true, + }, + }, + }, + }, + }, + zhmn2tkzdh2: { + type: 'void', + 'x-component': 'Grid.Row', + properties: { + yuwyej17i64: { + type: 'void', + 'x-component': 'Grid.Col', + properties: { + recordId: { + type: 'string', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'auditLogs.recordId', + 'x-component-props': {}, + }, + }, + }, + }, + }, + '6q933ic06mj': { + type: 'void', + 'x-component': 'Grid.Row', + properties: { + nwtbkqd99zl2: { + type: 'void', + 'x-component': 'Grid.Col', + properties: { + 'collection.title': { + type: 'string', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'auditLogs.collection.title', + 'x-component-props': {}, + }, + }, + }, + nwtbkqd99zl1: { + type: 'void', + 'x-component': 'Grid.Col', + properties: { + 'collection.name': { + type: 'string', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'auditLogs.collection.name', + 'x-component-props': {}, + }, + }, + }, + }, + }, + xd4kzulndqk: { + type: 'void', + 'x-component': 'Grid.Row', + properties: { + k3sd72n83zd: { + type: 'void', + 'x-component': 'Grid.Col', + properties: { + user: { + type: 'string', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'auditLogs.user', + 'x-component-props': {}, + 'x-read-pretty': true, + properties: { + viewer: { + type: 'void', + title: '{{ t("View record") }}', + 'x-component': 'RecordPicker.Viewer', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'TabPaneInitializers', + properties: { + tab1: { + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + '3lghgclbtcg': { + type: 'void', + 'x-component': 'Grid.Row', + properties: { + '8l1v1auwlx0': { + type: 'void', + 'x-component': 'Grid.Col', + properties: { + changes: { + type: 'void', + 'x-component': 'TableField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'auditLogs.changes', + 'x-component-props': {}, + properties: { + block: { + type: 'void', + 'x-decorator': 'TableFieldProvider', + 'x-acl-action': 'auditChanges:list', + 'x-decorator-props': { + collection: 'auditChanges', + association: 'auditLogs.changes', + resource: 'auditLogs.changes', + action: 'list', + params: { + paginate: false, + }, + showIndex: true, + dragSort: false, + }, + properties: { + actions: { + type: 'void', + 'x-component': 'TableField.ActionBar', + 'x-component-props': {}, + }, + changes: { + type: 'array', + 'x-component': 'TableV2', + 'x-component-props': { + rowSelection: false, + useProps: '{{ useTableFieldProps }}', + }, + properties: { + '5uvv96u9ict': { + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-component': 'TableV2.Column', + properties: { + field: { + 'x-collection-field': 'auditChanges.field', + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-component-props': { + ellipsis: true, + }, + }, + }, + }, + h7a4tgt11gd: { + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-component': 'TableV2.Column', + properties: { + before: { + 'x-collection-field': 'auditChanges.before', + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-component-props': { + ellipsis: true, + }, + }, + }, + }, + m275z8rglzx: { + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-component': 'TableV2.Column', + properties: { + after: { + 'x-collection-field': 'auditChanges.after', + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-component-props': { + ellipsis: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + return ; +}; diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/deplicated/AuditLogs.tsx b/packages/plugins/@hera/plugin-audit-logs/src/client/deplicated/AuditLogs.tsx new file mode 100644 index 000000000..54e62873d --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/deplicated/AuditLogs.tsx @@ -0,0 +1,513 @@ +import { ArrayTable } from '@formily/antd-v5'; +import { observer, useField } from '@formily/react'; +import { + ExtendCollectionsProvider, + FormProvider, + SchemaComponent, + TableBlockProvider, + useCollection_deprecated, + useCompile, + useRecord, +} from '@nocobase/client'; +import React, { createContext, useContext } from 'react'; +import { AuditLogsDesigner } from './AuditLogsDesigner'; + +const collection = { + name: 'auditLogs', + title: '{{t("Audit logs")}}', + fields: [ + { + name: 'createdAt', + type: 'date', + interface: 'createdAt', + uiSchema: { + type: 'datetime', + title: '{{t("Created at")}}', + 'x-component': 'DatePicker', + 'x-component-props': { + showTime: true, + }, + 'x-read-pretty': true, + }, + }, + { + name: 'type', + type: 'string', + interface: 'select', + uiSchema: { + type: 'string', + title: '{{t("Action type")}}', + 'x-component': 'Select', + 'x-read-pretty': true, + enum: [ + { label: '{{t("Create record")}}', value: 'create', color: 'lime' }, + { label: '{{t("Update record")}}', value: 'update', color: 'gold' }, + { label: '{{t("Delete record")}}', value: 'destroy', color: 'magenta' }, + ], + }, + }, + ], +}; + +const Username = observer( + () => { + const field = useField(); + return
{field?.value?.nickname || field.value?.id}
; + }, + { displayName: 'Username' }, +); + +const Collection = observer( + () => { + const field = useField(); + const { title, name } = field.value || {}; + const compile = useCompile(); + return
{title ? compile(title) : name}
; + }, + { displayName: 'Collection' }, +); + +const Field = observer( + () => { + const field = useField(); + const compile = useCompile(); + if (!field.value) { + return null; + } + return
{field.value?.uiSchema?.title ? compile(field.value?.uiSchema?.title) : field.value.name}
; + }, + { displayName: 'Field' }, +); + +const Value = observer( + () => { + const field = useField(); + const record = ArrayTable.useRecord(); + if (record.field?.uiSchema) { + return ( + + + + ); + } + return
{field.value ? JSON.stringify(field.value) : null}
; + }, + { displayName: 'Value' }, +); + +const IsAssociationBlock = createContext(null); +IsAssociationBlock.displayName = 'IsAssociationBlock'; + +export const AuditLogs: any = () => { + const isAssoc = useContext(IsAssociationBlock); + const ext = {}; + if (!isAssoc) { + ext['column31'] = { + type: 'void', + 'x-component': 'TableV2.Column', + title: '{{t("Record ID")}}', + properties: { + recordId: { + 'x-component': 'Input', + 'x-read-pretty': true, + }, + }, + }; + } + return ( + + ); +}; + +AuditLogs.Decorator = observer( + (props: any) => { + const parent = useCollection_deprecated(); + const record = useRecord(); + let filter = props?.params?.filter; + if (parent.name) { + const filterByTk = record?.[parent.filterTargetKey || 'id']; + if (filter) { + filter = { + $and: [ + filter, + { + collectionName: parent.name, + recordId: `${filterByTk}`, + }, + ], + }; + } else { + filter = { + collectionName: parent.name, + recordId: `${filterByTk}`, + }; + } + } + const defaults = { + collection: 'auditLogs', + resource: 'auditLogs', + action: 'list', + params: { + pageSize: 20, + appends: ['collection', 'user'], + ...props.params, + filter, + sort: '-createdAt', + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }; + return ( + + + {props.children} + + + ); + }, + { displayName: 'AuditLogs.Decorator' }, +); + +AuditLogs.Designer = AuditLogsDesigner; diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/deplicated/AuditLogsDesigner.tsx b/packages/plugins/@hera/plugin-audit-logs/src/client/deplicated/AuditLogsDesigner.tsx new file mode 100644 index 000000000..03f57cd8a --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/deplicated/AuditLogsDesigner.tsx @@ -0,0 +1,77 @@ +import { useField, useFieldSchema } from '@formily/react'; +import { + GeneralSchemaDesigner, + SchemaSettingsDataScope, + SchemaSettingsDivider, + SchemaSettingsRemove, + SchemaSettingsSelectItem, + useCollection_deprecated, + useDesignable, + useFormBlockContext, + useTableBlockContext, +} from '@nocobase/client'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export const AuditLogsDesigner = () => { + const { name, title } = useCollection_deprecated(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { form } = useFormBlockContext(); + const { service } = useTableBlockContext(); + const { t } = useTranslation(); + const { dn } = useDesignable(); + return ( + + { + const params = field.decoratorProps.params || {}; + params.filter = filter; + field.decoratorProps.params = params; + fieldSchema['x-decorator-props']['params'] = params; + service.run({ ...service.params?.[0], filter, page: 1 }); + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }} + /> + { + const params = field.decoratorProps.params || {}; + params.pageSize = pageSize; + field.decoratorProps.params = params; + fieldSchema['x-decorator-props']['params'] = params; + service.run({ ...service.params?.[0], pageSize, page: 1 }); + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }} + /> + + + + ); +}; diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/index.ts b/packages/plugins/@hera/plugin-audit-logs/src/client/index.ts new file mode 100644 index 000000000..3cb402f30 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/index.ts @@ -0,0 +1,39 @@ +import { Plugin, SchemaInitializerItemType } from '@nocobase/client'; +import { AuditLogsProvider } from './AuditLogsProvider'; +import { + auditLogsTableActionColumnInitializers, + auditLogsTableActionColumnInitializers_deprecated, +} from './initializers/AuditLogsTableActionColumnInitializers'; +import { + auditLogsTableActionInitializers, + auditLogsTableActionInitializers_deprecated, +} from './initializers/AuditLogsTableActionInitializers'; +import { + auditLogsTableColumnInitializers, + auditLogsTableColumnInitializers_deprecated, +} from './initializers/AuditLogsTableColumnInitializers'; +export * from './AuditLogsBlockInitializer'; +export * from './AuditLogsProvider'; + +export class AuditLogsPlugin extends Plugin { + async load() { + this.app.use(AuditLogsProvider); + this.app.schemaInitializerManager.add(auditLogsTableActionInitializers_deprecated); + this.app.schemaInitializerManager.add(auditLogsTableActionInitializers); + this.app.schemaInitializerManager.add(auditLogsTableActionColumnInitializers_deprecated); + this.app.schemaInitializerManager.add(auditLogsTableActionColumnInitializers); + this.app.schemaInitializerManager.add(auditLogsTableColumnInitializers_deprecated); + this.app.schemaInitializerManager.add(auditLogsTableColumnInitializers); + + const blockInitializers = this.app.schemaInitializerManager.get('page:addBlock'); + const recordBlockInitializers = this.app.schemaInitializerManager.get('popup:common:addBlock'); + const auditLogs: Omit = { + title: '{{t("Audit logs")}}', + Component: 'AuditLogsBlockInitializer', + }; + blockInitializers.add('otherBlocks.auditLogs', auditLogs); + recordBlockInitializers.add('otherBlocks.auditLogs', auditLogs); + } +} + +export default AuditLogsPlugin; diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/initializers/AuditLogsTableActionColumnInitializer.tsx b/packages/plugins/@hera/plugin-audit-logs/src/client/initializers/AuditLogsTableActionColumnInitializer.tsx new file mode 100644 index 000000000..a045445a7 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/initializers/AuditLogsTableActionColumnInitializer.tsx @@ -0,0 +1,27 @@ +import { InitializerWithSwitch, useSchemaInitializerItem } from '@nocobase/client'; +import React from 'react'; + +export const AuditLogsTableActionColumnInitializer = () => { + const schema = { + type: 'void', + title: '{{ t("Actions") }}', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-designer': 'TableV2.ActionColumnDesigner', + 'x-initializer': 'auditLogsTable:configureItemActions', + 'x-action-column': 'actions', + properties: { + actions: { + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + properties: {}, + }, + }, + }; + const itemConfig = useSchemaInitializerItem(); + return ; +}; diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/initializers/AuditLogsTableActionColumnInitializers.tsx b/packages/plugins/@hera/plugin-audit-logs/src/client/initializers/AuditLogsTableActionColumnInitializers.tsx new file mode 100644 index 000000000..437f47899 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/initializers/AuditLogsTableActionColumnInitializers.tsx @@ -0,0 +1,141 @@ +import { MenuOutlined } from '@ant-design/icons'; +import { useFieldSchema } from '@formily/react'; +import { + CompatibleSchemaInitializer, + createDesignable, + Resizable, + useAPIClient, + useDesignable, +} from '@nocobase/client'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +/** + * @deprecated + */ +export const auditLogsTableActionColumnInitializers_deprecated = new CompatibleSchemaInitializer({ + name: 'AuditLogsTableActionColumnInitializers', + insertPosition: 'beforeEnd', + Component: (props: any) => , + useInsert() { + const fieldSchema = useFieldSchema(); + const api = useAPIClient(); + const { refresh } = useDesignable(); + const { t } = useTranslation(); + + return (schema) => { + const spaceSchema = fieldSchema.reduceProperties((buf, schema) => { + if (schema['x-component'] === 'Space') { + return schema; + } + return buf; + }, null); + if (!spaceSchema) { + return; + } + const dn = createDesignable({ + t, + api, + refresh, + current: spaceSchema, + }); + dn.loadAPIClientEvents(); + dn.insertBeforeEnd(schema); + }; + }, + items: [ + { + name: 'enableActions', + type: 'itemGroup', + title: '{{t("Enable actions")}}', + children: [ + { + name: 'view', + type: 'item', + title: '{{t("View")}}', + Component: 'AuditLogsViewActionInitializer', + schema: { + 'x-component': 'Action.Link', + 'x-action': 'view', + 'x-decorator': 'ACLActionProvider', + }, + }, + ], + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'columnWidth', + type: 'item', + title: '{{t("Column width")}}', + Component: Resizable, + }, + ], +}); + +export const auditLogsTableActionColumnInitializers = new CompatibleSchemaInitializer( + { + name: 'auditLogsTable:configureItemActions', + insertPosition: 'beforeEnd', + Component: (props: any) => , + useInsert() { + const fieldSchema = useFieldSchema(); + const api = useAPIClient(); + const { refresh } = useDesignable(); + const { t } = useTranslation(); + + return (schema) => { + const spaceSchema = fieldSchema.reduceProperties((buf, schema) => { + if (schema['x-component'] === 'Space') { + return schema; + } + return buf; + }, null); + if (!spaceSchema) { + return; + } + const dn = createDesignable({ + t, + api, + refresh, + current: spaceSchema, + }); + dn.loadAPIClientEvents(); + dn.insertBeforeEnd(schema); + }; + }, + items: [ + { + name: 'enableActions', + type: 'itemGroup', + title: '{{t("Enable actions")}}', + children: [ + { + name: 'view', + type: 'item', + title: '{{t("View")}}', + Component: 'AuditLogsViewActionInitializer', + schema: { + 'x-component': 'Action.Link', + 'x-action': 'view', + 'x-decorator': 'ACLActionProvider', + }, + }, + ], + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'columnWidth', + type: 'item', + title: '{{t("Column width")}}', + Component: Resizable, + }, + ], + }, + auditLogsTableActionColumnInitializers_deprecated, +); diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/initializers/AuditLogsTableActionInitializers.tsx b/packages/plugins/@hera/plugin-audit-logs/src/client/initializers/AuditLogsTableActionInitializers.tsx new file mode 100644 index 000000000..41b74bec4 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/initializers/AuditLogsTableActionInitializers.tsx @@ -0,0 +1,76 @@ +import { CompatibleSchemaInitializer } from '@nocobase/client'; + +/** + * @deprecated + * 操作记录表格操作配置 + */ +export const auditLogsTableActionInitializers_deprecated = new CompatibleSchemaInitializer({ + name: 'AuditLogsTableActionInitializers', + title: "{{t('Configure actions')}}", + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'itemGroup', + title: "{{t('Enable actions')}}", + name: 'enableActions', + children: [ + { + name: 'filter', + title: "{{t('Filter')}}", + Component: 'FilterActionInitializer', + schema: { + 'x-align': 'left', + }, + }, + { + name: 'refresh', + title: "{{t('Refresh')}}", + Component: 'RefreshActionInitializer', + schema: { + 'x-align': 'right', + }, + }, + ], + }, + ], +}); + +export const auditLogsTableActionInitializers = new CompatibleSchemaInitializer( + { + name: 'auditLogsTable:configureActions', + title: "{{t('Configure actions')}}", + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'itemGroup', + title: "{{t('Enable actions')}}", + name: 'enableActions', + children: [ + { + name: 'filter', + title: "{{t('Filter')}}", + Component: 'FilterActionInitializer', + schema: { + 'x-align': 'left', + }, + }, + { + name: 'refresh', + title: "{{t('Refresh')}}", + Component: 'RefreshActionInitializer', + schema: { + 'x-align': 'right', + }, + }, + ], + }, + ], + }, + auditLogsTableActionInitializers_deprecated, +); diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/initializers/AuditLogsTableColumnInitializers.tsx b/packages/plugins/@hera/plugin-audit-logs/src/client/initializers/AuditLogsTableColumnInitializers.tsx new file mode 100644 index 000000000..81ae12a81 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/initializers/AuditLogsTableColumnInitializers.tsx @@ -0,0 +1,139 @@ +import { + CompatibleSchemaInitializer, + SchemaInitializerChildren, + useAssociatedTableColumnInitializerFields, + useCompile, + useInheritsTableColumnInitializerFields, + useTableColumnInitializerFields, +} from '@nocobase/client'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +// 表格列配置 +const ParentCollectionFields = () => { + const inheritFields = useInheritsTableColumnInitializerFields(); + const { t } = useTranslation(); + const compile = useCompile(); + if (!inheritFields?.length) return null; + const res = []; + inheritFields.forEach((inherit) => { + Object.values(inherit)[0].length && + res.push({ + type: 'itemGroup', + divider: true, + title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', + children: Object.values(inherit)[0].filter((v: any) => !v?.field?.isForeignKey), + }); + }); + return {res}; +}; + +const AssociatedFields = () => { + const associatedFields = useAssociatedTableColumnInitializerFields(); + const { t } = useTranslation(); + + if (!associatedFields?.length) return null; + const schema: any = [ + { + type: 'itemGroup', + divider: true, + title: t('Display association fields'), + children: associatedFields, + }, + ]; + return {schema}; +}; + +/** + * @deprecated + */ +export const auditLogsTableColumnInitializers_deprecated = new CompatibleSchemaInitializer({ + name: 'AuditLogsTableColumnInitializers', + insertPosition: 'beforeEnd', + icon: 'SettingOutlined', + title: '{{t("Configure columns")}}', + wrap(s) { + if (s['x-action-column']) { + return s; + } + return { + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-designer': 'TableV2.Column.Designer', + 'x-component': 'TableV2.Column', + properties: { + [s.name]: { + ...s, + }, + }, + }; + }, + items: [ + { + name: 'displayFields', + type: 'itemGroup', + title: '{{t("Display fields")}}', + useChildren: useTableColumnInitializerFields, + }, + { + name: 'parentCollectionFields', + Component: ParentCollectionFields, + }, + { + name: 'associationFields', + Component: AssociatedFields, + }, + { + name: 'actionColumn', + title: '{{t("Action column")}}', + Component: 'AuditLogsTableActionColumnInitializer', + }, + ], +}); + +export const auditLogsTableColumnInitializers = new CompatibleSchemaInitializer( + { + name: 'auditLogsTable:configureColumns', + insertPosition: 'beforeEnd', + icon: 'SettingOutlined', + title: '{{t("Configure columns")}}', + wrap(s) { + if (s['x-action-column']) { + return s; + } + return { + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-designer': 'TableV2.Column.Designer', + 'x-component': 'TableV2.Column', + properties: { + [s.name]: { + ...s, + }, + }, + }; + }, + items: [ + { + name: 'displayFields', + type: 'itemGroup', + title: '{{t("Display fields")}}', + useChildren: useTableColumnInitializerFields, + }, + { + name: 'parentCollectionFields', + Component: ParentCollectionFields, + }, + { + name: 'associationFields', + Component: AssociatedFields, + }, + { + name: 'actionColumn', + title: '{{t("Action column")}}', + Component: 'AuditLogsTableActionColumnInitializer', + }, + ], + }, + auditLogsTableColumnInitializers_deprecated, +); diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/locale/index.ts b/packages/plugins/@hera/plugin-audit-logs/src/client/locale/index.ts new file mode 100644 index 000000000..799a97370 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/locale/index.ts @@ -0,0 +1,14 @@ +import { i18n, tval as nTval } from '@nocobase/client'; +import { useTranslation } from 'react-i18next'; + +export const NAMESPACE = 'audit-logs'; + +export function lang(key: string) { + return i18n.t(key, { ns: NAMESPACE }); +} + +export function useAuditLogsTranslation() { + return useTranslation(NAMESPACE); +} + +export const tval = (key: string) => nTval(key, { ns: NAMESPACE }); diff --git a/packages/plugins/@hera/plugin-audit-logs/src/client/utils.ts b/packages/plugins/@hera/plugin-audit-logs/src/client/utils.ts new file mode 100644 index 000000000..1ecc63443 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/client/utils.ts @@ -0,0 +1,303 @@ +import { uid } from '@formily/shared'; + +export const createSchema = () => { + const filterSchema = { + 'x-component': 'Action', + 'x-component-props': { + popover: true, + }, + type: 'void', + title: '{{t("Filter")}}', + properties: { + popover: { + type: 'void', + 'x-decorator': 'Form', + 'x-decorator-props': {}, + 'x-component': 'Action.Popover', + 'x-component-props': { + trigger: 'click', + placement: 'bottomLeft', + }, + properties: { + filter: { + type: 'object', + default: { + $and: [{}], + }, + 'x-component': 'Filter', + 'x-component-props': { + useDataSource: '{{cm.useFilterDataSource}}', + }, + }, + footer: { + type: 'void', + 'x-component': 'Action.Popover.Footer', + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + properties: { + saveDefault: { + type: 'void', + 'x-component': 'Filter.SaveDefaultValue', + 'x-component-props': {}, + }, + cancel: { + type: 'void', + title: '{{t("Cancel")}}', + 'x-component': 'Action', + 'x-component-props': { + useAction: '{{cm.useCancelFilterAction}}', + }, + }, + submit: { + type: 'void', + title: '{{t("Submit")}}', + 'x-component': 'Action', + 'x-component-props': { + type: 'primary', + useAction: '{{cm.useFilterAction}}', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + const schema = { + type: 'void', + name: 'actionLog', + 'x-decorator': 'ResourceActionProvider', + 'x-decorator-props': { + collection: 'action_logs', + dragSort: false, + request: { + resource: 'action_logs', + action: 'list', + params: { + pageSize: 20, + filter: {}, + appends: [], + }, + }, + }, + 'x-designer': 'ActionLog.Designer', + 'x-component': 'CardItem', + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + marginBottom: 16, + }, + }, + properties: { + [uid()]: { ...filterSchema }, + }, + }, + table: { + type: 'void', + 'x-component': 'Table.Void', + 'x-component-props': { + rowKey: 'id', + useDataSource: '{{cm.useDataSourceFromRAC}}', + }, + properties: { + column1: { + type: 'void', + title: "{{t('Created at')}}", + 'x-component': 'Table.Column', + properties: { + created_at: { + type: 'string', + 'x-component': 'DatePicker', + 'x-read-pretty': true, + 'x-component-props': { + format: 'YYYY-MM-DD HH:mm:ss', + }, + }, + }, + }, + column2: { + type: 'void', + title: "{{t('Created by')}}", + 'x-component': 'Table.Column', + properties: { + 'user.nickname': { + type: 'string', + 'x-component': 'Input', + 'x-read-pretty': true, + }, + }, + }, + column3: { + type: 'void', + title: "{{t('Collection display name')}}", + 'x-component': 'Table.Column', + properties: { + 'collection.title': { + type: 'string', + 'x-component': 'Input', + 'x-read-pretty': true, + }, + }, + }, + column4: { + type: 'void', + title: "{{t('Action type')}}", + 'x-component': 'Table.Column', + properties: { + type: { + type: 'string', + 'x-component': 'Select', + 'x-read-pretty': true, + enum: [ + { label: "{{ t('Add new') }}", value: 'create', color: 'green' }, + { label: "{{ t('Update') }}", value: 'update', color: 'blue' }, + { label: "{{ t('Delete') }}", value: 'destroy', color: 'red' }, + ], + }, + }, + }, + [uid()]: { + type: 'void', + title: "{{t('Actions')}}", + 'x-component': 'Table.Column', + 'x-component-props': { + width: 60, + align: 'center', + }, + properties: { + [uid()]: { + title: '{{ t("View") }}', + type: 'void', + 'x-action': 'view', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + }, + properties: { + drawer: { + type: 'void', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + title: '{{ t("View record") }}', + properties: { + created_at: { + type: 'string', + title: "{{t('Created at')}}", + 'x-decorator': 'FormItem', + 'x-component': 'DatePicker', + 'x-read-pretty': true, + 'x-component-props': { + format: 'YYYY-MM-DD HH:mm:ss', + }, + }, + 'user.nickname': { + type: 'string', + title: "{{t('Created by')}}", + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-read-pretty': true, + }, + 'collection.title': { + type: 'string', + title: "{{t('Collection display name')}}", + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-read-pretty': true, + }, + type: { + type: 'string', + title: "{{t('Action type')}}", + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-read-pretty': true, + enum: [ + { + label: "{{t('Insert')}}", + value: 'create', + color: 'green', + }, + { + label: "{{t('Update')}}", + value: 'update', + color: 'blue', + }, + { + label: "{{t('Delete')}}", + value: 'destroy', + color: 'red', + }, + ], + }, + changes: { + type: 'array', + title: "{{t('Data changes')}}", + 'x-decorator': 'FormItem', + 'x-component': 'Table.Array', + 'x-component-props': { + pagination: false, + showIndex: true, + }, + items: { + type: 'object', + properties: { + column1: { + type: 'void', + 'x-component': 'Table.Column', + 'x-component-props': { title: "{{t('Field display name')}}" }, + properties: { + field: { + type: 'string', + 'x-decorator': 'FormilyFormItem', + 'x-component': 'ActionLog.Field', + }, + }, + }, + column2: { + type: 'void', + 'x-component': 'Table.Column', + 'x-component-props': { title: "{{ t('Before change') }}" }, + properties: { + before: { + type: 'string', + 'x-decorator': 'FormilyFormItem', + 'x-component': 'ActionLog.FieldValue', + }, + }, + }, + column3: { + type: 'void', + 'x-component': 'Table.Column', + 'x-component-props': { title: "{{ t('After change') }}" }, + properties: { + after: { + type: 'string', + 'x-decorator': 'FormilyFormItem', + 'x-component': 'ActionLog.FieldValue', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + return schema; +}; diff --git a/packages/plugins/@hera/plugin-audit-logs/src/index.ts b/packages/plugins/@hera/plugin-audit-logs/src/index.ts new file mode 100644 index 000000000..7e74612df --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/index.ts @@ -0,0 +1,2 @@ +export * from './server'; +export { default } from './server'; diff --git a/packages/plugins/@hera/plugin-audit-logs/src/locale/en-US.json b/packages/plugins/@hera/plugin-audit-logs/src/locale/en-US.json new file mode 100644 index 000000000..3dc09f65a --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/locale/en-US.json @@ -0,0 +1,4 @@ +{ + "Details of changes": "Details of changes", + "Create record": "Create record" +} diff --git a/packages/plugins/@hera/plugin-audit-logs/src/locale/zh-CN.json b/packages/plugins/@hera/plugin-audit-logs/src/locale/zh-CN.json new file mode 100644 index 000000000..783f355b5 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/locale/zh-CN.json @@ -0,0 +1,4 @@ +{ + "Details of changes": "变更详情", + "Create record": "创建数据" +} diff --git a/packages/plugins/@hera/plugin-audit-logs/src/server/__tests__/hook.test.ts b/packages/plugins/@hera/plugin-audit-logs/src/server/__tests__/hook.test.ts new file mode 100644 index 000000000..4f8920a04 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/server/__tests__/hook.test.ts @@ -0,0 +1,117 @@ +import Database from '@nocobase/database'; +import { createMockServer, MockServer } from '@nocobase/test'; + +describe('hook', () => { + let api: MockServer; + let db: Database; + + beforeEach(async () => { + api = await createMockServer({ + plugins: ['audit-logs'], + }); + db = api.db; + db.collection({ + name: 'posts', + logging: true, + fields: [ + { + type: 'string', + name: 'title', + }, + { + type: 'string', + name: 'status', + defaultValue: 'draft', + }, + ], + }); + db.collection({ + name: 'users', + logging: false, + fields: [ + { type: 'string', name: 'nickname' }, + { type: 'string', name: 'token' }, + ], + }); + await db.sync(); + }); + + afterEach(async () => { + await api.destroy(); + }); + + it('model', async () => { + const Post = db.getCollection('posts').model; + const post = await Post.create({ title: 't1' }); + await post.update({ title: 't2' }); + await post.destroy(); + const auditLogs = await db.getCollection('auditLogs').repository.find({ + appends: ['changes'], + }); + expect(auditLogs.length).toBe(3); + + const titleChange = (changes) => { + return changes.find((item) => item.field.name === 'title'); + }; + + expect(titleChange(auditLogs[0].changes).before).toBeNull(); + expect(titleChange(auditLogs[0].changes).after).toBe('t1'); + + expect(titleChange(auditLogs[1].changes).before).toBe('t1'); + expect(titleChange(auditLogs[1].changes).after).toBe('t2'); + + expect(titleChange(auditLogs[2].changes).before).toBe('t2'); + }); + + it('repository', async () => { + const Post = db.getCollection('posts'); + const User = db.getCollection('users').model; + const user = await User.create({ nickname: 'a', token: 'token1' }); + const post = await Post.repository.create({ + values: { title: 't1' }, + context: { + state: { + currentUser: user, + }, + }, + }); + const AuditLog = db.getCollection('auditLogs'); + const log = await AuditLog.repository.findOne({ + appends: ['changes'], + }); + expect(log.toJSON()).toMatchObject({ + collectionName: 'posts', + type: 'create', + userId: 1, + recordId: `${post.get('id')}`, + changes: [ + { + field: { + name: 'title', + type: 'string', + }, + before: null, + after: 't1', + }, + ], + }); + }); + + // it.skip('resource', async () => { + // const agent = api.agent(); + // agent.set('Authorization', `Bearer token1`); + // const response = await agent.resource('posts').create({ + // values: { title: 't1' }, + // }); + // await agent.resource('posts').update({ + // resourceIndex: response.body.data.id, + // values: { title: 't2' }, + // }); + // await agent.resource('posts').destroy({ + // resourceIndex: response.body.data.id, + // }); + // const ActionLog = db.getCollection('action_logs').model; + // const count = await ActionLog.count(); + // expect(count).toBe(3); + // }); +}); diff --git a/packages/plugins/@hera/plugin-audit-logs/src/server/collections/auditChanges.ts b/packages/plugins/@hera/plugin-audit-logs/src/server/collections/auditChanges.ts new file mode 100644 index 000000000..c0713a451 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/server/collections/auditChanges.ts @@ -0,0 +1,34 @@ +import { defineCollection } from '@nocobase/database'; + +export default defineCollection({ + dumpRules: { + group: 'log', + }, + name: 'auditChanges', + title: '变动值', + createdBy: false, + updatedBy: false, + createdAt: false, + updatedAt: false, + shared: true, + fields: [ + { + type: 'json', + name: 'field', + }, + { + type: 'json', + name: 'before', + }, + { + type: 'json', + name: 'after', + }, + { + type: 'belongsTo', + name: 'log', + target: 'auditLogs', + foreignKey: 'auditLogId', + }, + ], +}); diff --git a/packages/plugins/@hera/plugin-audit-logs/src/server/collections/auditLogs.ts b/packages/plugins/@hera/plugin-audit-logs/src/server/collections/auditLogs.ts new file mode 100644 index 000000000..76609b423 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/server/collections/auditLogs.ts @@ -0,0 +1,51 @@ +import { defineCollection } from '@nocobase/database'; + +export default defineCollection({ + dumpRules: { + group: 'log', + }, + name: 'auditLogs', + createdBy: false, + updatedBy: false, + updatedAt: false, + shared: true, + fields: [ + { + type: 'date', + name: 'createdAt', + }, + { + type: 'string', + name: 'type', + }, + { + type: 'string', + name: 'recordId', + index: true, + }, + { + type: 'string', + name: 'collectionName', + }, + { + type: 'belongsTo', + name: 'collection', + target: 'collections', + targetKey: 'name', + sourceKey: 'id', + foreignKey: 'collectionName', + constraints: false, + }, + { + type: 'hasMany', + name: 'changes', + target: 'auditChanges', + foreignKey: 'auditLogId', + }, + { + type: 'belongsTo', + name: 'user', + target: 'users', + }, + ], +}); diff --git a/packages/plugins/@hera/plugin-audit-logs/src/server/constants.ts b/packages/plugins/@hera/plugin-audit-logs/src/server/constants.ts new file mode 100644 index 000000000..0cf75c724 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/server/constants.ts @@ -0,0 +1,3 @@ +export const LOG_TYPE_CREATE = 'create'; +export const LOG_TYPE_UPDATE = 'update'; +export const LOG_TYPE_DESTROY = 'destroy'; diff --git a/packages/plugins/@hera/plugin-audit-logs/src/server/hooks/after-create.ts b/packages/plugins/@hera/plugin-audit-logs/src/server/hooks/after-create.ts new file mode 100644 index 000000000..4f6f480c2 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/server/hooks/after-create.ts @@ -0,0 +1,45 @@ +import { LOG_TYPE_CREATE } from '../constants'; + +export async function afterCreate(model, options) { + if (options.logging === false) { + return; + } + const { collection } = model.constructor; + if (!collection || !collection.options.logging) { + return; + } + const transaction = options.transaction; + const AuditLog = model.constructor.database.getCollection('auditLogs'); + const currentUserId = options?.context?.state?.currentUser?.id; + try { + const changes = []; + const changed = model.changed(); + if (changed) { + changed.forEach((key: string) => { + const field = collection.findField((field) => { + return field.name === key || field.options.field === key; + }); + if (field && !field.options.hidden) { + changes.push({ + field: field.options, + after: model.get(key), + }); + } + }); + } + await AuditLog.repository.create({ + values: { + type: LOG_TYPE_CREATE, + collectionName: model.constructor.name, + recordId: model.get(model.constructor.primaryKeyAttribute), + createdAt: model.get('createdAt'), + userId: currentUserId, + changes, + }, + transaction, + hooks: false, + }); + } catch (error) { + // console.error(error); + } +} diff --git a/packages/plugins/@hera/plugin-audit-logs/src/server/hooks/after-destroy.ts b/packages/plugins/@hera/plugin-audit-logs/src/server/hooks/after-destroy.ts new file mode 100644 index 000000000..4a7b8fa59 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/server/hooks/after-destroy.ts @@ -0,0 +1,43 @@ +import { LOG_TYPE_DESTROY } from '../constants'; + +export async function afterDestroy(model, options) { + const { collection } = model.constructor; + if (!collection || !collection.options.logging) { + return; + } + const transaction = options.transaction; + const AuditLog = model.constructor.database.getCollection('auditLogs'); + const currentUserId = options?.context?.state?.currentUser?.id; + try { + const changes = []; + Object.keys(model.get()).forEach((key: string) => { + const field = collection.findField((field) => { + return field.name === key || field.options.field === key; + }); + if (field) { + changes.push({ + field: field.options, + before: model.get(key), + }); + } + }); + await AuditLog.repository.create({ + values: { + type: LOG_TYPE_DESTROY, + collectionName: model.constructor.name, + recordId: model.get(model.constructor.primaryKeyAttribute), + userId: currentUserId, + changes, + }, + transaction, + hooks: false, + }); + // if (!options.transaction) { + // await transaction.commit(); + // } + } catch (error) { + // if (!options.transaction) { + // await transaction.rollback(); + // } + } +} diff --git a/packages/plugins/@hera/plugin-audit-logs/src/server/hooks/after-update.ts b/packages/plugins/@hera/plugin-audit-logs/src/server/hooks/after-update.ts new file mode 100644 index 000000000..72407e63d --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/server/hooks/after-update.ts @@ -0,0 +1,52 @@ +import { LOG_TYPE_UPDATE } from '../constants'; + +export async function afterUpdate(model, options) { + const { collection } = model.constructor; + if (!collection || !collection.options.logging) { + return; + } + const changed = model.changed(); + if (!changed) { + return; + } + const transaction = options.transaction; + const AuditLog = model.constructor.database.getCollection('auditLogs'); + const currentUserId = options?.context?.state?.currentUser?.id; + const changes = []; + changed.forEach((key: string) => { + const field = collection.findField((field) => { + return field.name === key || field.options.field === key; + }); + if (field && !field.options.hidden) { + changes.push({ + field: field.options, + after: model.get(key), + before: model.previous(key), + }); + } + }); + if (!changes.length) { + return; + } + try { + await AuditLog.repository.create({ + values: { + type: LOG_TYPE_UPDATE, + collectionName: model.constructor.name, + recordId: model.get(model.constructor.primaryKeyAttribute), + createdAt: model.get('updatedAt'), + userId: currentUserId, + changes, + }, + transaction, + hooks: false, + }); + // if (!options.transaction) { + // await transaction.commit(); + // } + } catch (error) { + // if (!options.transaction) { + // await transaction.rollback(); + // } + } +} diff --git a/packages/plugins/@hera/plugin-audit-logs/src/server/hooks/index.ts b/packages/plugins/@hera/plugin-audit-logs/src/server/hooks/index.ts new file mode 100644 index 000000000..3c84cf55e --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/server/hooks/index.ts @@ -0,0 +1,3 @@ +export * from './after-create'; +export * from './after-update'; +export * from './after-destroy'; diff --git a/packages/plugins/@hera/plugin-audit-logs/src/server/index.ts b/packages/plugins/@hera/plugin-audit-logs/src/server/index.ts new file mode 100644 index 000000000..7093d6227 --- /dev/null +++ b/packages/plugins/@hera/plugin-audit-logs/src/server/index.ts @@ -0,0 +1,23 @@ +import { Plugin } from '@nocobase/server'; +import path from 'path'; +import { afterCreate, afterDestroy, afterUpdate } from './hooks'; + +export default class PluginActionLogs extends Plugin { + async beforeLoad() { + this.db.on('afterCreate', afterCreate); + this.db.on('afterUpdate', afterUpdate); + this.db.on('afterDestroy', afterDestroy); + } + + async load() { + await this.importCollections(path.resolve(__dirname, 'collections')); + + this.db.addMigrations({ + namespace: 'audit-logs', + directory: path.resolve(__dirname, './migrations'), + context: { + plugin: this, + }, + }); + } +} diff --git a/packages/plugins/@hera/plugin-audit-logs/src/server/migrations/.gitkeep b/packages/plugins/@hera/plugin-audit-logs/src/server/migrations/.gitkeep new file mode 100644 index 000000000..e69de29bb