Skip to content

Commit f93a45b

Browse files
authored
Merge pull request #2407 from beyonnex-io/feature/ui-history-exploration
Add history exploration capabilities to Explorer UI
2 parents ea8aa00 + 8961a9f commit f93a45b

22 files changed

Lines changed: 2117 additions & 13 deletions

ui/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
<div class="collapse show" id="collapseThings" data-bs-parent="#page-content">
9797
<div id="thingsHTML"></div>
9898
<div id="featuresHTML"></div>
99-
<div id="messagesIncomingHTML"></div>
99+
<div id="thingUpdatesHTML"></div>
100100
</div>
101101
<div class="collapse" id=collapsePolicies data-bs-parent="#page-content">
102102
<div id="policyHTML"></div>

ui/main.scss

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,3 +286,67 @@ h5>.badge {
286286
button.busy .spinner-border {
287287
visibility:visible !important;
288288
}
289+
290+
/* ace-diff styles are imported via main.ts */
291+
292+
#diffContainer {
293+
.acediff {
294+
--acediff-diff-bg: rgba(58, 140, 154, 0.12);
295+
--acediff-diff-border: rgba(58, 140, 154, 0.4);
296+
--acediff-diff-char-bg: rgba(58, 140, 154, 0.25);
297+
--acediff-gutter-bg: #f8f9fa;
298+
--acediff-gutter-border: #dee2e6;
299+
}
300+
301+
.acediff__connector {
302+
fill: rgba(58, 140, 154, 0.2);
303+
stroke: rgba(58, 140, 154, 0.5);
304+
}
305+
}
306+
307+
/* History mode styles */
308+
.history-active #details {
309+
border-left: 4px solid var(--bs-warning);
310+
}
311+
312+
.history-banner {
313+
font-size: 0.85em;
314+
}
315+
316+
.history-badge {
317+
font-size: 0.6em;
318+
vertical-align: top;
319+
}
320+
321+
#historyRevisionControls .form-range {
322+
align-self: center;
323+
}
324+
325+
/* Revision picker alignment: force fixed sizes to align stacked rows */
326+
#collapseThings .input-group input[type="number"],
327+
#collapseThingUpdates .input-group input[type="number"] {
328+
flex: 0 0 80px !important;
329+
width: 80px !important;
330+
padding-right: 6px !important;
331+
-moz-appearance: textfield !important;
332+
}
333+
334+
#collapseThings .input-group input[type="number"]::-webkit-outer-spin-button,
335+
#collapseThings .input-group input[type="number"]::-webkit-inner-spin-button,
336+
#collapseThingUpdates .input-group input[type="number"]::-webkit-outer-spin-button,
337+
#collapseThingUpdates .input-group input[type="number"]::-webkit-inner-spin-button {
338+
-webkit-appearance: none;
339+
margin: 0;
340+
}
341+
342+
.rev-label {
343+
flex: 0 0 75px !important;
344+
width: 75px !important;
345+
justify-content: center;
346+
}
347+
348+
.rev-action-btn {
349+
flex: 0 0 80px !important;
350+
width: 80px !important;
351+
justify-content: center;
352+
}

