Skip to content

Commit 039da1b

Browse files
authored
[OGUI-1871] Add labels dynamically calculated for repository (#3288)
* extract trim functionality into its own function and test it * add labels generation dynamically for layout depending on contained objects * include layout repository tests into test file * update functionality for updateLayout not to store the labels if sent in the request
1 parent bc49b48 commit 039da1b

11 files changed

Lines changed: 579 additions & 49 deletions

File tree

QualityControl/lib/dtos/LayoutDto.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const ALLOWED_LAYOUT_FIELDS = [
2626
'owner_id',
2727
'owner_name',
2828
'tabs',
29+
'labels',
2930
];
3031

3132
/**
@@ -71,6 +72,7 @@ export const LayoutDto = Joi.object({
7172
id: Joi.string().required(),
7273
name: Joi.string().min(3).max(40).required(),
7374
tabs: Joi.array().min(1).max(45).items(TabsDto).required(),
75+
labels: Joi.array().items(Joi.string()).default([]),
7476
owner_id: Joi.number().min(0).required(),
7577
owner_name: Joi.string().required(),
7678
description: Joi.string().min(0).max(100).optional(),

QualityControl/lib/repositories/LayoutRepository.js

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
import { NotFoundError } from '@aliceo2/web-ui';
1515
import { BaseRepository } from './BaseRepository.js';
16+
import { addLabelsToLayout } from '../utils/layout/addLabelsToLayout.js';
17+
import { trimLayoutPerRequiredFields } from '../utils/layout/trimLayoutPerRequiredFields.js';
1618

1719
/**
1820
* LayoutRepository class to handle CRUD operations for Layouts.
@@ -26,20 +28,18 @@ export class LayoutRepository extends BaseRepository {
2628
* @param {object} [options.filter] - Filter layouts by containing filter.objectPath, case insensitive
2729
* @returns {Array<object>} Array of layout objects matching the filters, containing only the specified fields
2830
*/
29-
listLayouts({ name, fields = [], filter } = {}) {
31+
listLayouts({ name, fields, filter } = {}) {
3032
const { layouts } = this._jsonFileService.data;
3133
const filteredLayouts = this._filterLayouts(layouts, { ...filter, name });
3234

33-
if (fields.length === 0) {
34-
return filteredLayouts;
35-
}
36-
return filteredLayouts.map((layout) => {
37-
const layoutObj = {};
38-
fields.forEach((field) => {
39-
layoutObj[field] = layout[field];
35+
const trimmedAndLabelledLayouts = filteredLayouts
36+
.map((layout) => {
37+
const labeledLayout = addLabelsToLayout(layout);
38+
const trimmedLayout = trimLayoutPerRequiredFields(labeledLayout, fields);
39+
return trimmedLayout;
4040
});
41-
return layoutObj;
42-
});
41+
42+
return trimmedAndLabelledLayouts;
4343
}
4444

4545
/**
@@ -79,25 +79,27 @@ export class LayoutRepository extends BaseRepository {
7979
* @throws {NotFoundError} - if the layout is not found
8080
*/
8181
readLayoutById(layoutId) {
82-
const foundLayout = this._jsonFileService.data.layouts.find((layout) => layout.id === layoutId);
83-
if (!foundLayout) {
82+
const layout = this._jsonFileService.data.layouts.find((layout) => layout.id === layoutId);
83+
if (!layout) {
8484
throw new NotFoundError(`layout (${layoutId}) not found`);
8585
}
86-
return foundLayout;
86+
const labeledLayout = addLabelsToLayout(layout);
87+
return labeledLayout;
8788
}
8889

8990
/**
9091
* Given a string, representing layout name, retrieve the layout if it exists
9192
* @param {string} layoutName - name of the layout to retrieve
9293
* @returns {Layout} - object with layout information
93-
* @throws
94+
* @throws {NotFoundError} - if the layout is not found
9495
*/
9596
readLayoutByName(layoutName) {
9697
const layout = this._jsonFileService.data.layouts.find((layout) => layout.name === layoutName);
9798
if (!layout) {
9899
throw new NotFoundError(`Layout (${layoutName}) not found`);
99100
}
100-
return layout;
101+
const labeledLayout = addLabelsToLayout(layout);
102+
return labeledLayout;
101103
}
102104

103105
/**
@@ -127,9 +129,17 @@ export class LayoutRepository extends BaseRepository {
127129
* @param {string} layoutId - id of the layout to be updated
128130
* @param {LayoutDto} newData - layout new data
129131
* @returns {string} id of the layout updated
132+
* @throws {NotFoundError} - if the layout is not found
130133
*/
131134
async updateLayout(layoutId, newData) {
132-
const layout = this.readLayoutById(layoutId);
135+
if (newData.labels) {
136+
// labels are retrieved on front-end and might be send as PATCH/PUT if forgotten by developer
137+
delete newData.labels;
138+
}
139+
const layout = this._jsonFileService.data.layouts.find((layout) => layout.id === layoutId);
140+
if (!layout) {
141+
throw new NotFoundError(`layout (${layoutId}) not found`);
142+
}
133143
Object.assign(layout, newData);
134144
await this._jsonFileService.writeToFile();
135145
return layoutId;
@@ -139,10 +149,13 @@ export class LayoutRepository extends BaseRepository {
139149
* Delete a single layout by its id
140150
* @param {string} layoutId - id of the layout to be removed
141151
* @returns {string} id of the layout deleted
152+
* @throws {NotFoundError} - if the layout is not found
142153
*/
143154
async deleteLayout(layoutId) {
144-
const layout = this.readLayoutById(layoutId);
145-
const index = this._jsonFileService.data.layouts.indexOf(layout);
155+
const index = this._jsonFileService.data.layouts.findIndex((layout) => layout.id === layoutId);
156+
if (index === -1) {
157+
throw new NotFoundError(`layout (${layoutId}) not found`);
158+
}
146159
this._jsonFileService.data.layouts.splice(index, 1);
147160
await this._jsonFileService.writeToFile();
148161
return layoutId;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* @license
3+
* Copyright CERN and copyright holders of ALICE O2. This software is
4+
* distributed under the terms of the GNU General Public License v3 (GPL
5+
* Version 3), copied verbatim in the file "COPYING".
6+
*
7+
* See http://alice-o2.web.cern.ch/license for full licensing information.
8+
*
9+
* In applying this license CERN does not waive the privileges and immunities
10+
* granted to it by virtue of its status as an Intergovernmental Organization
11+
* or submit itself to any jurisdiction.
12+
*/
13+
14+
/**
15+
* Method to identify the unique prefix (encountering first '/') of objects and add it as a set of labels to layout
16+
* @param {LayoutDto} layout - layout object to which labels will be added
17+
* @returns {{LayoutDto, labels: string[]}} - layout object with added labels
18+
*/
19+
export const addLabelsToLayout = (layout) => {
20+
const labelsSet = new Set();
21+
layout.tabs?.forEach((tab) => {
22+
tab.objects?.forEach((obj) => {
23+
if (obj.name) {
24+
const [prefix] = obj.name.split('/');
25+
labelsSet.add(prefix);
26+
}
27+
});
28+
});
29+
return { ...layout, labels: Array.from(labelsSet) };
30+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* @license
3+
* Copyright CERN and copyright holders of ALICE O2. This software is
4+
* distributed under the terms of the GNU General Public License v3 (GPL
5+
* Version 3), copied verbatim in the file "COPYING".
6+
*
7+
* See http://alice-o2.web.cern.ch/license for full licensing information.
8+
*
9+
* In applying this license CERN does not waive the privileges and immunities
10+
* granted to it by virtue of its status as an Intergovernmental Organization
11+
* or submit itself to any jurisdiction.
12+
*/
13+
14+
/**
15+
* Trims a layout object to only include requested fields
16+
* @param {LayoutDto} layout - layout object to be trimmed
17+
* @param {string[]} [fields = []] - Array of field names to include in the returned layout object
18+
* @returns {Partial<LayoutDto>} - Trimmed layout object
19+
*/
20+
export const trimLayoutPerRequiredFields = (layout, fields = []) => {
21+
if (fields.length === 0) {
22+
return layout;
23+
}
24+
const trimmedLayout = {};
25+
for (const field of fields) {
26+
if (field in layout) {
27+
trimmedLayout[field] = layout[field];
28+
}
29+
}
30+
return trimmedLayout;
31+
};
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export const RequestFields = Object.freeze({
2-
LAYOUT_CARD: 'id,name,owner_id,owner_name,description,isOfficial',
2+
LAYOUT_CARD: 'id,name,owner_id,owner_name,description,isOfficial,labels',
33
});

QualityControl/test/api/layouts/api-get-layout.test.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { OWNER_TEST_TOKEN, URL_ADDRESS } from '../config.js';
1717
import request from 'supertest';
1818
import { deepStrictEqual } from 'node:assert';
1919
import { LAYOUT_MOCK_4, LAYOUT_MOCK_5, LAYOUT_MOCK_6 } from '../../demoData/layout/layout.mock.js';
20+
import { addLabelsToLayout } from '../../../lib/utils/layout/addLabelsToLayout.js';
2021

2122
export const apiGetLayoutsTests = () => {
2223
suite('GET /layouts', () => {
@@ -43,7 +44,9 @@ export const apiGetLayoutsTests = () => {
4344
if (!Array.isArray(res.body)) {
4445
throw new Error('Expected array of layouts');
4546
}
46-
47+
res.body.forEach((layout) => {
48+
delete layout.labels; // remove labels for deep comparison
49+
});
4750
deepStrictEqual(res.body, [LAYOUT_MOCK_4, LAYOUT_MOCK_5], 'Unexpected Layout structure was returned');
4851
});
4952
});
@@ -80,10 +83,11 @@ export const apiGetLayoutsTests = () => {
8083
suite('GET /layout/:id', () => {
8184
test('should return a single layout by id', async () => {
8285
const layoutId = '671b8c22402408122e2f20dd';
86+
const expectedLayout = addLabelsToLayout(LAYOUT_MOCK_6);
8387
await request(`${URL_ADDRESS}/api/layout/${layoutId}`)
8488
.get(`?token=${OWNER_TEST_TOKEN}`)
8589
.expect(200)
86-
.expect((res) => deepStrictEqual(res.body, LAYOUT_MOCK_6, 'Unexpected Layout structure was returned'));
90+
.expect((res) => deepStrictEqual(res.body, expectedLayout, 'Unexpected Layout structure was returned'));
8791
});
8892

8993
test('should return 400 when id parameter is an empty string', async () => {
@@ -103,19 +107,22 @@ export const apiGetLayoutsTests = () => {
103107
suite('GET /layout?name=', () => {
104108
test('should return layout by name', async () => {
105109
const layoutName = 'a-test';
110+
const expectedLayout = addLabelsToLayout(LAYOUT_MOCK_5);
106111
await request(`${URL_ADDRESS}/api/layout`)
107112
.get(`?token=${OWNER_TEST_TOKEN}&name=${layoutName}`)
108113
.expect(200)
109-
.expect((res) => deepStrictEqual(res.body, LAYOUT_MOCK_5, 'Unexpected Layout structure was returned'));
114+
.expect((res) => deepStrictEqual(res.body, expectedLayout, 'Unexpected Layout structure was returned'));
110115
});
111116

112117
test('should return layout by runDefinition', async () => {
113118
const runDefinition = 'a-test';
119+
const expectedLayout = addLabelsToLayout(LAYOUT_MOCK_5);
114120
await request(`${URL_ADDRESS}/api/layout`)
115121
.get(`?token=${OWNER_TEST_TOKEN}&runDefinition=${runDefinition}`)
116122
.expect(200)
117-
.expect((res) => deepStrictEqual(res.body, LAYOUT_MOCK_5, 'Unexpected Layout structure was returned'));
123+
.expect((res) => deepStrictEqual(res.body, expectedLayout, 'Unexpected Layout structure was returned'));
118124
});
125+
119126
test('should return layout by runDefinition and pdpBeamType combination', async () => {
120127
const runDefinition = 'rundefinition';
121128
const pdpBeamType = 'pdpBeamType';

QualityControl/test/lib/controllers/LayoutController.test.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
* or submit itself to any jurisdiction.
1313
*/
1414

15-
import { ok, throws, doesNotThrow, AssertionError } from 'node:assert';
15+
import { ok, deepStrictEqual, throws, doesNotThrow, AssertionError } from 'node:assert';
1616
import { suite, test, beforeEach } from 'node:test';
1717
import sinon from 'sinon';
1818

@@ -416,6 +416,7 @@ export const layoutControllerTestSuite = async () => {
416416
tabs: [{ name: 'tab', id: '1', columns: 2, objects: [] }],
417417
owner_id: 1,
418418
owner_name: 'one',
419+
labels: [],
419420
collaborators: [],
420421
displayTimestamp: false,
421422
autoTabChange: 0,
@@ -467,6 +468,7 @@ export const layoutControllerTestSuite = async () => {
467468
tabs: [{ name: 'tab', id: '1', columns: 2, objects: [] }],
468469
owner_id: 1,
469470
owner_name: 'one',
471+
labels: [],
470472
collaborators: [],
471473
displayTimestamp: false,
472474
autoTabChange: 0,
@@ -643,6 +645,7 @@ export const layoutControllerTestSuite = async () => {
643645
owner_name: 'admin',
644646
tabs: [{ id: '123', name: 'tab', columns: 2, objects: [] }],
645647
collaborators: [],
648+
labels: [],
646649
displayTimestamp: false,
647650
autoTabChange: 0,
648651
};
@@ -671,6 +674,7 @@ export const layoutControllerTestSuite = async () => {
671674
owner_id: 1,
672675
owner_name: 'admin',
673676
tabs: [{ id: '123', name: 'tab', columns: 2, objects: [] }],
677+
labels: [],
674678
collaborators: [],
675679
displayTimestamp: false,
676680
autoTabChange: 0,
@@ -682,7 +686,10 @@ export const layoutControllerTestSuite = async () => {
682686
status: 500,
683687
title: 'Unknown Error',
684688
}), 'DataConnector error message is incorrect');
685-
ok(jsonStub.createLayout.calledWith(expected), 'New layout body was not used in data connector call');
689+
690+
// Log what was actually called for debugging
691+
const actualCall = jsonStub.createLayout.getCall(0)?.args[0];
692+
deepStrictEqual(expected, actualCall);
686693
});
687694
});
688695

0 commit comments

Comments
 (0)