Skip to content

Commit 2024619

Browse files
suryaiyer95claude
andcommitted
feat: single-page login form with all 3 fields and tab navigation
Replace multi-step wizard with a single form showing Instance Name, API Key, and URL fields together. Tab cycles between fields, Enter submits. Validates all fields and tests credentials before saving. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 633c98a commit 2024619

1 file changed

Lines changed: 129 additions & 54 deletions

File tree

Lines changed: 129 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { createSignal, Show } from "solid-js"
1+
import { createSignal, Show, onMount } from "solid-js"
2+
import { createStore } from "solid-js/store"
3+
import { TextareaRenderable, TextAttributes } from "@opentui/core"
4+
import { useKeyboard } from "@opentui/solid"
25
import { useDialog } from "@tui/ui/dialog"
36
import { useSDK } from "../context/sdk"
47
import { useSync } from "@tui/context/sync"
58
import { useLocal } from "@tui/context/local"
6-
import { DialogPrompt } from "../ui/dialog-prompt"
79
import { useTheme } from "../context/theme"
810
import { AltimateApi } from "@/altimate/api/client"
911
import { Filesystem } from "@/util/filesystem"
@@ -14,21 +16,75 @@ export function DialogAltimateLogin() {
1416
const sync = useSync()
1517
const local = useLocal()
1618
const { theme } = useTheme()
17-
const [step, setStep] = createSignal<"instance" | "key" | "url">("instance")
18-
const [instanceName, setInstanceName] = createSignal("")
19-
const [apiKey, setApiKey] = createSignal("")
2019
const [error, setError] = createSignal("")
2120
const [validating, setValidating] = createSignal(false)
21+
const [store, setStore] = createStore({
22+
active: "instance" as "instance" | "key" | "url",
23+
})
24+
25+
let instanceRef: TextareaRenderable
26+
let keyRef: TextareaRenderable
27+
let urlRef: TextareaRenderable
28+
29+
const fields = ["instance", "key", "url"] as const
30+
31+
function focusActive() {
32+
setTimeout(() => {
33+
const ref = { instance: instanceRef, key: keyRef, url: urlRef }[store.active]
34+
if (ref && !ref.isDestroyed) ref.focus()
35+
}, 1)
36+
}
37+
38+
useKeyboard((evt) => {
39+
if (evt.name === "tab") {
40+
const idx = fields.indexOf(store.active)
41+
const next = fields[(idx + 1) % fields.length]
42+
setStore("active", next)
43+
focusActive()
44+
evt.preventDefault()
45+
}
46+
if (evt.name === "return") {
47+
submit()
48+
}
49+
})
50+
51+
onMount(() => {
52+
dialog.setSize("medium")
53+
focusActive()
54+
})
55+
56+
async function submit() {
57+
const instance = instanceRef.plainText
58+
const key = keyRef.plainText
59+
const url = urlRef.plainText
60+
61+
if (!instance) {
62+
setError("Instance name is required")
63+
setStore("active", "instance")
64+
focusActive()
65+
return
66+
}
67+
if (!key) {
68+
setError("API key is required")
69+
setStore("active", "key")
70+
focusActive()
71+
return
72+
}
73+
if (!url) {
74+
setError("URL is required")
75+
setStore("active", "url")
76+
focusActive()
77+
return
78+
}
2279

23-
async function validateAndSave(url: string) {
2480
setError("")
2581
setValidating(true)
2682
try {
2783
const res = await fetch(`${url}/auth_health`, {
2884
method: "GET",
2985
headers: {
30-
Authorization: `Bearer ${apiKey()}`,
31-
"x-tenant": instanceName(),
86+
Authorization: `Bearer ${key}`,
87+
"x-tenant": instance,
3288
},
3389
})
3490
if (!res.ok) {
@@ -42,7 +98,7 @@ export function DialogAltimateLogin() {
4298
setValidating(false)
4399
return
44100
}
45-
} catch (e) {
101+
} catch {
46102
setError(`Connection failed — could not reach ${url}`)
47103
setValidating(false)
48104
return
@@ -51,8 +107,8 @@ export function DialogAltimateLogin() {
51107

52108
const creds = {
53109
altimateUrl: url,
54-
altimateInstanceName: instanceName(),
55-
altimateApiKey: apiKey(),
110+
altimateInstanceName: instance,
111+
altimateApiKey: key,
56112
}
57113
await Filesystem.writeJson(AltimateApi.credentialsPath(), creds, 0o600)
58114
await sdk.client.instance.dispose()
@@ -62,57 +118,76 @@ export function DialogAltimateLogin() {
62118
}
63119

64120
return (
65-
<>
66-
{step() === "instance" && (
67-
<DialogPrompt
68-
title="Instance Name"
121+
<box paddingLeft={2} paddingRight={2} gap={1}>
122+
<box flexDirection="row" justifyContent="space-between">
123+
<text attributes={TextAttributes.BOLD} fg={theme.text}>
124+
Login to Altimate
125+
</text>
126+
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
127+
esc
128+
</text>
129+
</box>
130+
131+
<box>
132+
<text fg={store.active === "instance" ? theme.text : theme.textMuted}>Instance Name:</text>
133+
<textarea
134+
height={3}
135+
ref={(val: TextareaRenderable) => (instanceRef = val)}
69136
placeholder="your-instance"
70-
description={() => (
71-
<text fg={theme.textMuted}>Enter your Altimate instance (tenant) name</text>
72-
)}
73-
onConfirm={(value) => {
74-
if (!value) return
75-
setInstanceName(value)
76-
setStep("key")
137+
textColor={theme.text}
138+
focusedTextColor={theme.text}
139+
cursorColor={theme.text}
140+
onMouseUp={() => {
141+
setStore("active", "instance")
142+
focusActive()
77143
}}
78144
/>
79-
)}
80-
{step() === "key" && (
81-
<DialogPrompt
82-
title="API Key"
145+
</box>
146+
147+
<box>
148+
<text fg={store.active === "key" ? theme.text : theme.textMuted}>API Key:</text>
149+
<textarea
150+
height={3}
151+
ref={(val: TextareaRenderable) => (keyRef = val)}
83152
placeholder="your-api-key"
84-
description={() => (
85-
<text fg={theme.textMuted}>Enter your Altimate API key</text>
86-
)}
87-
onConfirm={(value) => {
88-
if (!value) return
89-
setApiKey(value)
90-
setStep("url")
153+
textColor={theme.text}
154+
focusedTextColor={theme.text}
155+
cursorColor={theme.text}
156+
onMouseUp={() => {
157+
setStore("active", "key")
158+
focusActive()
91159
}}
92160
/>
93-
)}
94-
{step() === "url" && (
95-
<DialogPrompt
96-
title="Altimate URL"
161+
</box>
162+
163+
<box>
164+
<text fg={store.active === "url" ? theme.text : theme.textMuted}>URL:</text>
165+
<textarea
166+
height={3}
167+
ref={(val: TextareaRenderable) => (urlRef = val)}
168+
initialValue="https://api.myaltimate.com"
97169
placeholder="https://api.myaltimate.com"
98-
value="https://api.myaltimate.com"
99-
description={() => (
100-
<box gap={1}>
101-
<text fg={theme.textMuted}>Enter your Altimate server URL</text>
102-
<Show when={validating()}>
103-
<text fg={theme.textMuted}>Validating credentials...</text>
104-
</Show>
105-
<Show when={error()}>
106-
<text fg={theme.error}>{error()}</text>
107-
</Show>
108-
</box>
109-
)}
110-
onConfirm={async (value) => {
111-
if (!value) return
112-
await validateAndSave(value)
170+
textColor={theme.text}
171+
focusedTextColor={theme.text}
172+
cursorColor={theme.text}
173+
onMouseUp={() => {
174+
setStore("active", "url")
175+
focusActive()
113176
}}
114177
/>
115-
)}
116-
</>
178+
</box>
179+
180+
<Show when={error()}>
181+
<text fg={theme.error}>{error()}</text>
182+
</Show>
183+
<Show when={validating()}>
184+
<text fg={theme.textMuted}>Validating credentials...</text>
185+
</Show>
186+
187+
<text fg={theme.textMuted} paddingBottom={1}>
188+
<span style={{ fg: theme.text }}>tab</span> next field{" "}
189+
<span style={{ fg: theme.text }}>enter</span> submit
190+
</text>
191+
</box>
117192
)
118193
}

0 commit comments

Comments
 (0)