Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/components/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@labkey/components",
"version": "6.56.3",
"version": "6.57.0",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down
5 changes: 5 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# @labkey/components
Components, models, actions, and utility functions for LabKey applications and pages

### version 6.57.0
*Released*: 30 July 2025
- Add file system audit events for apps
- Add `TransactionAuditIdRenderer` for displaying link to page of audit records associated with a transaction id

### version 6.56.3
*Released*: 24 July 2025
- Address Issue 53366 by not putting `undefined` into `List<ValueDescriptor>`
Expand Down
2 changes: 2 additions & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ import { ANCESTOR_LOOKUP_CONCEPT_URI, AncestorRenderer } from './internal/render
import { StorageStatusRenderer } from './internal/renderers/StorageStatusRenderer';
import { StoredAmountRenderer } from './internal/renderers/StoredAmountRenderer';
import { SampleStatusRenderer } from './internal/renderers/SampleStatusRenderer';
import { TransactionAuditIdRenderer } from './internal/renderers/TransactionAuditIdRenderer';
import { ExpirationDateColumnRenderer } from './internal/renderers/ExpirationDateColumnRenderer';
import { FolderColumnRenderer } from './internal/renderers/FolderColumnRenderer';
import { AppendUnits } from './internal/renderers/AppendUnits';
Expand Down Expand Up @@ -1745,6 +1746,7 @@ export {
StorageAmountInput,
StorageStatusRenderer,
StoredAmountRenderer,
TransactionAuditIdRenderer,
SVGIcon,
Tab,
TabbedGridPanel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,37 @@ describe('AuditDetails', () => {
test('changeDetails', () => {
renderWithAppContext(
<AuditDetails
rowId={1}
user={TEST_USER_APP_ADMIN}
changeDetails={AuditDetailsModel.create({
oldData: { a: 1 },
newData: { a: 2 },
})}
rowId={1}
user={TEST_USER_APP_ADMIN}
/>,
{ serverContext: { user: TEST_USER_APP_ADMIN } }
);
expect(document.querySelectorAll('.table-responsive')).toHaveLength(0);
expect(document.querySelectorAll('.user-link')).toHaveLength(0);
expect(document.querySelector('.panel-body').textContent).toBe('a12');
});
test('original data', () => {
renderWithAppContext(
<AuditDetails
changeDetails={AuditDetailsModel.create({
oldData: { a: 'file.txt' },
newData: { a: 'new-1.txt' },
originalValues: {
a: 'new.txt',
},
})}
rowId={1}
user={TEST_USER_APP_ADMIN}
/>,
{ serverContext: { user: TEST_USER_APP_ADMIN } }
);
expect(document.querySelectorAll('.table-responsive')).toHaveLength(0);
expect(document.querySelectorAll('.user-link')).toHaveLength(0);
expect(document.querySelector('.panel-body').textContent).toBe('afile.txtnew-1.txt');
expect(document.querySelector('.original-value-icon')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import React, { Component, PropsWithChildren, ReactNode } from 'react';
import { List, Map } from 'immutable';

import { User } from '../base/models/User';
import { capitalizeFirstChar } from '../../util/utils';
import { capitalizeFirstChar, caseInsensitive } from '../../util/utils';
import { GridColumn } from '../base/models/GridColumn';
import { Grid } from '../base/Grid';

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

import { getEventDataValueDisplay } from './utils';
import { AuditDetailsModel } from './models';
import { LabelHelpTip } from '../base/LabelHelpTip';

interface Props extends PropsWithChildren {
changeDetails?: AuditDetailsModel;
Expand Down Expand Up @@ -52,7 +53,7 @@ export class AuditDetails extends Component<Props> {
return displayVal;
};

renderRow(field: string, oldVal: string, newVal: string, isUpdate: boolean, isInsert: boolean): ReactNode {
renderRow(field: string, oldVal: string, newVal: string, originalVal: string, isUpdate: boolean, isInsert: boolean): ReactNode {
const { user } = this.props;

if (!user.isSignedIn && AuditDetails.isUserFieldLabel(field)) return null;
Expand All @@ -64,7 +65,17 @@ export class AuditDetails extends Component<Props> {
return (
<div className="row margin-bottom" key={field}>
<div className="left-padding right-padding">
<span className="audit-detail-row-label right-padding">{capitalizeFirstChar(field)}</span>
<span className="audit-detail-row-label right-padding">
{capitalizeFirstChar(field)}
{originalVal != null && (
<LabelHelpTip
iconComponent={<i className="original-value-icon fa fa-info-circle left-padding" />}
placement="right"
>
<div className="ws-pre-wrap">Original value: {originalVal}</div>
</LabelHelpTip>
)}
</span>
</div>
<div className="left-padding right-padding">
{isInsert && <span className="new-audit-value">{newValue}</span>}
Expand All @@ -91,24 +102,24 @@ export class AuditDetails extends Component<Props> {
const isInsert = changeDetails.isInsert();
const usedFields = [];

let oldFields, newFields;
let newFields, oldFields;
if (changeDetails.oldData) {
oldFields = changeDetails.oldData.entrySeq().map(([field, value]) => {
let newValue;
if (changeDetails.newData) {
newValue = changeDetails.newData.get(field);
usedFields.push(field);
}

return this.renderRow(field, value, newValue, isUpdate, isInsert);
const originalValue = caseInsensitive(changeDetails.originalValues, field);
return this.renderRow(field, value, newValue, originalValue, isUpdate, isInsert);
});
}

if (changeDetails.newData) {
newFields = changeDetails.newData.entrySeq().map(([field, value]) => {
if (usedFields.indexOf(field) >= 0) return null;

return this.renderRow(field, undefined, value, isUpdate, isInsert);
const originalValue = caseInsensitive(changeDetails.originalValues, field);
return this.renderRow(field, undefined, value, originalValue, isUpdate, isInsert);
});
}

Expand Down
32 changes: 28 additions & 4 deletions packages/components/src/internal/components/auditlog/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Query } from '@labkey/api';
export type AuditQuery = {
containerFilter?: Query.ContainerFilter;
hasDetail?: boolean;
hasTransactionId?: boolean;
label: string;
value: string;
};
Expand All @@ -21,16 +22,18 @@ export const QUERY_UPDATE_AUDIT_QUERY: AuditQuery = {

export const DATACLASS_DATA_UPDATE_AUDIT_QUERY: AuditQuery = {
hasDetail: true,
hasTransactionId: true,
label: 'Data Update Events',
value: 'dataclassdataauditevent',
};

export const INVENTORY_AUDIT_QUERY: AuditQuery = {
hasDetail: true,
hasTransactionId: true,
label: 'Storage Management Events',
value: 'inventoryauditevent',
};
export const LIST_AUDIT_QUERY: AuditQuery = { label: 'List Events', value: 'listauditevent' };
export const LIST_AUDIT_QUERY: AuditQuery = { hasTransactionId: true, label: 'List Events', value: 'listauditevent' };
export const GROUP_AUDIT_QUERY: AuditQuery = {
containerFilter: Query.ContainerFilter.allFolders,
label: 'Roles and Assignment Events',
Expand All @@ -41,9 +44,14 @@ export const CONTAINER_AUDIT_QUERY: AuditQuery = {
label: 'Folder Events',
value: 'containerauditevent',
};
export const SAMPLE_TYPE_AUDIT_QUERY: AuditQuery = { label: 'Sample Type Events', value: 'samplesetauditevent' };
export const SAMPLE_TYPE_AUDIT_QUERY: AuditQuery = {
hasTransactionId: true,
label: 'Sample Type Events',
value: 'samplesetauditevent',
};
export const SAMPLE_TIMELINE_AUDIT_QUERY: AuditQuery = {
hasDetail: true,
hasTransactionId: true,
label: 'Sample Timeline Events',
value: 'sampletimelineevent',
};
Expand All @@ -52,13 +60,22 @@ export const USER_AUDIT_QUERY: AuditQuery = {
label: 'User Events',
value: 'userauditevent',
};
export const ASSAY_AUDIT_QUERY: AuditQuery = { value: 'assayauditevent', label: 'Assay Events' };
export const ASSAY_AUDIT_QUERY: AuditQuery = {
hasTransactionId: true,
value: 'assayauditevent',
label: 'Assay Events',
};
export const WORKFLOW_AUDIT_QUERY: AuditQuery = {
hasDetail: true,
label: 'Sample Workflow Events',
value: 'samplesworkflowauditevent',
};
export const SOURCE_AUDIT_QUERY: AuditQuery = { hasDetail: true, label: 'Sources Events', value: 'sourcesauditevent' };
export const SOURCE_AUDIT_QUERY: AuditQuery = {
hasDetail: true,
hasTransactionId: true,
label: 'Sources Events',
value: 'sourcesauditevent',
};

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

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

export const FILE_SYSTEM_AUDIT_QUERY: AuditQuery = {
hasTransactionId: true,
label: 'File Events',
value: 'filesystem',
};

export const AUDIT_EVENT_TYPE_PARAM = 'eventType';

export const COMMON_AUDIT_QUERIES: AuditQuery[] = [
ATTACHMENT_AUDIT_QUERY,
DOMAIN_AUDIT_QUERY,
DOMAIN_PROPERTY_AUDIT_QUERY,
FILE_SYSTEM_AUDIT_QUERY,
QUERY_UPDATE_AUDIT_QUERY,
INVENTORY_AUDIT_QUERY,
LIST_AUDIT_QUERY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) 2016-2018 LabKey Corporation. All rights reserved. No portion of this work may be reproduced in
* any form or by any electronic or mechanical means without written permission from LabKey Corporation.
*/
import { fromJS, Map, Record, List } from 'immutable';
import { fromJS, List, Map, Record } from 'immutable';

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

Expand All @@ -13,6 +13,7 @@ export class AuditDetailsModel extends Record({
eventDateFormatted: undefined,
oldData: undefined,
newData: undefined,
originalValues: undefined,
userComment: undefined,
}) {
declare rowId?: number;
Expand All @@ -21,13 +22,15 @@ export class AuditDetailsModel extends Record({
declare eventDateFormatted?: string;
declare oldData?: Map<string, string>;
declare newData?: Map<string, string>;
declare originalValues?: Record<string, string>;
declare userComment?: string;

static create(raw: any): AuditDetailsModel {
return new AuditDetailsModel({
...raw,
oldData: raw.oldData ? fromJS(raw.oldData) : undefined,
newData: raw.newData ? fromJS(raw.newData) : undefined,
originalValues: raw.originalValues,
});
}

Expand Down Expand Up @@ -65,6 +68,7 @@ export class TimelineEventModel extends Record({
metadata: undefined,
oldData: undefined,
newData: undefined,
originalValues: undefined, // map from field name to user-provided values that were converted
Comment thread
cnathe marked this conversation as resolved.
userComment: undefined,
}) {
declare rowId?: number;
Expand All @@ -80,6 +84,7 @@ export class TimelineEventModel extends Record({
declare metadata?: List<Map<string, any>>;
declare oldData?: Map<string, string>;
declare newData?: Map<string, string>;
declare originalValues?: Map<string, string>;
declare userComment?: string;

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

if (raw.oldData) fields.oldData = fromJS(raw.oldData);
if (raw.newData) fields.newData = fromJS(raw.newData);
if (raw.originalValues) fields.originalValues = raw.originalValues;

return new TimelineEventModel(fields);
}
Expand All @@ -129,6 +135,7 @@ export class TimelineEventModel extends Record({
rowId: this.rowId,
oldData: this.oldData,
newData: this.newData,
originalValues: this.originalValues,
userComment: this.userComment,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,26 @@ import {
describe('getAuditQueries', () => {
test('LKS starter', () => {
const auditQueries = getAuditQueries(TEST_LKS_STARTER_MODULE_CONTEXT);
expect(auditQueries.length).toBe(12);
expect(auditQueries.length).toBe(13);
expect(auditQueries.findIndex(entry => entry === ASSAY_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(10);
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(11);
expect(auditQueries.findIndex(entry => entry === WORKFLOW_AUDIT_QUERY)).toBe(-1);
expect(auditQueries.findIndex(entry => entry === SOURCE_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
});

test('LKSM starter', () => {
const auditQueries = getAuditQueries(TEST_LKSM_STARTER_MODULE_CONTEXT);
expect(auditQueries.length).toBe(11);
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(9);
expect(auditQueries.length).toBe(12);
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(10);
expect(auditQueries.findIndex(entry => entry === ASSAY_AUDIT_QUERY)).toBe(-1);
expect(auditQueries.findIndex(entry => entry === WORKFLOW_AUDIT_QUERY)).toBe(-1);
expect(auditQueries.findIndex(entry => entry === SOURCE_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
});

test('LKSM professional', () => {
const auditQueries = getAuditQueries(TEST_LKSM_PROFESSIONAL_MODULE_CONTEXT);
expect(auditQueries.length).toBe(15);
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(13);
expect(auditQueries.length).toBe(16);
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(14);
expect(auditQueries.findIndex(entry => entry === ASSAY_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
expect(auditQueries.findIndex(entry => entry === WORKFLOW_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
expect(auditQueries.findIndex(entry => entry === SOURCE_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
Expand All @@ -69,8 +69,8 @@ describe('getAuditQueries', () => {
},
};
const auditQueries = getAuditQueries(moduleContext);
expect(auditQueries.length).toBe(16);
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(14);
expect(auditQueries.length).toBe(17);
expect(auditQueries.findIndex(entry => entry === INVENTORY_AUDIT_QUERY)).toBe(15);
expect(auditQueries.findIndex(entry => entry === ASSAY_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
expect(auditQueries.findIndex(entry => entry === WORKFLOW_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
expect(auditQueries.findIndex(entry => entry === NOTEBOOK_AUDIT_QUERY)).toBeGreaterThanOrEqual(0);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Map } from 'immutable';
import React, { PureComponent, ReactNode } from 'react';
import { AppLink } from '../url/AppLink';
import { AppURL } from '../url/AppURL';
import { caseInsensitive } from '../util/utils';

interface Props {
row: Map<any, any>;
}

export class TransactionAuditIdRenderer extends PureComponent<Props> {
render(): ReactNode {
const { row } = this.props;
const _row = row.toJS();
const id = caseInsensitive(_row, 'transactionId')?.value;
if (!id) {
return null;
}
let url = AppURL.create('audit', id);
const activeTab = caseInsensitive(_row, 'EventType')?.value;
if (activeTab) {
url = url.addParam('tab', activeTab);
}
return <AppLink to={url}>{id}</AppLink>;
}
}
1 change: 1 addition & 0 deletions packages/components/src/theme/timeline.scss
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@

}

.original-value-icon,
.timeline-comments-icon {
color: $brand-primary;
}
Expand Down