Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
5530be6
chore: multiple updates
joshuaunity Jan 23, 2026
2123746
chore: stabilized leeft side of modal and kicked off work onteh right…
joshuaunity Jan 26, 2026
a8d722d
chore: completed left side of graph modal
joshuaunity Jan 27, 2026
ca1e01f
Merge branch 'feat/allow-Ssensorstoshow-schema' of github.com:FlexMea…
joshuaunity Jan 27, 2026
f7966b0
Merge branch 'feat/allow-Ssensorstoshow-schema' of github.com:FlexMea…
joshuaunity Jan 30, 2026
79a1c8a
chore: work in progress
joshuaunity Feb 2, 2026
4b75e82
refactor: more stabilization work as well as reactivation of broken f…
joshuaunity Feb 2, 2026
3e03eed
Merge branch 'feat/allow-Ssensorstoshow-schema' of github.com:FlexMea…
joshuaunity Feb 2, 2026
b5f7700
Merge branch 'feat/allow-Ssensorstoshow-schema' of github.com:FlexMea…
joshuaunity Feb 3, 2026
9b08119
chore: reorder tabs
joshuaunity Feb 6, 2026
502554d
fix: fixed bug whre options keep gettgin added teh configType dropdow…
joshuaunity Feb 9, 2026
ce0a984
fix: fixed error where graph cant be removed
joshuaunity Feb 9, 2026
d6d75d8
fix: ixed issue with graph titles not being editable
joshuaunity Feb 9, 2026
b5b9876
fix: fixed broken units dropdown as well as some other refactoring to…
joshuaunity Feb 11, 2026
f878460
refactor: Major refactor phase 1
joshuaunity Feb 16, 2026
6ff2570
refactor: Major refactor phase 2
joshuaunity Feb 19, 2026
f9823c0
feat; new util function to find an asset site_asset
joshuaunity Feb 20, 2026
a96ca2b
chore: add extra info icon to form elements
joshuaunity Feb 20, 2026
9419875
chore: update writeup
joshuaunity Feb 20, 2026
66fe145
fix: fix failing util function due to wrong formatting allowing trail…
joshuaunity Feb 23, 2026
5eca7e8
Merge branch 'feat/allow-Ssensorstoshow-schema' into feat/sensortosho…
joshuaunity Feb 24, 2026
88b0449
fix: fixed bug where a new sensor cant be added to an existing graph
joshuaunity Feb 24, 2026
5552322
refactor: Fixed graphs to properly display subcharts and mixed charts…
joshuaunity Feb 26, 2026
25326dd
chore: removed unused code
joshuaunity Feb 26, 2026
1c4635d
Merge branch 'feat/allow-Ssensorstoshow-schema' into feat/sensortosho…
joshuaunity Feb 26, 2026
9582d27
refactor: post resolving conflicts refactoring
joshuaunity Feb 26, 2026
63b4539
tests: remove unsupported edgecase
joshuaunity Feb 26, 2026
cafc05b
fix: fix issue where unsuppoerted flexocntext fields are sent to API,…
joshuaunity Mar 2, 2026
c77f718
feat: up to date asset data for graph modals
joshuaunity Mar 2, 2026
b437123
fix: Fix bug where you cant remove sensors from a grpah with multiple…
joshuaunity Mar 3, 2026
5ee01b4
chore: amek all asset ref inputs equal size
joshuaunity Mar 3, 2026
2850567
Update flexmeasures/ui/templates/assets/asset_graph.html
joshuaunity Mar 3, 2026
eb47a6f
Update flexmeasures/ui/templates/assets/asset_graph.html
joshuaunity Mar 3, 2026
d02c738
fix: fixed error when trying to remove polts thats not a array of sen…
joshuaunity Mar 3, 2026
5a79e24
fix: Fixed bug where the options to select a felxConfig field disable…
joshuaunity Mar 3, 2026
d63a6aa
refactor: imporve fuction to improve readability and also fix edgecas…
joshuaunity Mar 3, 2026
c791f5a
fix: Fixed bug where sensors filter sectoin doesnt change after a sel…
joshuaunity Mar 3, 2026
d83587c
chore: udpate temp sensor naming sceme
joshuaunity Mar 5, 2026
69a1c9a
chore: persisten root asset selection
joshuaunity Mar 5, 2026
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
203 changes: 203 additions & 0 deletions flexmeasures/ui/static/js/components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { getAsset, getAccount, getSensor, apiBasePath } from "./ui-utils.js";

