Skip to content

Commit 6571ec0

Browse files
[OGUI-1767] Runs mode object tree smart refresh + header refactor (#3066)
This PR provides with a smart refresh in which the objects tree list is refreshed only if new paths need to be listed. It also includes a refactor of the headers for a better styling.
1 parent 253d66f commit 6571ec0

File tree

11 files changed

+173
-87
lines changed

11 files changed

+173
-87
lines changed

QualityControl/public/common/filters/filterViews.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ export function filtersPanel(filterModel, viewModel) {
6262
runNumber,
6363
runStatus,
6464
isVisible,
65+
lastRefresh,
66+
ONGOING_RUN_INTERVAL_MS: refreshRate,
6567
} = filterModel;
6668
const onInputCallback = setFilterValue.bind(filterModel);
6769
const onChangeCallback = setFilterValue.bind(filterModel);
@@ -84,10 +86,7 @@ export function filtersPanel(filterModel, viewModel) {
8486
...filtersList.map((filter) =>
8587
createFilterElement(filter, filterMap, onInputCallback, onEnterCallback, onChangeCallback)),
8688
]),
87-
isRunModeActivated && runStatusPanel(
88-
runNumber,
89-
runStatus,
90-
),
89+
isRunModeActivated && runStatusPanel({ runNumber, runStatus, lastRefresh, refreshRate }),
9190
],
9291
);
9392
};

QualityControl/public/common/filters/model/FilterModel.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export default class FilterModel extends Observable {
3838
this._runNumber = null;
3939
this._runStatus = RemoteData.notAsked();
4040
this._isRunModeActivated = false;
41+
this._lastRefresh = null;
4142

4243
this.ONGOING_RUN_INTERVAL_MS = 15000;
4344
}
@@ -122,6 +123,8 @@ export default class FilterModel extends Observable {
122123
this._manageRunsModeInterval(baseViewModel);
123124
}
124125
baseViewModel.triggerFilter();
126+
this._lastRefresh = Date.now();
127+
this.notify();
125128
}
126129

