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.62.6",
"version": "6.62.7",
"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.62.7
*Released*: 29 September 2025
- Enumerate plate, plate set auditing events
- Introduce `isQueryUpdateEvent` to flag which events are backed by query audit events

### version 6.62.6
*Released*: 29 September 2025
- Issue 53979: TextInput to handle non-finite numeric values
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 React, { Component, PropsWithChildren } from 'react';
import React, { Component, PropsWithChildren, ReactNode } from 'react';
import { List, Map } from 'immutable';

import { User } from '../base/models/User';
Expand All @@ -17,11 +17,17 @@ import { AuditDetailsModel } from './models';
import { LabelHelpTip } from '../base/LabelHelpTip';
import { AUDIT_DETAIL_FIELD_VALUE_INHERITED } from './constants';

const USER_FIELDS = ['created by', 'createdby', 'modified by', 'modifiedby'];

function isUserFieldLabel(field: string): boolean {
return USER_FIELDS.indexOf(field.toLowerCase()) > -1;
}

interface Props extends PropsWithChildren {
changeDetails?: AuditDetailsModel;
emptyMsg?: string;
fieldValueRenderer?: (label, value, displayValue, hasProvidedValue?: boolean) => any;
gridColumnRenderer?: (data: any, row: any, displayValue: any) => any;
fieldValueRenderer?: (label: string, value: any, displayValue: any, hasProvidedValue?: boolean) => ReactNode;
gridColumnRenderer?: (data: any, row: any, displayValue: any) => ReactNode;
gridData?: List<Map<string, any>>;
inheritedFieldMsg?: string;
rowId?: number;
Expand All @@ -36,10 +42,6 @@ export class AuditDetails extends Component<Props> {
emptyMsg: 'No audit event selected.',
};

static isUserFieldLabel(field: string): boolean {
return ['created by', 'createdby', 'modified by', 'modifiedby'].indexOf(field.toLowerCase()) > -1;
}

getValueDisplay = (field: string, value: string, hasProvidedValues: boolean, isInherited?: boolean): any => {
const { fieldValueRenderer } = this.props;

Expand All @@ -48,7 +50,7 @@ export class AuditDetails extends Component<Props> {
let displayVal: any = value;
if (value == null || value === '') displayVal = 'NA';

if (AuditDetails.isUserFieldLabel(field) && value !== undefined) {
if (isUserFieldLabel(field) && value !== undefined) {
displayVal = <UserLink userId={parseInt(value, 10)} />;
}

Expand All @@ -68,7 +70,7 @@ export class AuditDetails extends Component<Props> {
): React.ReactNode {
const { user, inheritedFieldMsg } = this.props;

if (!user.isSignedIn && AuditDetails.isUserFieldLabel(field)) return null;
if (!user.isSignedIn && isUserFieldLabel(field)) return null;

const oldValue = this.getValueDisplay(
field,
Expand All @@ -88,6 +90,7 @@ export class AuditDetails extends Component<Props> {
if (providedVal) {
providedVals.push('Provided value: ' + providedVal);
}

return (
<div className="row margin-bottom" key={field}>
<div className="left-padding right-padding">
Expand Down Expand Up @@ -131,10 +134,9 @@ export class AuditDetails extends Component<Props> {

renderChanges() {
const { changeDetails } = this.props;

const isUpdate = changeDetails.isUpdate();
const isInsert = changeDetails.isInsert();
const usedFields = [];
const usedFields: string[] = [];

let newFields, oldFields;
if (changeDetails.oldData) {
Expand Down
82 changes: 50 additions & 32 deletions packages/components/src/internal/components/auditlog/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,121 +4,139 @@ export type AuditQuery = {
containerFilter?: Query.ContainerFilter;
hasDetail?: boolean;
hasTransactionId?: boolean;
/** Indicates that the audit event is backed by a query update event. */
isQueryUpdateEvent?: boolean;
label: string;
value: string;
};

export const ATTACHMENT_AUDIT_QUERY: AuditQuery = { label: 'Attachment Events', value: 'attachmentauditevent' };
export const DOMAIN_AUDIT_QUERY: AuditQuery = { label: 'Domain Events', value: 'domainauditevent' };
export const AUDIT_EVENT_TYPE_PARAM = 'eventType';

export const ATTACHMENT_AUDIT_QUERY: AuditQuery = { label: 'Attachment Events', value: 'AttachmentAuditEvent' };
export const DOMAIN_AUDIT_QUERY: AuditQuery = { label: 'Domain Events', value: 'DomainAuditEvent' };
export const DOMAIN_PROPERTY_AUDIT_QUERY: AuditQuery = {
label: 'Domain Property Events',
value: 'domainpropertyauditevent',
value: 'DomainPropertyAuditEvent',
};
export const QUERY_UPDATE_AUDIT_QUERY: AuditQuery = {
hasDetail: true,
label: 'Query Update Events',
value: 'queryupdateauditevent',
value: 'QueryUpdateAuditEvent',
};

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

export const INVENTORY_AUDIT_QUERY: AuditQuery = {
hasDetail: true,
hasTransactionId: true,
label: 'Storage Management Events',
value: 'inventoryauditevent',
value: 'InventoryAuditEvent',
};
export const LIST_AUDIT_QUERY: AuditQuery = { hasTransactionId: true, 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',
value: 'groupauditevent',
value: 'GroupAuditEvent',
};
export const CONTAINER_AUDIT_QUERY: AuditQuery = {
containerFilter: Query.ContainerFilter.allFolders,
label: 'Folder Events',
value: 'containerauditevent',
value: 'ContainerAuditEvent',
};
export const SAMPLE_TYPE_AUDIT_QUERY: AuditQuery = {
hasTransactionId: true,
label: 'Sample Type Events',
value: 'samplesetauditevent',
value: 'SampleSetAuditEvent',
};
export const SAMPLE_TIMELINE_AUDIT_QUERY: AuditQuery = {
hasDetail: true,
hasTransactionId: true,
label: 'Sample Timeline Events',
value: 'sampletimelineevent',
value: 'SampleTimelineEvent',
};
export const USER_AUDIT_QUERY: AuditQuery = {
containerFilter: Query.ContainerFilter.allFolders,
label: 'User Events',
value: 'userauditevent',
value: 'UserAuditEvent',
};
export const ASSAY_AUDIT_QUERY: AuditQuery = {
hasTransactionId: true,
value: 'assayauditevent',
label: 'Assay Events',
value: 'AssayAuditEvent',
};
export const ASSAY_RESULT_AUDIT_QUERY: AuditQuery = {
hasDetail: true,
hasTransactionId: true,
isQueryUpdateEvent: true,
label: 'Assay Result Events',
value: 'assayresultauditevent',
value: 'AssayResultAuditEvent',
};
export const WORKFLOW_AUDIT_QUERY: AuditQuery = {
hasDetail: true,
label: 'Sample Workflow Events',
value: 'samplesworkflowauditevent',
value: 'SamplesWorkflowAuditEvent',
};
export const SOURCE_AUDIT_QUERY: AuditQuery = {
hasDetail: true,
hasTransactionId: true,
isQueryUpdateEvent: true,
label: 'Sources Events',
value: 'sourcesauditevent',
};

export const NOTEBOOK_AUDIT_QUERY: AuditQuery = {
label: 'Notebook Events',
value: 'LabBookEvent',
};

export const NOTEBOOK_REVIEW_AUDIT_QUERY: AuditQuery = {
label: 'Notebook Review Events',
value: 'NotebookEvent',
value: 'SourcesAuditEvent',
};

export const NOTEBOOK_AUDIT_QUERY: AuditQuery = { label: 'Notebook Events', value: 'LabBookEvent' };
export const NOTEBOOK_REVIEW_AUDIT_QUERY: AuditQuery = { label: 'Notebook Review Events', value: 'NotebookEvent' };
export const REGISTRY_AUDIT_QUERY: AuditQuery = { label: 'Registry Events', value: 'RegistryEvent' };

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',
value: 'FileSystem',
};

export const AUDIT_EVENT_TYPE_PARAM = 'eventType';
export const PLATE_AUDIT_QUERY: AuditQuery = {
hasDetail: true,
hasTransactionId: true,
label: 'Plate Events',
value: 'PlateEvent',
};

export const PLATE_DATA_AUDIT_QUERY: AuditQuery = {
hasDetail: true,
hasTransactionId: true,
isQueryUpdateEvent: true,
label: 'Plate Data Events',
value: 'PlateDataAuditEvent',
};

export const PLATE_SET_AUDIT_QUERY: AuditQuery = {
hasDetail: true,
hasTransactionId: true,
label: 'Plate Set Events',
value: 'PlateSetEvent',
};

export const COMMON_AUDIT_QUERIES: AuditQuery[] = [
ATTACHMENT_AUDIT_QUERY,
DOMAIN_AUDIT_QUERY,
DOMAIN_PROPERTY_AUDIT_QUERY,
FILE_SYSTEM_AUDIT_QUERY,
QUERY_UPDATE_AUDIT_QUERY,
GROUP_AUDIT_QUERY,
INVENTORY_AUDIT_QUERY,
LIST_AUDIT_QUERY,
GROUP_AUDIT_QUERY,
QUERY_UPDATE_AUDIT_QUERY,
SAMPLE_TYPE_AUDIT_QUERY,
SAMPLE_TIMELINE_AUDIT_QUERY,
USER_AUDIT_QUERY,
];

export const EXPERIMENT_AUDIT_EVENT = 'experimentauditevent';
export const EXPERIMENT_AUDIT_EVENT = 'ExperimentAuditEvent';

export const AUDIT_DETAIL_FIELD_VALUE_INHERITED = '$$aliquot-inherited-field$$';
35 changes: 20 additions & 15 deletions packages/components/src/internal/components/auditlog/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
*/
import React, { ReactNode } from 'react';
import { Map, OrderedMap } from 'immutable';
import { QueryKey } from '@labkey/api';

import { FREEZER_MANAGER_PRODUCT_ID, isSampleManagerEnabled } from '../../app/products';
import {
isAssayEnabled,
isChartBuilderEnabled,
isELNEnabled,
isPlatesEnabled,
isProductFoldersEnabled,
isRegistryEnabled,
isWorkflowEnabled,
Expand All @@ -30,31 +32,28 @@ import {
DATACLASS_DATA_UPDATE_AUDIT_QUERY,
NOTEBOOK_AUDIT_QUERY,
NOTEBOOK_REVIEW_AUDIT_QUERY,
PLATE_AUDIT_QUERY,
PLATE_DATA_AUDIT_QUERY,
PLATE_SET_AUDIT_QUERY,
REGISTRY_AUDIT_QUERY,
REPORT_AUDIT_QUERY,
SOURCE_AUDIT_QUERY,
WORKFLOW_AUDIT_QUERY,
} from './constants';
import { QueryKey } from '@labkey/api';
import { GENERAL_ASSAY_PROVIDER_NAME } from '../assay/constants';

export function getAuditQueries(ctx: ModuleContext): AuditQuery[] {
const queries = [...COMMON_AUDIT_QUERIES];

if (isProductFoldersEnabled(ctx)) queries.push(CONTAINER_AUDIT_QUERY);
if (isWorkflowEnabled(ctx)) queries.push(WORKFLOW_AUDIT_QUERY);
if (isAssayEnabled(ctx)) {
queries.push(ASSAY_AUDIT_QUERY);
queries.push(ASSAY_RESULT_AUDIT_QUERY);
}
if (isAssayEnabled(ctx)) queries.push(ASSAY_AUDIT_QUERY, ASSAY_RESULT_AUDIT_QUERY);
if (isSampleManagerEnabled(ctx) && !isRegistryEnabled(ctx)) queries.push(SOURCE_AUDIT_QUERY);
if (isRegistryEnabled(ctx)) {
queries.push(DATACLASS_DATA_UPDATE_AUDIT_QUERY);
queries.push(REGISTRY_AUDIT_QUERY);
}
if (isELNEnabled(ctx)) {
queries.push(NOTEBOOK_AUDIT_QUERY);
queries.push(NOTEBOOK_REVIEW_AUDIT_QUERY);
}
if (isRegistryEnabled(ctx)) queries.push(DATACLASS_DATA_UPDATE_AUDIT_QUERY, REGISTRY_AUDIT_QUERY);
if (isELNEnabled(ctx)) queries.push(NOTEBOOK_AUDIT_QUERY, NOTEBOOK_REVIEW_AUDIT_QUERY);
if (isChartBuilderEnabled(ctx)) queries.push(REPORT_AUDIT_QUERY);
if (isPlatesEnabled(ctx)) queries.push(PLATE_AUDIT_QUERY, PLATE_DATA_AUDIT_QUERY, PLATE_SET_AUDIT_QUERY);

return queries.sort(naturalSortByProperty('label'));
}

Expand Down Expand Up @@ -113,11 +112,17 @@ export function getTimelineEntityUrl(d: Record<string, any>): AppURL {
switch (urlType) {
case 'assayRun':
if (Array.isArray(value) && value.length > 1) {
url = AppURL.create(ASSAYS_KEY, 'general', value[0], 'runs', value[1]);
url = AppURL.create(
ASSAYS_KEY,
GENERAL_ASSAY_PROVIDER_NAME.toLowerCase(),
value[0],
'runs',
value[1]
);
}
break;
case ASSAYS_KEY:
url = AppURL.create(ASSAYS_KEY, 'general', value);
url = AppURL.create(ASSAYS_KEY, GENERAL_ASSAY_PROVIDER_NAME.toLowerCase(), value);
break;
case 'inventoryBox':
url = AppURL.create(BOXES_KEY, value).setProductId(FREEZER_MANAGER_PRODUCT_ID);
Expand Down