const addInfo = (label, value, infoDiv, Resource, isLink = false) => {
const b = document.createElement("b");
b.textContent = `${label}: `;
infoDiv.appendChild(b);
const isSensor = Resource.hasOwnProperty("unit");

if (isLink) {
const a = document.createElement("a");
a.href = `${apiBasePath}/${isSensor ? "sensors" : "assets"}/${Resource.id}`;
a.textContent = value;
infoDiv.appendChild(a);
} else {
infoDiv.appendChild(document.createTextNode(value));
}
};

export async function renderAssetPlotCard(assetPlot, graphIndex, plotIndex) {
const Asset = await getAsset(assetPlot.asset);
let IsFlexContext = false;
let IsFlexModel = false;
let flexConfigValue = null;

if ("flex-context" in assetPlot) {
IsFlexContext = true;
flexConfigValue = assetPlot["flex-context"];
}

if ("flex-model" in assetPlot) {
IsFlexModel = true;
flexConfigValue = assetPlot["flex-model"];
}

const container = document.createElement("div");
container.className = "p-1 mb-3 border-bottom border-secondary";

const flexDiv = document.createElement("div");
flexDiv.className = "d-flex justify-content-between";

const infoDiv = document.createElement("div");

addInfo("ID", Asset.id, infoDiv, Asset, true);
infoDiv.appendChild(document.createTextNode(", "));
addInfo("Name", Asset.name, infoDiv, Asset);
infoDiv.appendChild(document.createTextNode(", "));
if (IsFlexContext) {
addInfo("Flex Context", flexConfigValue, infoDiv, Asset);
} else if (IsFlexModel) {
addInfo("Flex Model", flexConfigValue, infoDiv, Asset);
}

const closeIcon = document.createElement("i");
closeIcon.className = "fa fa-times";
closeIcon.style.cursor = "pointer";
closeIcon.setAttribute("data-bs-toggle", "tooltip");
closeIcon.title = "Remove Asset Plot";

// Attach the actual function here
closeIcon.addEventListener("click", (e) => {
e.stopPropagation(); // Prevent card selection click
// removeAssetPlotFromGraph(graphIndex, plotIndex);
});

flexDiv.appendChild(infoDiv);
flexDiv.appendChild(closeIcon);
container.appendChild(flexDiv);

return container;
}

export async function renderSensorCard(sensorId, graphIndex, sensorIndex) {
const Sensor = await getSensor(sensorId);
const Asset = await getAsset(Sensor.generic_asset_id);
const Account = await getAccount(Asset.account_id);

const container = document.createElement("div");
container.className = "p-1 mb-3 border-bottom border-secondary";

const flexDiv = document.createElement("div");
flexDiv.className = "d-flex justify-content-between";

const infoDiv = document.createElement("div");

addInfo("ID", Sensor.id, infoDiv, Sensor, true);
infoDiv.appendChild(document.createTextNode(", "));
addInfo("Unit", Sensor.unit, infoDiv, Sensor);
infoDiv.appendChild(document.createTextNode(", "));
addInfo("Name", Sensor.name, infoDiv, Sensor);

const spacer = document.createElement("div");
spacer.style.paddingTop = "1px";
infoDiv.appendChild(spacer);

addInfo("Asset", Asset.name, infoDiv, Asset);
infoDiv.appendChild(document.createTextNode(", "));
addInfo("Account", Account?.name ? Account.name : "PUBLIC", infoDiv, Account);

const closeIcon = document.createElement("i");
closeIcon.className = "fa fa-times";
closeIcon.style.cursor = "pointer";
closeIcon.setAttribute("data-bs-toggle", "tooltip");
closeIcon.title = "Remove Sensor";

// Attach the actual function here
closeIcon.addEventListener("click", (e) => {
e.stopPropagation(); // Prevent card selection click
removeSensorFromGraph(graphIndex, sensorIndex);
});

flexDiv.appendChild(infoDiv);
flexDiv.appendChild(closeIcon);
container.appendChild(flexDiv);

// Return both the element and the unit (so we can check for mixed units later)
return { element: container, unit: Sensor.unit };
}

export async function renderSensorsList(sensorIds, graphIndex) {
const listContainer = document.createElement("div");
const units = [];

if (sensorIds.length === 0) {
listContainer.innerHTML = `<div class="alert alert-warning">No sensors added to this graph.</div>`;
return { element: listContainer, uniqueUnits: [] };
}

// Using Promise.all to maintain order and wait for all sensors
const results = await Promise.all(
sensorIds.map((id, sIdx) => renderSensorCard(id, graphIndex, sIdx)),
);

results.forEach((res) => {
listContainer.appendChild(res.element);
units.push(res.unit);
});

return { element: listContainer, uniqueUnits: [...new Set(units)] };
}

