Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c56544e
dashboard/widgets: Match port picker choices against one ordered phrase
ccrisan Sep 13, 2026
451fa03
frontend/dashboard: Contain layout and style within a widget container
ccrisan Sep 13, 2026
59bcf3a
frontend: Depend on the modular jquery-ui package
ccrisan Sep 13, 2026
8f90db7
frontend: Update qui to 1.20.0-alpha.2
ccrisan Sep 13, 2026
0e7c9d4
Update qui-server together with @qtoggle/qui
ccrisan Sep 13, 2026
28f1c50
Update qui to 1.20.0-alpha.2
ccrisan Sep 13, 2026
6cb5cd0
Update Dockerfile
ccrisan Sep 13, 2026
c3dba40
Update qui to 1.20.0-alpha.3
ccrisan Sep 13, 2026
1f55e90
frontend: Add the Babel runtime helper packages
ccrisan Sep 13, 2026
73958b9
Update qui to 1.20.0-alpha.4
ccrisan Sep 13, 2026
89a6df2
frontend: Bound how long list updates may be postponed
ccrisan Sep 14, 2026
ab42d90
frontend: Wrap the debouncer constructor calls to 120 columns
ccrisan Sep 14, 2026
4b199bb
frontend/cache: Look up a port's slave device by prefix
ccrisan Sep 14, 2026
7d5e3fb
frontend/events: Group port events in one pass over the bulk
ccrisan Sep 15, 2026
68dda75
Update qui to 1.20.0-alpha.6
ccrisan Sep 15, 2026
4fd3ca7
Update qui to 1.20.0-alpha.7
ccrisan Sep 15, 2026
7149a19
frontend/settings: Skip updating a closed settings form
ccrisan Sep 25, 2026
fbb0ba1
Update qui to 1.20.0-beta.1
ccrisan Sep 25, 2026
b24cb4c
Update qui to 1.20.0-beta.2
ccrisan Sep 25, 2026
03098c3
Update qui to 1.20.0
ccrisan Sep 25, 2026
ef00ec7
Update qui to 1.20.1
ccrisan Sep 27, 2026
e96ec4d
frontend/utils: Build the digits regex once
ccrisan Sep 28, 2026
2da001b
frontend/ports: Update port flags on in-place list updates (#225)
ccrisan Sep 28, 2026
9359e1e
frontend/ports: Build the port flags with raw DOM (#226)
ccrisan Sep 28, 2026
bd4985c
Update qui to 1.20.2
ccrisan Oct 2, 2026
051dd90
Update qui to 1.20.3
ccrisan Oct 5, 2026
a146b50
Fix off pointer coordinates when using scaling factor
ccrisan Oct 5, 2026
0e5f124
frontend/cache: Return null when no slave device owns a port
ccrisan Oct 6, 2026
28a712e
Correct what normalizes the qui prerelease version
ccrisan Oct 6, 2026
31aeb3d
Fix slave ports stuck offline (#227)
ccrisan Oct 6, 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
1 change: 1 addition & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Build with:
# docker build -f docker/Dockerfile -t qtoggle/qtoggleserver .
#
# Run with:
Expand Down
7 changes: 5 additions & 2 deletions qtoggleserver/core/ports.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,8 @@ async def enable(self) -> None:

self.debug("enabling")
self._enabled = True
self.invalidate_attr("enabled")
# Invalidate all attributes, not just `enabled`: others may be derived from it, e.g. `online`
self.invalidate_attrs()

# Reset port expression
if self._expression:
Expand All @@ -499,6 +500,7 @@ async def enable(self) -> None:
except Exception:
self.error("failed to enable")
self._enabled = False
self.invalidate_attrs()

raise

Expand All @@ -514,13 +516,14 @@ async def disable(self) -> None:

self.debug("disabling")
self._enabled = False
self.invalidate_attr("enabled")
self.invalidate_attrs()

try:
await self.handle_disable()
except Exception:
self.error("failed to disable")
self._enabled = True
self.invalidate_attrs()

raise

Expand Down
21 changes: 18 additions & 3 deletions qtoggleserver/frontend/js/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -706,9 +706,24 @@ export function findPortSlaveDevice(portId) {
return null
}

return ObjectUtils.findValue(slaveDevices, function (deviceName) {
return portId.startsWith(`${deviceName}.`)
})
/* A slave port id is the device name, a dot, then the port id, so walking the dot-separated prefixes and looking
Comment thread
ccrisan marked this conversation as resolved.
Resolved
* each one up costs a couple of lookups however many devices there are. Testing startsWith() against every device
* instead was proportional to their number, and this is called from inside a per-event rescan of the whole event
* bulk: with 20 devices, the 40000 lookups a 200-event bulk needs took 123ms, against 2.4ms this way.
*
* A device name can never contain a dot (see the `name` pattern in api/attrdefs.js), so only the prefix before the
* first dot can ever match; the loop then just falls through for a local port id that contains dots. */
let dot = portId.indexOf('.')
while (dot >= 0) {
let device = slaveDevices[portId.substring(0, dot)]
if (device) {
return device
}

dot = portId.indexOf('.', dot + 1)
}

return null
}

/**
Expand Down
9 changes: 9 additions & 0 deletions qtoggleserver/frontend/js/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@
* @type {Number}
*/
export const COMMON_DEBOUNCE_DELAY = 1000

/**
* The longest a debounced UI update may be postponed, however long the stream of events driving it lasts. Without a
* bound, each event pushes the update back by the full delay, so a device that reports faster than once a second holds
* the list at its old contents for as long as it keeps talking -- which reads as a frozen UI rather than a slow one.
* @alias qtoggle.constants.COMMON_DEBOUNCE_MAX_WAIT
* @type {Number}
*/
export const COMMON_DEBOUNCE_MAX_WAIT = 3000
47 changes: 38 additions & 9 deletions qtoggleserver/frontend/js/dashboard/widgets/port-picker-field.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class PortPickerField extends ComboField {
let choices = ports.map(function (port) {
return {
label: this.makeLabel(port),
matchPhrase: this.makeMatchPhrase(port),
value: port.id,
port: port
}
Expand All @@ -71,19 +72,47 @@ class PortPickerField extends ComboField {
return choices
}

filterFunc(choice, searchText) {
let port = choice.port
/**
* Build the text a search filter is matched against. The parts of the port id appear in the order in which they
* are displayed, each one as it is displayed and, when that is a display name, followed by the corresponding id
* part, so that a filter can span them and either name can be searched for.
* @param {Object} port
* @returns {String}
*/
makeMatchPhrase(port) {
let device = Cache.findPortSlaveDevice(port.id)
let phrase = port.id.split('.')
let mainDevice = Cache.getMainDevice()

if (port.display_name) {
phrase.push(port.display_name)
}
if (device && device.attrs['display_name']) {
phrase.push(device.attrs['display_name'])
let parts = port.id.split('.')
if (parts.length === 1 && Config.slavesEnabled) {
parts.splice(0, 0, mainDevice.display_name || mainDevice.name)
}

return phrase.some(p => StringUtils.intelliSearch(p, searchText) != null)
let phraseParts = []
parts.forEach(function (part, i) {

let displayName = null
if (i === 0 && device && device.attrs['display_name']) {
displayName = device.attrs['display_name']
}
else if (i === parts.length - 1 && port.display_name) {
displayName = port.display_name
}

if (displayName) {
phraseParts.push(displayName)
}
if (displayName !== part) {
phraseParts.push(part)
}

})

return phraseParts.join(' ').toLowerCase()
}

filterFunc(choice, searchText) {
return StringUtils.intelliSearch(choice.matchPhrase, searchText) != null
}

makeLabelHTML() {
Expand Down
21 changes: 14 additions & 7 deletions qtoggleserver/frontend/js/dashboard/widgets/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ class Widget extends mix().with(ViewMixin) {

function handleMove(elemX, elemY) {
/* Make the new offsets relative to the panel body */
let panelBodyOffset = widget._getPanelBody().offset()
let panelBodyOffset = widget._getPanelBodyOffset()
let newOffsetLeft = elemX - panelBodyOffset.left
let newOffsetTop = elemY - panelBodyOffset.top

Expand Down Expand Up @@ -381,7 +381,7 @@ class Widget extends mix().with(ViewMixin) {
let newOffsetTop = pageY

/* Make the new offset relative to the panel body */
let panelBodyOffset = widget._getPanelBody().offset()
let panelBodyOffset = widget._getPanelBodyOffset()
newOffsetTop -= panelBodyOffset.top

/* Compute the new offset in em units */
Expand Down Expand Up @@ -430,7 +430,7 @@ class Widget extends mix().with(ViewMixin) {
let newOffsetRight = pageX

/* Make the new offset relative to the panel body */
let panelBodyOffset = widget._getPanelBody().offset()
let panelBodyOffset = widget._getPanelBodyOffset()
newOffsetRight -= panelBodyOffset.left

/* Compute the new offset in em units */
Expand Down Expand Up @@ -483,7 +483,7 @@ class Widget extends mix().with(ViewMixin) {
let newOffsetBottom = pageY

/* Make the new offset relative to the panel body */
let panelBodyOffset = widget._getPanelBody().offset()
let panelBodyOffset = widget._getPanelBodyOffset()
newOffsetBottom -= panelBodyOffset.top

/* Compute the new offset in em units */
Expand Down Expand Up @@ -536,7 +536,7 @@ class Widget extends mix().with(ViewMixin) {
let newOffsetLeft = pageX

/* Make the new offset relative to the panel body */
let panelBodyOffset = widget._getPanelBody().offset()
let panelBodyOffset = widget._getPanelBodyOffset()
newOffsetLeft -= panelBodyOffset.left

/* Compute the new offset in em units */
Expand Down Expand Up @@ -581,6 +581,13 @@ class Widget extends mix().with(ViewMixin) {
return this._panel.getBody()
}

/* Panel body offset, in CSS pixels, so that it can be compared with dragging coordinates and cell widths */
_getPanelBodyOffset() {
let offset = this._getPanelBody().offset()

return {left: CSS.unscalePx(offset.left), top: CSS.unscalePx(offset.top)}
}

/**
* Return the current cell width in pixels.
* @returns {Number}
Expand Down Expand Up @@ -1008,8 +1015,8 @@ class Widget extends mix().with(ViewMixin) {
let elemPosition = this._dragElem.position()

this._px2em = CSS.px2em(1, this._bodyDiv)
this._clickDragElemX = elemPosition.left
this._clickDragElemY = elemPosition.top
this._clickDragElemX = CSS.unscalePx(elemPosition.left)
this._clickDragElemY = CSS.unscalePx(elemPosition.top)

this.onDragBegin()

Expand Down
4 changes: 3 additions & 1 deletion qtoggleserver/frontend/js/devices/devices-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ class DevicesTable extends PageTable {
})

this.deviceForm = null
this._updateUIDebouncer = new Debouncer(() => this.updateUI(this), Constants.COMMON_DEBOUNCE_DELAY)
this._updateUIDebouncer = new Debouncer(
() => this.updateUI(this), Constants.COMMON_DEBOUNCE_DELAY, Constants.COMMON_DEBOUNCE_MAX_WAIT
)
}

init() {
Expand Down
Loading
Loading