127130
/**
@@ -193,6 +196,7 @@ export default class FilterModel extends Observable {
193196
this.isRunModeActivated = false;
194197
this.runNumber = null;
195198
this.runStatus = RemoteData.notAsked();
199+
this._lastRefresh = null;
196200
this.clearRunsModeInterval();
197201
this.notify();
198202
}
@@ -205,22 +209,26 @@ export default class FilterModel extends Observable {
205209
*/
206210
async _manageRunsModeInterval(baseViewModel) {
207211
this.clearRunsModeInterval();
208-
this._currentViewModel = baseViewModel;
212+
const currentRunNumber = this.runNumber;
213+
if (this.runStatus?.payload?.runStatus !== RunStatus.ONGOING) {
214+
return;
215+
}
209216
this._runsModeInterval = setInterval(async () => {
210-
if (!this._currentViewModel) {
217+
if (!baseViewModel) {
211218
return;
212219
}
213220
this.runStatus = RemoteData.loading();
214221
this.notify();
215-
this.runStatus = await this.filterService.getRunStatus(this.runNumber);
222+
this.runStatus = await this.filterService.getRunStatus(currentRunNumber);
216223
this.notify();
217224
this.runStatus.match({
218225
Success: (res) => {
219226
if (res?.runStatus !== RunStatus.ONGOING) {
220227
this.clearRunsModeInterval();
221-
} else {
222-
baseViewModel.triggerFilter();
223228
}
229+
baseViewModel.triggerFilter();
230+
this._lastRefresh = Date.now();
231+
this.notify();
224232
},
225233
Other: () => this.clearRunsModeInterval(),
226234
});
@@ -236,7 +244,6 @@ export default class FilterModel extends Observable {
236244
clearInterval(this._runsModeInterval);
237245
this._runsModeInterval = null;
238246
}
239-
this._currentViewModel = null;
240247
}
241248

242249
/**
@@ -307,6 +314,14 @@ export default class FilterModel extends Observable {
307314
this._isRunModeActivated = value;
308315
}
309316

317+
get lastRefresh() {
318+
return this._lastRefresh;
319+
}
320+
321+
get runsModeInterval() {
322+
return this._runsModeInterval;
323+
}
324+
310325
/**
311326
* Validates a run number for run mode.
312327
* @returns {{ isValid: boolean, title: string }} An object indicating
@@ -336,4 +351,19 @@ export default class FilterModel extends Observable {
336351
}
337352
return { isValid: true, title: 'Update filters' };
338353
};
354+
355+
/**
356+
* Executes a generic check to determine if a refresh is required.
357+
* @param {() => Promise<T>} fetchFn - Async function to fetch the data or object.
358+
* @param {(RemoteData) => boolean} validateFn - Validates whether the fetched result means no refresh is needed.
359+
* @returns {Promise<{ refreshNeeded: boolean, data: object | null }>}
360+
*/
361+
async refreshCheck(fetchFn, validateFn) {
362+
if (this._runsModeInterval) {
363+
const data = await fetchFn();
364+
const refreshNeeded = validateFn(data);
365+
return { refreshNeeded, data };
366+
}
367+
return { refreshNeeded: true, data: null };
368+
}
339369
}

QualityControl/public/common/filters/runMode/runStatusPanel.js

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,71 @@
1111
* or submit itself to any jurisdiction.
1212
*/
1313

14+
import { RunStatus } from '../../../../../library/runStatus.enum.js';
15+
import { spinner } from '../../spinner.js';
1416
import { h } from '/js/src/index.js';
1517

1618
/**
17-
* Render a run mode switch
18-
* @param {number} runNumber - The run number
19-
* @param {RunStatus} status - The run status
20-
* @returns {HTMLElement} - The rendered run status panel
19+
* Creates and returns a run status panel element displaying the current run number,
20+
* its status, the last refresh timestamp, and the refresh rate.
21+
* @param {object} options Options for rendering the run status panel.
22+
* @param {number} options.runNumber The current run number to display.
23+
* @param {string} options.runStatus The status of the run (e.g., "running", "completed").
24+
* @param {Date} options.lastRefresh Timestamp of the last refresh.
25+
* @param {number} options.refreshRate - Refresh rate in milliseconds.
26+
* @returns {vnode} The element representing the run status panel.
2127
*/
22-
export const runStatusPanel = (runNumber, status) =>
23-
status.match({
28+
export const runStatusPanel = ({ runNumber, runStatus, lastRefresh, refreshRate = 15000 }) => {
29+
const runNumberPanel = h('span', { id: 'runNumberLabel' }, [
30+
'Run ',
31+
h('b', `#${runNumber}`),
32+
]);
33+
const statusPanel = (runStatus) =>
34+
runStatus
35+
? h(
36+
`.badge.white.bg-${runStatus === RunStatus.ONGOING ? 'success' : 'gray-darker'}`,
37+
{ id: 'runStatusBadge' },
38+
runStatus,
39+
)
40+
: h('span', spinner(1));
41+
const formatDateTime = (dateStr) =>
42+
new Date(dateStr).toLocaleString('en-GB'); // dd/mm/yyyy, hh:mm:ss
43+
44+
const lastUpdatePanel = (runStatus) => [
45+
h(
46+
'span.highlight',
47+
{ id: 'lastUpdate' },
48+
`Last update: ${formatDateTime(lastRefresh)}`,
49+
),
50+
runStatus === RunStatus.ONGOING &&
51+
h(
52+
'span',
53+
{ id: 'refreshInfo' },
54+
` - As run is ONGOING, will refresh every ${refreshRate / 1000} seconds`,
55+
),
56+
];
57+
58+
return runStatus.match({
2459
Loading: () =>
2560
h('.flex-row.g1.items-center.justify-center', [
26-
h('b', { id: 'runNumberLabel' }, `#${runNumber}`),
27-
h('.flex-row.g1', [
28-
h('label', 'Status: '),
29-
h('b.color-gray', 'Loading...'),
30-
]),
61+
runNumberPanel,
62+
statusPanel(null),
3163
]),
3264

33-
Success: (res) =>
34-
h('.flex-row.g1.items-center.justify-center', { id: 'runStatusPanel' }, [
35-
h('b', { id: 'runNumberLabel' }, `#${runNumber}`),
36-
h('.flex-row.g1', [
37-
h('span', 'Status: '),
38-
h(
39-
`b.${
40-
res?.runStatus === 'ONGOING' ? 'success' : 'gray'
41-
}`,
42-
{
43-
id: 'runStatus',
44-
},
45-
res?.runStatus,
46-
),
65+
Success: (res) => {
66+
const runStatus = res?.runStatus;
67+
const shouldShowTimestamp = runStatus === RunStatus.ONGOING || runStatus === RunStatus.ENDED;
68+
return h('.flex-column', [
69+
h('.flex-row.g1.items-center.justify-center', { id: 'runStatusPanel' }, [
70+
runNumberPanel,
71+
statusPanel(runStatus),
4772
]),
48-
]),
73+
shouldShowTimestamp && h(
74+
'.flex-row.g1.items-center.justify-center.f7.gray-darker.text-center',
75+
lastUpdatePanel(runStatus),
76+
),
77+
]);
78+
},
4979
Other: () => null,
5080
});
81+
};

QualityControl/public/common/header.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,24 @@ import { filtersPanel } from './filters/filterViews.js';
3030
* @param {Model} model - root model of the application
3131
* @returns {vnode} - header element
3232
*/
33-
export default (model) => h('.flex-col', [
34-
h('.flex-row.p2.items-center', { id: 'qcg-header' }, [
35-
commonHeader(model),
36-
h('.flex-grow.flex-row', headerSpecific(model)),
37-
]),
38-
filterSpecific(model),
39-
]);
33+
export default (model) => {
34+
const specific = headerSpecific(model) || {};
35+
const { centerCol, rightCol } = specific;
36+
37+
return h('.flex-col', [
38+
h('.flex-row.p2.items-center', { id: 'qcg-header' }, [
39+
commonHeader(model),
40+
centerCol || h('.flex-grow'),
41+
rightCol || h('.w-33'),
42+
]),
43+
filterSpecific(model),
44+
]);
45+
};
4046

4147
/**
4248
* Shows the page specific header (center and right side)
4349
* @param {Model} model - root model of the application
44-
* @returns {vnode} - virtual node element
50+
* @returns {{centerCol: vnode, rightCol: vnode} | null}
4551
*/
4652
const headerSpecific = (model) => {
4753
const { layoutListModel, filterModel, layout, object, page } = model;

QualityControl/public/layout/view/header.js

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,19 @@ const toolbarViewMode = (layout, filterModel) => {
4343
const layoutItem = layout.item;
4444
const { isOfficial, owner_id, name } = layoutItem;
4545

46-
return [
47-
h('.flex-grow.text-center', h('.header-layout', [tabViewLinks(layoutItem, layout)])),
48-
h('.flex-grow.text-right', [
46+
return {
47+
centerCol: h('.flex-grow.text-center', [h('.header-layout', [tabViewLinks(layoutItem, layout)])]),
48+
rightCol: h('.w-33.text-right.g2.flex-row.justify-end', [
4949
h('b.f4.items-center', [isOfficial ? iconBadge() : '', layoutItem.name]),
5050
' ',
51-
// Show group button edit/duplicate only for owner of the layout shown
5251
h('.btn-group', [
5352
filterPanelToggleButton(filterModel),
5453
newLayoutButton(layout),
5554
jsonExportButton(layoutItem, name),
5655
layout.ownsLayout(owner_id) && [editDropdown(layout), deleteButton(layout)],
5756
]),
5857
]),
59-
];
58+
};
6059
};
6160

6261
/**
@@ -92,8 +91,8 @@ const toolbarEditMode = (layout, filterModel) => {
9291
layout.item.name = e.target.value.trim();
9392
};
9493

95-
return [
96-
h('.w-50.text-center', [
94+
return {
95+
centerCol: h('.flex-grow.text-center', [
9796
h('div', { class: 'header-layout' }, [
9897
h('span', editTabLinks(layout)),
9998
h('.btn-group', [
@@ -110,7 +109,7 @@ const toolbarEditMode = (layout, filterModel) => {
110109
]),
111110
]),
112111
]),
113-
h('.text-right.flex-grow', [
112+
rightCol: h('.w-33.text-right.flex-row.justify-end', [
114113
h('input.form-control.form-inline', {
115114
type: 'text',
116115
value: layout.item.name,
@@ -122,7 +121,7 @@ const toolbarEditMode = (layout, filterModel) => {
122121
cancelButton(layout),
123122
]),
124123
]),
125-
];
124+
};
126125
};
127126

128127
/**

QualityControl/public/object/QCObject.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,16 +168,32 @@ export default class QCObject extends BaseViewModel {
168168
this.notify();
169169
}
170170

171+
/**
172+
* Checks if the object list needs to be refreshed.
173+
* @returns {Promise<{ refreshNeeded: boolean, data: object | null }>}
174+
* whether a refresh is needed and the fetched data
175+
*/
176+
checkIfListHasToBeRefreshed() {
177+
const fetchFn = async () => await this.model.services.object.getObjects(true);
178+
const validateFn = (result) => result.isSuccess() && result.payload.paths?.length !== this.list?.length;
179+
return this.model.filterModel.refreshCheck(fetchFn, validateFn);
180+
}
181+
171182
/**
172183
* Ask server for all available objects, fills `tree` of objects
173184
* @returns {undefined}
174185
*/
175186
async loadList() {
187+
const { refreshNeeded, data } = await this.checkIfListHasToBeRefreshed();
188+
if (!refreshNeeded) {
189+
this.notify();
190+
return;
191+
}
176192
this.objectsRemote = RemoteData.loading();
177193
this.notify();
178194
this.queryingObjects = true;
179195
let offlineObjects = [];
180-
const result = await this.model.services.object.getObjects(this.model.filterModel.isRunModeActivated);
196+
const result = data ?? await this.model.services.object.getObjects(this.model.filterModel.isRunModeActivated);
181197

182198
if (result.isSuccess()) {
183199
offlineObjects = this.model.filterModel.isRunModeActivated ? result.payload.paths : result.payload;
@@ -539,7 +555,7 @@ export default class QCObject extends BaseViewModel {
539555
*/
540556
async triggerFilter() {
541557
// don't clear selected object refreshing in run mode
542-
if (!this.model.filterModel.isRunModeActivated) {
558+
if (!this.model.filterModel.isRunModeActivated || !this.model.filterModel.runsModeInterval) {
543559
this.selected = null;
544560
}
545561
this.loadList();

QualityControl/public/object/objectTreeHeader.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { filterPanelToggleButton } from '../common/filters/filterViews.js';
2121
* by name.
2222
* @param {QcObject} qcObject - Model that manages the QCObject state.
2323
* @param {FilterModel} filterModel - The model handeling the filter state
24-
* @returns {vnode} - virtual node element
24+
* @returns {{centerCol: vnode, rightCol: vnode} | null} - object with vnode elements
2525
*/
2626
export default function objectTreeHeader(qcObject, filterModel) {
2727
if (!qcObject.currentList) {
@@ -32,13 +32,14 @@ export default function objectTreeHeader(qcObject, filterModel) {
3232
? `${qcObject.searchResult.length} found of ${qcObject.currentList.length}`
3333
: `${qcObject.currentList.length} items`;
3434

35-
return [
36-
h('.w-33.text-center.flex-grow.flex-row.justify-center.items-center', [
35+
return {
36+
centerCol: h('.flex-grow.text-center.flex-row.justify-center.items-center', [
3737
h('b.f4', 'Objects'),
3838
' ',
3939
qcObject.objectsRemote.isSuccess() && h('span', `(${howMany})`),
4040
]),
41-
h('.flex-row.items-center.g2.justify-end', [
41+
42+
rightCol: h('.w-33.flex-row.items-center.g2.justify-end', [
4243
filterModel.isRunModeActivated ? null : filterPanelToggleButton(filterModel),
4344
' ',
4445
h('.dropdown', {
@@ -71,7 +72,7 @@ export default function objectTreeHeader(qcObject, filterModel) {
7172
}),
7273
' ',
7374
]),
74-
];
75+
};
7576
}
7677

7778
/**

QualityControl/public/pages/aboutView/components/aboutViewHeader.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@ import { h } from '/js/src/index.js';
1818
* Shows header of Framework Information
1919
* @returns {vnode} - virtual node element
2020
*/
21-
export default () => [
22-
h(
23-
'.w-50.flex-row.justify-center',
24-
h('b.f4.ph2', 'About'),
25-
),
26-
h('.flex-grow'),
27-
];
21+
export default () => ({
22+
centerCol: h('.flex-row.justify-center.flex-grow', [h('b.f4.ph2', 'About')]),
23+
rightCol: h('.w-33'),
24+
});

0 commit comments

Comments
 (0)