Skip to content

Commit 2e26f75

Browse files
Add audit logging for file fields and renaming of files (#1836)
1 parent 8da1070 commit 2e26f75

11 files changed

Lines changed: 122 additions & 26 deletions

File tree

packages/components/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@labkey/components",
3-
"version": "6.56.3",
3+
"version": "6.57.0",
44
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
55
"sideEffects": false,
66
"files": [

packages/components/releaseNotes/components.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4+
### version 6.57.0
5+
*Released*: 30 July 2025
6+
- Add file system audit events for apps
7+
- Add `TransactionAuditIdRenderer` for displaying link to page of audit records associated with a transaction id
8+
49
### version 6.56.3
510
*Released*: 24 July 2025
611
- Address Issue 53366 by not putting `undefined` into `List<ValueDescriptor>`

packages/components/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ import { ANCESTOR_LOOKUP_CONCEPT_URI, AncestorRenderer } from './internal/render
286286
import { StorageStatusRenderer } from './internal/renderers/StorageStatusRenderer';
287287
import { StoredAmountRenderer } from './internal/renderers/StoredAmountRenderer';
288288
import { SampleStatusRenderer } from './internal/renderers/SampleStatusRenderer';
289+
import { TransactionAuditIdRenderer } from './internal/renderers/TransactionAuditIdRenderer';
289290
import { ExpirationDateColumnRenderer } from './internal/renderers/ExpirationDateColumnRenderer';
290291
import { FolderColumnRenderer } from './internal/renderers/FolderColumnRenderer';
291292
import { AppendUnits } from './internal/renderers/AppendUnits';
@@ -1745,6 +1746,7 @@ export {
17451746
StorageAmountInput,
17461747
StorageStatusRenderer,
17471748
StoredAmountRenderer,
1749+
TransactionAuditIdRenderer,
17481750
SVGIcon,
17491751
Tab,
17501752
TabbedGridPanel,

packages/components/src/internal/components/auditlog/AuditDetails.test.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,37 @@ describe('AuditDetails', () => {
8989
test('changeDetails', () => {
9090
renderWithAppContext(
9191
<AuditDetails
92-
rowId={1}
93-
user={TEST_USER_APP_ADMIN}
9492
changeDetails={AuditDetailsModel.create({
9593
oldData: { a: 1 },
9694
newData: { a: 2 },
9795
})}
96+
rowId={1}
97+
user={TEST_USER_APP_ADMIN}
9898
/>,
9999
{ serverContext: { user: TEST_USER_APP_ADMIN } }
100100
);
101101
expect(document.querySelectorAll('.table-responsive')).toHaveLength(0);
102102
expect(document.querySelectorAll('.user-link')).toHaveLength(0);
103103
expect(document.querySelector('.panel-body').textContent).toBe('a12');
104104
});
105+
test('original data', () => {
106+
renderWithAppContext(
107+
<AuditDetails
108+
changeDetails={AuditDetailsModel.create({
109+
oldData: { a: 'file.txt' },
110+
newData: { a: 'new-1.txt' },
111+
originalValues: {
112+
a: 'new.txt',
113+
},
114+
})}
115+
rowId={1}
116+
user={TEST_USER_APP_ADMIN}
117+
/>,
118+
{ serverContext: { user: TEST_USER_APP_ADMIN } }
119+
);
120+
expect(document.querySelectorAll('.table-responsive')).toHaveLength(0);
121+
expect(document.querySelectorAll('.user-link')).toHaveLength(0);
122+
expect(document.querySelector('.panel-body').textContent).toBe('afile.txtnew-1.txt');
123+
expect(document.querySelector('.original-value-icon')).toBeInTheDocument();
124+
});
105125
});

packages/components/src/internal/components/auditlog/AuditDetails.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import React, { Component, PropsWithChildren, ReactNode } from 'react';
66
import { List, Map } from 'immutable';
77

88
import { User } from '../base/models/User';
9-
import { capitalizeFirstChar } from '../../util/utils';
9+
import { capitalizeFirstChar, caseInsensitive } from '../../util/utils';
1010
import { GridColumn } from '../base/models/GridColumn';
1111
import { Grid } from '../base/Grid';
1212

1313
import { UserLink } from '../user/UserLink';
1414

1515
import { getEventDataValueDisplay } from './utils';
1616
import { AuditDetailsModel } from './models';
17+
import { LabelHelpTip } from '../base/LabelHelpTip';
1718

1819
interface Props extends PropsWithChildren {
1920
changeDetails?: AuditDetailsModel;
@@ -52,7 +53,7 @@ export class AuditDetails extends Component<Props> {
5253
return displayVal;
5354
};
5455

55-
renderRow(field: string, oldVal: string, newVal: string, isUpdate: boolean, isInsert: boolean): ReactNode {
56+
renderRow(field: string, oldVal: string, newVal: string, originalVal: string, isUpdate: boolean, isInsert: boolean): ReactNode {
5657
const { user } = this.props;
5758

5859
if (!user.isSignedIn && AuditDetails.isUserFieldLabel(field)) return null;
@@ -64,7 +65,17 @@ export class AuditDetails extends Component<Props> {
6465
return (
6566
<div className="row margin-bottom" key={field}>
6667
<div className="left-padding right-padding">
67-
<span className="audit-detail-row-label right-padding">{capitalizeFirstChar(field)}</span>
68+
<span className="audit-detail-row-label right-padding">
69+
{capitalizeFirstChar(field)}
70+
{originalVal != null && (
71+
<LabelHelpTip
72+
iconComponent={<i className="original-value-icon fa fa-info-circle left-padding" />}
73+
placement="right"
74+
>
75+
<div className="ws-pre-wrap">Original value: {originalVal}</div>
76+
</LabelHelpTip>
77+
)}
78+
</span>
6879
</div>
6980
<div className="left-padding right-padding">
7081
{isInsert && <span className="new-audit-value">{newValue}</span>}
@@ -91,24 +102,24 @@ export class AuditDetails extends Component<Props> {
91102
const isInsert = changeDetails.isInsert();
92103
const usedFields = [];
93104

94-
let oldFields, newFields;
105+
let newFields, oldFields;
95106
if (changeDetails.oldData) {
96107
oldFields = changeDetails.oldData.entrySeq().map(([field, value]) => {
97108
let newValue;
98109
if (changeDetails.newData) {
99110
newValue = changeDetails.newData.get(field);
100111
usedFields.push(field);
101112
}
102-
103-
return this.renderRow(field, value, newValue, isUpdate, isInsert);
113+
const originalValue = caseInsensitive(changeDetails.originalValues, field);
114+
return this.renderRow(field, value, newValue, originalValue, isUpdate, isInsert);
104115
});
105116
}
106117

107118
if (changeDetails.newData) {
108119
newFields = changeDetails.newData.entrySeq().map(([field, value]) => {
109120
if (usedFields.indexOf(field) >= 0) return null;
110-
111-
return this.renderRow(field, undefined, value, isUpdate, isInsert);
121+
const originalValue = caseInsensitive(changeDetails.originalValues, field);
122+
return this.renderRow(field, undefined, value, originalValue, isUpdate, isInsert);
112123
});
113124
}
114125

packages/components/src/internal/components/auditlog/constants.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Query } from '@labkey/api';
33
export type AuditQuery = {
44
containerFilter?: Query.ContainerFilter;
55
hasDetail?: boolean;
6+
hasTransactionId?: boolean;
67
label: string;
78
value: string;
89
};
@@ -21,16 +22,18 @@ export const QUERY_UPDATE_AUDIT_QUERY: AuditQuery = {
2122

2223
export const DATACLASS_DATA_UPDATE_AUDIT_QUERY: AuditQuery = {
2324
hasDetail: true,
25+
hasTransactionId: true,
2426
label: 'Data Update Events',
2527
value: 'dataclassdataauditevent',
2628
};
2729

2830
export const INVENTORY_AUDIT_QUERY: AuditQuery = {
2931
hasDetail: true,
32+
hasTransactionId: true,
3033
label: 'Storage Management Events',
3134
value: 'inventoryauditevent',
3235
};
33-
export const LIST_AUDIT_QUERY: AuditQuery = { label: 'List Events', value: 'listauditevent' };
36+
export const LIST_AUDIT_QUERY: AuditQuery = { hasTransactionId: true, label: 'List Events', value: 'listauditevent' };
3437
export const GROUP_AUDIT_QUERY: AuditQuery = {
3538
containerFilter: Query.ContainerFilter.allFolders,
3639
label: 'Roles and Assignment Events',
@@ -41,9 +44,14 @@ export const CONTAINER_AUDIT_QUERY: AuditQuery = {
4144
label: 'Folder Events',
4245
value: 'containerauditevent',
4346
};
44-
export const SAMPLE_TYPE_AUDIT_QUERY: AuditQuery = { label: 'Sample Type Events', value: 'samplesetauditevent' };
47+
export const SAMPLE_TYPE_AUDIT_QUERY: AuditQuery = {
48+
hasTransactionId: true,
49+
label: 'Sample Type Events',
50+
value: 'samplesetauditevent',
51+
};
4552
export const SAMPLE_TIMELINE_AUDIT_QUERY: AuditQuery = {
4653
hasDetail: true,
54+
hasTransactionId: true,
4755
label: 'Sample Timeline Events',
4856
value: 'sampletimelineevent',
4957
};
@@ -52,13 +60,22 @@ export const USER_AUDIT_QUERY: AuditQuery = {
5260
label: 'User Events',
5361
value: 'userauditevent',
5462
};
55-
export const ASSAY_AUDIT_QUERY: AuditQuery = { value: 'assayauditevent', label: 'Assay Events' };
63+
export const ASSAY_AUDIT_QUERY: AuditQuery = {
64+
hasTransactionId: true,
65+
value: 'assayauditevent',
66+
label: 'Assay Events',
67+
};
5668
export const WORKFLOW_AUDIT_QUERY: AuditQuery = {
5769
hasDetail: true,
5870
label: 'Sample Workflow Events',
5971
value: 'samplesworkflowauditevent',
6072
};
61-
export const SOURCE_AUDIT_QUERY: AuditQuery = { hasDetail: true, label: 'Sources Events', value: 'sourcesauditevent' };
73+
export const SOURCE_AUDIT_QUERY: AuditQuery = {
74+
hasDetail: true,
75+
hasTransactionId: true,
76+
label: 'Sources Events',
77+
value: 'sourcesauditevent',
78+
};
6279

6380
export const NOTEBOOK_AUDIT_QUERY: AuditQuery = {
6481
label: 'Notebook Events',
@@ -74,12 +91,19 @@ export const REGISTRY_AUDIT_QUERY: AuditQuery = { label: 'Registry Events', valu
7491

7592
export const REPORT_AUDIT_QUERY: AuditQuery = { label: 'Report Events', value: 'ReportEvent' };
7693

94+
export const FILE_SYSTEM_AUDIT_QUERY: AuditQuery = {
95+
hasTransactionId: true,
96+
label: 'File Events',
97+
value: 'filesystem',
98+
};
99+
77100
export const AUDIT_EVENT_TYPE_PARAM = 'eventType';
78101

79102
export const COMMON_AUDIT_QUERIES: AuditQuery[] = [
80103
ATTACHMENT_AUDIT_QUERY,
81104
DOMAIN_AUDIT_QUERY,
82105
DOMAIN_PROPERTY_AUDIT_QUERY,
106+
FILE_SYSTEM_AUDIT_QUERY,
83107
QUERY_UPDATE_AUDIT_QUERY,
84108
INVENTORY_AUDIT_QUERY,
85109
LIST_AUDIT_QUERY,

packages/components/src/internal/components/auditlog/models.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Copyright (c) 2016-2018 LabKey Corporation. All rights reserved. No portion of this work may be reproduced in
33
* any form or by any electronic or mechanical means without written permission from LabKey Corporation.
44
*/
5-
import { fromJS, Map, Record, List } from 'immutable';
5+
import { fromJS, List, Map, Record } from 'immutable';
66

77
import { ASSAYS_KEY, SAMPLES_KEY } from '../../app/constants';
88

@@ -13,6 +13,7 @@ export class AuditDetailsModel extends Record({
1313
eventDateFormatted: undefined,
1414
oldData: undefined,
1515
newData: undefined,
16+
originalValues: undefined,
1617
userComment: undefined,
1718
}) {
1819
declare rowId?: number;
@@ -21,13 +22,15 @@ export class AuditDetailsModel extends Record({
2122
declare eventDateFormatted?: string;
2223
declare oldData?: Map<string, string>;
2324
declare newData?: Map<string, string>;
25+
declare originalValues?: Record<string, string>;
2426
declare userComment?: string;
2527

2628
static create(raw: any): AuditDetailsModel {
2729
return new AuditDetailsModel({
2830
...raw,
2931
oldData: raw.oldData ? fromJS(raw.oldData) : undefined,
3032
newData: raw.newData ? fromJS(raw.newData) : undefined,
33+
originalValues: raw.originalValues,
3134
});
3235
}
3336

@@ -65,6 +68,7 @@ export class TimelineEventModel extends Record({
6568
metadata: undefined,
6669
oldData: undefined,
6770
newData: undefined,
71+
originalValues: undefined, // map from field name to user-provided values that were converted
6872
userComment: undefined,
6973
}) {
7074
declare rowId?: number;
@@ -80,6 +84,7 @@ export class TimelineEventModel extends Record({
8084
declare metadata?: List<Map<string, any>>;
8185
declare oldData?: Map<string, string>;
8286
declare newData?: Map<string, string>;
87+
declare originalValues?: Map<string, string>;
8388
declare userComment?: string;
8489

8590
constructor(values?: { [key: string]: any }) {
@@ -118,6 +123,7 @@ export class TimelineEventModel extends Record({
118123

119124
if (raw.oldData) fields.oldData = fromJS(raw.oldData);
120125
if (raw.newData) fields.newData = fromJS(raw.newData);
126+
if (raw.originalValues) fields.originalValues = raw.originalValues;
121127

122128
return new TimelineEventModel(fields);
123129
}
@@ -129,6 +135,7 @@ export class TimelineEventModel extends Record({
129135
rowId: this.rowId,
130136
oldData: this.oldData,
131137
newData: this.newData,
138+
originalValues: this.originalValues,
132139
userComment: this.userComment,
133140
});
134141
}

packages/components/src/internal/components/auditlog/utils.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,26 @@ import {
2828
describe('getAuditQueries', () => {
2929
test('LKS starter', () => {
3030
const auditQueries = getAuditQueries(TEST_LKS_STARTER_MODULE_CONTEXT);
31-
expect(auditQueries.length).toBe(12);
31+
expect(auditQueries.length).toBe(13);
3232
expect(auditQueries.findIndex(entry => entry === ASSAY_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
33-
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(10);
33+
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(11);
3434
expect(auditQueries.findIndex(entry => entry === WORKFLOW_AUDIT_QUERY)).toBe(-1);
3535
expect(auditQueries.findIndex(entry => entry === SOURCE_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
3636
});
3737

3838
test('LKSM starter', () => {
3939
const auditQueries = getAuditQueries(TEST_LKSM_STARTER_MODULE_CONTEXT);
40-
expect(auditQueries.length).toBe(11);
41-
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(9);
40+
expect(auditQueries.length).toBe(12);
41+
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(10);
4242
expect(auditQueries.findIndex(entry => entry === ASSAY_AUDIT_QUERY)).toBe(-1);
4343
expect(auditQueries.findIndex(entry => entry === WORKFLOW_AUDIT_QUERY)).toBe(-1);
4444
expect(auditQueries.findIndex(entry => entry === SOURCE_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
4545
});
4646

4747
test('LKSM professional', () => {
4848
const auditQueries = getAuditQueries(TEST_LKSM_PROFESSIONAL_MODULE_CONTEXT);
49-
expect(auditQueries.length).toBe(15);
50-
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(13);
49+
expect(auditQueries.length).toBe(16);
50+
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(14);
5151
expect(auditQueries.findIndex(entry => entry === ASSAY_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
5252
expect(auditQueries.findIndex(entry => entry === WORKFLOW_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
5353
expect(auditQueries.findIndex(entry => entry === SOURCE_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
@@ -69,8 +69,8 @@ describe('getAuditQueries', () => {
6969
},
7070
};
7171
const auditQueries = getAuditQueries(moduleContext);
72-
expect(auditQueries.length).toBe(16);
73-
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(14);
72+
expect(auditQueries.length).toBe(17);
73+
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(15);
7474
expect(auditQueries.findIndex(entry => entry === ASSAY_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
7575
expect(auditQueries.findIndex(entry => entry === WORKFLOW_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
7676
expect(auditQueries.findIndex(entry => entry === NOTEBOOK_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Map } from 'immutable';
2+
import React, { PureComponent, ReactNode } from 'react';
3+
import { AppLink } from '../url/AppLink';
4+
import { AppURL } from '../url/AppURL';
5+
import { caseInsensitive } from '../util/utils';
6+
7+
interface Props {
8+
row: Map<any, any>;
9+
}
10+
11+
export class TransactionAuditIdRenderer extends PureComponent<Props> {
12+
render(): ReactNode {
13+
const { row } = this.props;
14+
const _row = row.toJS();
15+
const id = caseInsensitive(_row, 'transactionId')?.value;
16+
if (!id) {
17+
return null;
18+
}
19+
let url = AppURL.create('audit', id);
20+
const activeTab = caseInsensitive(_row, 'EventType')?.value;
21+
if (activeTab) {
22+
url = url.addParam('tab', activeTab);
23+
}
24+
return <AppLink to={url}>{id}</AppLink>;
25+
}
26+
}

0 commit comments

Comments
 (0)