Skip to content

Commit df0f7b8

Browse files
committed
feat(clients): let connections optionally associate with a machine
Expose inbound_clients.machine_id in the Connections page (chip on cards, picker + inline create in the side panel), make machine selection skippable during OAuth consent, and fix the inline create form defaulting the icon so the save button isn't stuck disabled. Also drop the hostname autofill in that form since it misleadingly suggested the local machine's hostname for remote connections. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent 7e7e832 commit df0f7b8

6 files changed

Lines changed: 262 additions & 10 deletions

File tree

apps/desktop/src-tauri/src/commands/oauth.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ pub async fn get_oauth_clients(
483483
created_at: client.created_at,
484484
reports_roots: client.reports_roots,
485485
roots_capability_known: client.roots_capability_known,
486+
machine_id: client.machine_id.map(|id| id.to_string()),
486487
})
487488
.collect();
488489

@@ -568,6 +569,11 @@ pub struct OAuthClientInfo {
568569
/// "Reports workspace" (`reports_roots = true`) or "Rootless"
569570
/// (`reports_roots = false`).
570571
pub roots_capability_known: bool,
572+
573+
/// Machine this client is tagged with, if any (see `machines.rs`
574+
/// commands for assignment). `None` means untagged/global.
575+
#[serde(skip_serializing_if = "Option::is_none")]
576+
pub machine_id: Option<String>,
571577
}
572578

573579
/// Request to update client settings.
@@ -639,6 +645,7 @@ pub async fn update_oauth_client(
639645
created_at: updated_client.created_at,
640646
reports_roots: updated_client.reports_roots,
641647
roots_capability_known: updated_client.roots_capability_known,
648+
machine_id: updated_client.machine_id.map(|id| id.to_string()),
642649
})
643650
}
644651

apps/desktop/src-tauri/src/services/admin_server.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ impl GatewayRuntime for DesktopGatewayRuntime {
305305
"created_at": client.created_at,
306306
"reports_roots": client.reports_roots,
307307
"roots_capability_known": client.roots_capability_known,
308+
"machine_id": client.machine_id.map(|id| id.to_string()),
308309
})
309310
})
310311
.collect::<Vec<_>>();

apps/desktop/src/components/OAuthConsentModal.tsx

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -282,12 +282,14 @@ export function OAuthConsentModal() {
282282

283283
/**
284284
* Create or pick a machine, link it to the client, then approve OAuth consent.
285+
* Machine assignment is optional: leaving the picker on "No machine" (and not
286+
* mid-way through creating a new one) just approves without tagging a machine.
285287
*/
286288
const handleNameAndAllow = async () => {
287289
if (modalState.type !== 'name-machine') return;
288290
const { details, machines } = modalState;
289291

290-
let machineId = selectedMachineId;
292+
let machineId: string | null = selectedMachineId || null;
291293
if (creatingMachine) {
292294
const missingField = getMissingMachineProfileField({
293295
name: machineName,
@@ -298,10 +300,7 @@ export function OAuthConsentModal() {
298300
setProcessError(t(`oauthConsent.nameMachine.${missingField}Required`));
299301
return;
300302
}
301-
} else if (!machineId) {
302-
setProcessError(t('oauthConsent.nameMachine.nameRequired'));
303-
return;
304-
} else {
303+
} else if (machineId) {
305304
const selected = machines.find((machine) => machine.id === machineId);
306305
if (selected && !isMachineRowComplete(selected)) {
307306
setProcessError(t('oauthConsent.nameMachine.profileIncomplete'));
@@ -324,7 +323,9 @@ export function OAuthConsentModal() {
324323
machineId = created.id;
325324
}
326325

327-
await setClientMachineId(details.clientId, machineId);
326+
if (machineId) {
327+
await setClientMachineId(details.clientId, machineId);
328+
}
328329
await finishApproval(details);
329330
} catch (err) {
330331
console.error('[OAuth] Failed to name machine and approve:', err);
@@ -334,6 +335,28 @@ export function OAuthConsentModal() {
334335
}
335336
};
336337

338+
/**
339+
* Skip machine assignment entirely and approve, regardless of whatever the
340+
* picker/create sub-form currently holds. The one-tap escape hatch for
341+
* "I don't care which machine this is" or getting unstuck mid-create.
342+
*/
343+
const handleSkipMachine = async () => {
344+
if (modalState.type !== 'name-machine') return;
345+
const { details } = modalState;
346+
347+
setIsProcessing(true);
348+
setProcessError(null);
349+
350+
try {
351+
await finishApproval(details);
352+
} catch (err) {
353+
console.error('[OAuth] Failed to skip machine and approve:', err);
354+
setProcessError(String(err));
355+
} finally {
356+
setIsProcessing(false);
357+
}
358+
};
359+
337360
const handleDeny = async () => {
338361
if (modalState.type !== 'consent' && modalState.type !== 'name-machine') return;
339362
const { details } = modalState;
@@ -438,9 +461,13 @@ export function OAuthConsentModal() {
438461
const { details, machines } = modalState;
439462
const createDraft = { name: machineName, icon: machineIcon, hostname: machineHostname };
440463
const selectedMachine = machines.find((machine) => machine.id === selectedMachineId);
464+
// Machine assignment is optional — picking nothing is valid (approves
465+
// untagged). Only an in-progress "create new machine" sub-form or an
466+
// incomplete existing machine profile blocks submission.
441467
const canSubmit = creatingMachine
442468
? isMachineProfileComplete(createDraft)
443-
: selectedMachineId.length > 0 && selectedMachine != null && isMachineRowComplete(selectedMachine);
469+
: selectedMachineId.length === 0 ||
470+
(selectedMachine != null && isMachineRowComplete(selectedMachine));
444471

445472
return (
446473
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
@@ -559,6 +586,14 @@ export function OAuthConsentModal() {
559586
<X className="mr-2 h-4 w-4" />
560587
{t('oauthConsent.deny')}
561588
</Button>
589+
<button
590+
type="button"
591+
onClick={() => void handleSkipMachine()}
592+
disabled={isProcessing || !approveReady}
593+
className="text-center text-xs text-[rgb(var(--muted))] transition-colors hover:text-[rgb(var(--foreground))] disabled:opacity-50"
594+
>
595+
{t('oauthConsent.nameMachine.skipBtn')}
596+
</button>
562597
</div>
563598
</CardContent>
564599
</Card>

0 commit comments

Comments
 (0)