ui/main.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import { Dropdown } from 'bootstrap';
1414
/* eslint-disable new-cap */
1515
import 'bootstrap/dist/css/bootstrap.min.css';
16+
import 'ace-diff/styles';
1617
import './main.scss';
1718
import * as Connections from './modules/connections/connections.js';
1819
import * as ConnectionsCRUD from './modules/connections/connectionsCRUD.js';
@@ -33,13 +34,16 @@ import * as Attributes from './modules/things/attributes.js';
3334
import * as FeatureMessages from './modules/things/featureMessages.js';
3435
import * as Features from './modules/things/features.js';
3536
import * as Fields from './modules/things/fields.js';
36-
import * as MessagesIncoming from './modules/things/messagesIncoming.js';
37+
import * as ThingsHistory from './modules/things/thingsHistory.js';
38+
import * as ThingUpdates from './modules/things/thingUpdates.js';
3739
import * as SearchFilter from './modules/things/searchFilter.js';
3840
import * as ThingMessages from './modules/things/thingMessages.js';
3941
import * as Things from './modules/things/things.js';
4042
import * as ThingsCRUD from './modules/things/thingsCRUD.js';
4143
import * as ThingsSearch from './modules/things/thingsSearch.js';
4244
import * as ThingsSSE from './modules/things/thingsSSE.js';
45+
import { SubDiff } from './modules/things/subDiff.js';
46+
import { ThingsDiff } from './modules/things/thingsDiff.js';
4347
import { WoTDescription } from './modules/things/wotDescription.js';
4448
import * as Utils from './modules/utils.js';
4549
import './modules/utils/crudToolbar.js';
@@ -56,7 +60,8 @@ document.addEventListener('DOMContentLoaded', async function() {
5660
await ThingsCRUD.ready();
5761
await ThingMessages.ready();
5862
await ThingsSSE.ready();
59-
MessagesIncoming.ready();
63+
ThingsHistory.ready();
64+
ThingUpdates.ready();
6065
Attributes.ready();
6166
await Fields.ready();
6267
await SearchFilter.ready();
@@ -86,13 +91,35 @@ document.addEventListener('DOMContentLoaded', async function() {
8691
Things.addChangeListener(thingDescription.onReferenceChanged);
8792
await thingDescription.ready();
8893

94+
const thingsDiff = ThingsDiff({
95+
itemsId: 'tabItemsThing',
96+
contentId: 'tabContentThing',
97+
});
98+
await thingsDiff.ready();
99+
Things.addChangeListener(thingsDiff.onThingChanged);
100+
101+
const attributesDiff = SubDiff({
102+
itemsId: 'tabItemsAttribute',
103+
contentId: 'tabContentAttribute',
104+
}, 'Compare attributes between two revisions');
105+
await attributesDiff.ready();
106+
thingsDiff.addSubDiff(attributesDiff, 'attributes');
107+
89108
const featureDescription = WoTDescription({
90109
itemsId: 'tabItemsFeatures',
91110
contentId: 'tabContentFeatures',
92111
}, true);
93112
Features.addChangeListener(featureDescription.onReferenceChanged);
94113
await featureDescription.ready();
95114

115+
const featureDiff = SubDiff({
116+
itemsId: 'tabItemsFeatures',
117+
contentId: 'tabContentFeatures',
118+
}, 'Compare selected feature between two revisions');
119+
await featureDiff.ready();
120+
thingsDiff.addSubDiff(featureDiff, 'feature');
121+
Features.addChangeListener(thingsDiff.onFeatureChanged);
122+
96123
// make dropdowns not cutting off
97124
new Dropdown(document.querySelector('.dropdown-toggle'), {
98125
popperConfig: {

ui/modules/api.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,20 @@ export function getEventSource(thingIds, urlParams) {
531531
}
532532
}
533533

534+
export function getHistoricalEventSource(thingId: string, urlParams: string) {
535+
if (authHeaderValue) {
536+
return new EventSourcePolyfill(
537+
`${Environments.current().api_uri}/api/2/things/${encodeURIComponent(thingId)}?${urlParams}`, {
538+
headers: {
539+
[authHeaderKey]: authHeaderValue,
540+
},
541+
},
542+
);
543+
} else {
544+
throw new Error("Authentication missing");
545+
}
546+
}
547+
534548
/**
535549
* Calls connections api. Uses devops api in case of Ditto and the solutions api in case of Things
536550
* @param {*} operation connections api operation

ui/modules/policies/policiesJSON.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*/
1313

1414
import * as ace from 'ace-builds/src-noconflict/ace';
15+
import { Ace } from 'ace-builds';
1516
import * as API from '../api.js';
1617
import * as Environments from '../environments/environments.js'
1718
import * as Utils from '../utils.js';
@@ -30,7 +31,7 @@ let dom: DomElements = {
3031
selectPolicyJSONTemplate: null,
3132
};
3233

33-
let policyEditor: ace.Editor;
34+
let policyEditor: Ace.Editor;
3435

3536
export function ready() {
3637

ui/modules/policies/policiesResources.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*/
1313

1414
import * as ace from 'ace-builds/src-noconflict/ace';
15+
import { Ace } from 'ace-builds';
1516
import * as API from '../api.js';
1617
import * as Utils from '../utils.js';
1718
import { CrudOperation, CrudToolbar } from '../utils/crudToolbar.js';
@@ -35,7 +36,7 @@ let dom : DomElements = {
3536
crudResource: null,
3637
} ;
3738

38-
let resourceEditor: ace.Editor;
39+
let resourceEditor: Ace.Editor;
3940

4041
export function ready() {
4142
Utils.getAllElementsById(dom);

ui/modules/policies/policiesSubjects.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*/
1313

1414
import * as ace from 'ace-builds/src-noconflict/ace';
15+
import { Ace } from 'ace-builds';
1516
import * as API from '../api.js';
1617
import * as Utils from '../utils.js';
1718
import { CrudOperation, CrudToolbar } from '../utils/crudToolbar.js';
@@ -33,7 +34,7 @@ let dom: DomElements = {
3334
crudSubject: null,
3435
} ;
3536

36-
let subjectEditor: ace.Editor;
37+
let subjectEditor: Ace.Editor;
3738

3839
export function ready() {
3940
Utils.getAllElementsById(dom);

ui/modules/things/subDiff.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!--
2+
~ Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
~
4+
~ See the NOTICE file(s) distributed with this work for additional
5+
~ information regarding copyright ownership.
6+
~
7+
~ This program and the accompanying materials are made available under the
8+
~ terms of the Eclipse Public License 2.0 which is available at
9+
~ http://www.eclipse.org/legal/epl-2.0
10+
~
11+
~ SPDX-License-Identifier: EPL-2.0
12+
-->
13+
<div class="tab-pane container no-margin" style="height: 100%; position: relative;">
14+
<div class="sub-diff-container" style="position: absolute; top: 0; bottom: 0; left: 0; right: 0;"></div>
15+
</div>

ui/modules/things/subDiff.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright (c) 2026 Contributors to the Eclipse Foundation
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
14+
import * as ace from 'ace-builds/src-noconflict/ace';
15+
import AceDiff from 'ace-diff';
16+
import * as Utils from '../utils.js';
17+
import subDiffHTML from './subDiff.html';
18+
19+
/**
20+
* Lightweight reusable diff panel without revision controls.
21+
* Shows a side-by-side ace-diff viewer in a dynamically added tab.
22+
* Content is provided externally via update().
23+
*/
24+
export function SubDiff(targetTab, title: string) {
25+
let diffInstance: AceDiff | null = null;
26+
let tabLink: HTMLAnchorElement | null = null;
27+
let container: HTMLDivElement | null = null;
28+
let pendingLeft: string | null = null;
29+
let pendingRight: string | null = null;
30+
let onActivatedCallback: ((link: HTMLAnchorElement) => void) | null = null;
31+
32+
return { ready, update, getTabLink, setOnActivated, destroy: destroyDiff };
33+
34+
async function ready() {
35+
const tabId = Utils.addTab(
36+
document.getElementById(targetTab.itemsId),
37+
document.getElementById(targetTab.contentId),
38+
'<i class="bi bi-file-diff"></i> Diff',
39+
subDiffHTML,
40+
title
41+
);
42+
43+
tabLink = document.querySelector(`a[data-bs-target="#${tabId}"]`);
44+
container = document.querySelector(`#${tabId} .sub-diff-container`);
45+
tabLink.addEventListener('shown.bs.tab', onTabActivated);
46+
}
47+
48+
function update(leftContent: string, rightContent: string) {
49+
pendingLeft = leftContent;
50+
pendingRight = rightContent;
51+
if (tabLink?.classList.contains('active')) {
52+
applyDiff();
53+
}
54+
}
55+
56+
function getTabLink(): HTMLAnchorElement | null {
57+
return tabLink;
58+
}
59+
60+
function setOnActivated(callback: (link: HTMLAnchorElement) => void) {
61+
onActivatedCallback = callback;
62+
}
63+
64+
function onTabActivated() {
65+
if (pendingLeft !== null) {
66+
applyDiff();
67+
}
68+
if (onActivatedCallback && tabLink) {
69+
onActivatedCallback(tabLink);
70+
}
71+
}
72+
73+
function applyDiff() {
74+
if (!container) return;
75+
const left = pendingLeft || '';
76+
const right = pendingRight || '';
77+
78+
if (diffInstance) {
79+
const editors = diffInstance.getEditors();
80+
editors.left.setValue(left, -1);
81+
editors.right.setValue(right, -1);
82+
diffInstance.diff();
83+
} else {
84+
diffInstance = new AceDiff({
85+
ace: ace as unknown as { edit: typeof ace.edit },
86+
element: container,
87+
mode: 'ace/mode/json',
88+
theme: null,
89+
diffGranularity: 'specific',
90+
lockScrolling: true,
91+
showDiffs: true,
92+
showConnectors: true,
93+
charDiffs: true,
94+
maxDiffs: 5000,
95+
left: {
96+
content: left,
97+
editable: false,
98+
copyLinkEnabled: false,
99+
},
100+
right: {
101+
content: right,
102+
editable: false,
103+
copyLinkEnabled: false,
104+
},
105+
});
106+
}
107+
}
108+
109+
function destroyDiff() {
110+
if (diffInstance) {
111+
diffInstance.destroy();
112+
diffInstance = null;
113+
}
114+
pendingLeft = null;
115+
pendingRight = null;
116+
if (container) {
117+
container.textContent = '';
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)