mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-18 09:05:24 -04:00
feat(tailscale): add connect/disconnect, exit-node and LAN-access controls (#2644)
* feat(tailscale): add connect/disconnect/exit-node/LAN-access backend The Tailscale backend previously exposed only read-only status (tailscale.getStatus, tailscale.refresh). This adds write actions through the existing tailscale.com/client/local integration: - tailscale.connect / tailscale.disconnect (EditPrefs WantRunning) - tailscale.setExitNode (EditPrefs ExitNodeID; empty id clears it and any legacy ExitNodeIP, mirroring `tailscale set --exit-node`) - tailscale.setAllowLanAccess (EditPrefs ExitNodeAllowLANAccess) The manager's client interface gains GetPrefs/EditPrefs; fetchState merges ExitNodeAllowLANAccess from prefs, and Peer exposes ExitNodeOption so the UI can list exit-node-capable peers. * feat(tailscale): expose the new actions in TailscaleService Adds connectTailscale/disconnectTailscale, setExitNode/clearExitNode and setAllowLanAccess wrappers, plus derived exitNodeOptions/currentExitNode and the exitNodeAllowLanAccess state. Write-action errors surface via ToastService. * feat(tailscale): add connection, exit-node and LAN-access controls to the widget The control-center widget toggle was a no-op. It now connects/disconnects, and the detail panel gains a connection status row with a connect/disconnect button, an exit-node picker and a LAN-access toggle.
This commit is contained in:
@@ -25,7 +25,14 @@ PluginComponent {
|
||||
}
|
||||
ccWidgetIsActive: TailscaleService.connected
|
||||
|
||||
onCcWidgetToggled: {}
|
||||
onCcWidgetToggled: {
|
||||
if (!TailscaleService.available)
|
||||
return;
|
||||
if (TailscaleService.connected)
|
||||
TailscaleService.disconnectTailscale(null);
|
||||
else
|
||||
TailscaleService.connectTailscale(null);
|
||||
}
|
||||
|
||||
ccDetailContent: Component {
|
||||
Rectangle {
|
||||
@@ -88,6 +95,122 @@ PluginComponent {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Connection status + connect/disconnect. Always shown
|
||||
// (when available) so the connection can be toggled from
|
||||
// the detail, including while disconnected.
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: 1
|
||||
|
||||
StyledText {
|
||||
text: TailscaleService.connected ? I18n.tr("Connected", "Tailscale connection status: connected") : I18n.tr("Disconnected", "Tailscale connection status: disconnected")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: TailscaleService.connected && TailscaleService.tailnetName.length > 0
|
||||
text: TailscaleService.tailnetName
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: connButton
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
height: 28
|
||||
radius: 14
|
||||
width: connButtonRow.implicitWidth + Theme.spacingM * 2
|
||||
|
||||
readonly property bool isConnected: TailscaleService.connected
|
||||
color: isConnected ? (connButtonArea.containsMouse ? Theme.errorHover : Theme.surfaceLight) : (connButtonArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight)
|
||||
|
||||
Row {
|
||||
id: connButtonRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: connButton.isConnected ? "link_off" : "link"
|
||||
size: Theme.fontSizeSmall
|
||||
color: connButton.isConnected ? Theme.surfaceText : Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: connButton.isConnected ? I18n.tr("Disconnect", "Tailscale disconnect button") : I18n.tr("Connect", "Tailscale connect button")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: connButton.isConnected ? Theme.surfaceText : Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (TailscaleService.connected)
|
||||
TailscaleService.disconnectTailscale(null);
|
||||
else
|
||||
TailscaleService.connectTailscale(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connection controls: exit node picker + LAN access.
|
||||
// Only meaningful while the backend is connected.
|
||||
Column {
|
||||
id: controlsColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: TailscaleService.connected
|
||||
|
||||
readonly property string noneLabel: I18n.tr("None", "Tailscale exit node: none selected")
|
||||
|
||||
DankDropdown {
|
||||
width: parent.width
|
||||
text: I18n.tr("Exit node", "Tailscale exit node selector label")
|
||||
currentValue: TailscaleService.currentExitNode ? TailscaleService.currentExitNode.hostname : controlsColumn.noneLabel
|
||||
options: {
|
||||
const opts = [controlsColumn.noneLabel];
|
||||
for (const p of TailscaleService.exitNodeOptions)
|
||||
opts.push(p.hostname);
|
||||
return opts;
|
||||
}
|
||||
onValueChanged: value => {
|
||||
if (value === controlsColumn.noneLabel) {
|
||||
TailscaleService.clearExitNode(null);
|
||||
return;
|
||||
}
|
||||
const peer = TailscaleService.exitNodeOptions.find(p => p.hostname === value);
|
||||
if (peer)
|
||||
TailscaleService.setExitNode(peer.id, null);
|
||||
}
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Allow LAN access", "Tailscale allow LAN access toggle")
|
||||
description: I18n.tr("Reach local network devices while using an exit node", "Tailscale allow LAN access description")
|
||||
visible: TailscaleService.currentExitNode !== null
|
||||
checked: TailscaleService.exitNodeAllowLanAccess
|
||||
onToggled: value => TailscaleService.setAllowLanAccess(value, null)
|
||||
}
|
||||
}
|
||||
|
||||
// Search bar + refresh button
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
|
||||
@@ -41,6 +41,7 @@ Singleton {
|
||||
property string tailnetName: ""
|
||||
property var selfNode: null
|
||||
property var peers: []
|
||||
property bool exitNodeAllowLanAccess: false
|
||||
|
||||
property bool available: false
|
||||
property bool stateInitialized: false
|
||||
@@ -56,6 +57,19 @@ Singleton {
|
||||
|
||||
readonly property var onlinePeers: allPeersList.filter(p => p.online)
|
||||
|
||||
// Peers that may be used as an exit node (offered && approved). Self is
|
||||
// excluded: a node can never route through itself, and tailscaled rejects it.
|
||||
readonly property var exitNodeOptions: allPeersList.filter(p => p && p.exitNodeOption && p !== selfNode)
|
||||
|
||||
// The currently selected exit node, or null if none is in use.
|
||||
readonly property var currentExitNode: {
|
||||
for (const p of allPeersList) {
|
||||
if (p && p.exitNode)
|
||||
return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
readonly property var myPeers: {
|
||||
if (!selfNode)
|
||||
return allPeersList;
|
||||
@@ -141,6 +155,7 @@ Singleton {
|
||||
tailnetName = data.tailnetName || "";
|
||||
selfNode = data.self || null;
|
||||
peers = data.peers || [];
|
||||
exitNodeAllowLanAccess = data.exitNodeAllowLanAccess || false;
|
||||
}
|
||||
|
||||
function refresh(callback) {
|
||||
@@ -152,6 +167,45 @@ Singleton {
|
||||
});
|
||||
}
|
||||
|
||||
// sendAction issues a state-changing request. The backend refreshes and
|
||||
// broadcasts on success, so subscribers update without an extra getStatus.
|
||||
function sendAction(method, params, callback) {
|
||||
if (!available)
|
||||
return;
|
||||
DMSService.sendRequest(method, params, response => {
|
||||
if (response.error) {
|
||||
root.log.warn(method + " failed: " + response.error);
|
||||
ToastService.showError(I18n.tr("Tailscale action failed", "Toast shown when a Tailscale write action is rejected"), response.error);
|
||||
}
|
||||
if (callback)
|
||||
callback(response);
|
||||
});
|
||||
}
|
||||
|
||||
function connectTailscale(callback) {
|
||||
sendAction("tailscale.connect", null, callback);
|
||||
}
|
||||
|
||||
function disconnectTailscale(callback) {
|
||||
sendAction("tailscale.disconnect", null, callback);
|
||||
}
|
||||
|
||||
function setExitNode(id, callback) {
|
||||
sendAction("tailscale.setExitNode", {
|
||||
"id": id || ""
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function clearExitNode(callback) {
|
||||
setExitNode("", callback);
|
||||
}
|
||||
|
||||
function setAllowLanAccess(enabled, callback) {
|
||||
sendAction("tailscale.setAllowLanAccess", {
|
||||
"enabled": enabled
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function isMine(peer) {
|
||||
const myOwner = selfNode ? (selfNode.owner || "") : "";
|
||||
if (peer.owner === myOwner && myOwner !== "")
|
||||
|
||||
Reference in New Issue
Block a user