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"
25import { useDialog } from "@tui/ui/dialog"
36import { useSDK } from "../context/sdk"
47import { useSync } from "@tui/context/sync"
58import { useLocal } from "@tui/context/local"
6- import { DialogPrompt } from "../ui/dialog-prompt"
79import { useTheme } from "../context/theme"
810import { AltimateApi } from "@/altimate/api/client"
911import { 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