/**
* Renders the header for a graph card.
* @param {string} title - The current title of the graph.
* @param {number} index - The index of the graph in the list.
* @param {boolean} isEditing - Whether this specific graph is in edit mode.
* @param {Function} onSave - Function to call when "Save" is clicked or "Enter" is pressed.
* @param {Function} onEdit - Function to call when "Edit" is clicked.
*/
export function renderGraphHeader(title, index, isEditing, onSave, onEdit) {
const header = document.createElement("div");
header.className = "d-flex align-items-center mb-2";

if (isEditing) {
// 1. Title Input
const input = document.createElement("input");
input.type = "text";
input.className = "form-control me-2";
input.id = `editTitle_${index}`;
input.value = title;

// Save on Enter key
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
onSave(index);
}
});

// 2. Save Button
const saveBtn = document.createElement("button");
saveBtn.className = "btn btn-success btn-sm";
saveBtn.textContent = "Save";
saveBtn.onclick = (e) => {
e.stopPropagation(); // Prevent card selection
onSave(index);
};

header.appendChild(input);
header.appendChild(saveBtn);

// Auto-focus the input
setTimeout(() => input.focus(), 0);
} else {
// 1. Display Title
const h5 = document.createElement("h5");
h5.className = "card-title me-2 mb-0";
h5.textContent = title;

// 2. Edit Button
const editBtn = document.createElement("button");
editBtn.className = "btn btn-warning btn-sm";
editBtn.textContent = "Edit";
editBtn.onclick = (e) => {
e.stopPropagation(); // Prevent card selection
onEdit(index);
};

header.appendChild(h5);
header.appendChild(editBtn);
}

return header;
}
47 changes: 38 additions & 9 deletions flexmeasures/ui/static/js/ui-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ export function processResourceRawJSON(schema, rawJSON, allowExtra = false) {
}

export function getFlexFieldTitle(fieldName) {
return fieldName
// .split("-")
// .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
// .join(" ");
return fieldName;
// .split("-")
// .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
// .join(" ");
}

export function renderFlexFieldOptions(schema, options) {
Expand Down Expand Up @@ -118,8 +118,8 @@ export async function renderSensor(sensorId) {
<div class="d-flex justify-content-between">
<div>
<b>Sensor:</b> <a href="${apiBasePath}/sensors/${
sensorData.id
}">${sensorData.id}</a>,
sensorData.id
}">${sensorData.id}</a>,
<b>Unit:</b> ${
sensorData.unit === ""
? '<span title="A sensor recording numbers rather than physical or economical quantities.">dimensionless</span>'
Expand Down Expand Up @@ -175,7 +175,7 @@ export function createReactiveState(initialValue, renderFunction) {
export function renderSensorSearchResults(
sensors,
resultContainer,
actionFunc
actionFunc,
) {
if (!resultContainer) {
console.error("Result container is not defined.");
Expand Down Expand Up @@ -208,8 +208,8 @@ export function renderSensorSearchResults(
<h5 class="card-title">${sensor.name}</h5>
<p class="card-text">
<b>ID:</b> <a href="${apiBasePath}/sensors/${
sensor.id
}">${sensor.id}</a>,
sensor.id
}">${sensor.id}</a>,
<b>Unit:</b> ${
sensor.unit === ""
? '<span title="A sensor recording numbers rather than physical or economical quantities.">dimensionless</span>'
Expand Down Expand Up @@ -295,3 +295,32 @@ export function setDefaultLegendPosition(checkbox) {
console.error("Error during API call:", error);
});
}

/**
* Swaps an item in an array with its neighbor based on direction.
* @param {Array} array - The source array.
* @param {number} index - The index of the item to move.
* @param {'up' | 'down'} direction - The direction to move.
* @returns {Array} A new array with the items swapped.
*/
export function moveArrayItem(array, index, direction) {
// Create a shallow copy to avoid mutating the original array
const newArray = [...array];

const isUp = direction === "up";
const targetIndex = isUp ? index - 1 : index + 1;

// Boundary Checks:
// Don't move 'up' if at the start, or 'down' if at the end.
if (targetIndex < 0 || targetIndex >= newArray.length) {
return newArray;
}

// Perform the swap using destructuring
[newArray[index], newArray[targetIndex]] = [
newArray[targetIndex],
newArray[index],
];

return newArray;
}
2 changes: 1 addition & 1 deletion flexmeasures/ui/templates/assets/asset_context.html
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ <h5 class="modal-title pe-2">Edit {{ asset.name }}'s flex-context</h5>
const card = document.createElement("div");
card.className = `card m-1 p-2 card-highlight ${isActiveCard() ? "border-on-click" : ""}`;
card.id = `${fieldName}-control`;
// set card element to disabled if it's an extrafield
// set card element to disabled if it's an extra field
if (isExtraField) {
card.setAttribute("data-disabled", "true");
card.style.pointerEvents = "none";
Expand Down
Loading