Skip to content

Commit e50bcdf

Browse files
authored
settings screen (#20)
1 parent 682315c commit e50bcdf

File tree

7 files changed

+290
-74
lines changed

7 files changed

+290
-74
lines changed

src/renderer/components/AuthScreen.tsx

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Text,
1010
TextField,
1111
} from "@radix-ui/themes";
12+
import { useMutation } from "@tanstack/react-query";
1213
import type React from "react";
1314
import { useId, useState } from "react";
1415
import { useAuthStore } from "../stores/authStore";
@@ -19,23 +20,18 @@ export function AuthScreen() {
1920
const apiHostId = useId();
2021
const [apiKey, setApiKey] = useState("");
2122
const [apiHost, setApiHost] = useState("https://app.posthog.com");
22-
const [error, setError] = useState("");
23-
const [isLoading, setIsLoading] = useState(false);
2423

2524
const { setCredentials } = useAuthStore();
2625

26+
const authMutation = useMutation({
27+
mutationFn: async ({ apiKey, host }: { apiKey: string; host: string }) => {
28+
await setCredentials(apiKey, host);
29+
},
30+
});
31+
2732
const handleSubmit = async (e: React.FormEvent) => {
2833
e.preventDefault();
29-
setError("");
30-
setIsLoading(true);
31-
32-
try {
33-
await setCredentials(apiKey, apiHost);
34-
} catch (err) {
35-
setError(err instanceof Error ? err.message : "Failed to authenticate");
36-
} finally {
37-
setIsLoading(false);
38-
}
34+
authMutation.mutate({ apiKey, host: apiHost });
3935
};
4036

4137
return (
@@ -102,20 +98,25 @@ export function AuthScreen() {
10298
</Flex>
10399
</Flex>
104100

105-
{error && (
101+
{authMutation.isError && (
106102
<Callout.Root color="red">
107-
<Callout.Text>{error}</Callout.Text>
103+
<Callout.Text>
104+
{authMutation.error instanceof Error
105+
? authMutation.error.message
106+
: "Failed to authenticate"}
107+
</Callout.Text>
108108
</Callout.Root>
109109
)}
110110

111111
<Button
112112
type="submit"
113-
disabled={isLoading || !apiKey}
113+
disabled={authMutation.isPending || !apiKey}
114114
variant="classic"
115115
size="3"
116116
mt="4"
117+
loading={authMutation.isPending}
117118
>
118-
{isLoading ? "Connecting..." : "Connect"}
119+
{authMutation.isPending ? "Connecting..." : "Connect"}
119120
</Button>
120121
</Flex>
121122
</form>

src/renderer/components/MainLayout.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useHotkeys } from "react-hotkeys-hook";
55
import { useIntegrations } from "../hooks/useIntegrations";
66
import { useTabStore } from "../stores/tabStore";
77
import { CommandMenu } from "./command";
8+
import { SettingsView } from "./SettingsView";
89
import { StatusBar } from "./StatusBar";
910
import { TabBar } from "./TabBar";
1011
import { TaskCreate } from "./TaskCreate";
@@ -33,7 +34,6 @@ export function MainLayout() {
3334
useHotkeys("mod+shift+n", () => setWorkflowCreateOpen(true));
3435

3536
const handleSelectTask = (task: Task) => {
36-
// Check if task is already open in a tab
3737
const existingTab = tabs.find(
3838
(tab) =>
3939
tab.type === "task-detail" &&
@@ -54,6 +54,19 @@ export function MainLayout() {
5454
}
5555
};
5656

57+
const handleOpenSettings = () => {
58+
const existingTab = tabs.find((tab) => tab.type === "settings");
59+
60+
if (existingTab) {
61+
setActiveTab(existingTab.id);
62+
} else {
63+
createTab({
64+
type: "settings",
65+
title: "Settings",
66+
});
67+
}
68+
};
69+
5770
const activeTab = tabs.find((tab) => tab.id === activeTabId);
5871

5972
return (
@@ -76,9 +89,11 @@ export function MainLayout() {
7689
{activeTab?.type === "workflow" && (
7790
<WorkflowView onSelectTask={handleSelectTask} />
7891
)}
92+
93+
{activeTab?.type === "settings" && <SettingsView />}
7994
</Box>
8095

81-
<StatusBar />
96+
<StatusBar onOpenSettings={handleOpenSettings} />
8297

8398
<CommandMenu
8499
open={commandMenuOpen}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import {
2+
Box,
3+
Button,
4+
Card,
5+
Flex,
6+
Heading,
7+
Switch,
8+
Text,
9+
TextField,
10+
} from "@radix-ui/themes";
11+
import { useMutation } from "@tanstack/react-query";
12+
import { useState } from "react";
13+
import { useAuthStore } from "../stores/authStore";
14+
import { useThemeStore } from "../stores/themeStore";
15+
import { AsciiArt } from "./AsciiArt";
16+
17+
export function SettingsView() {
18+
const {
19+
apiKey: currentApiKey,
20+
apiHost,
21+
isAuthenticated,
22+
setCredentials,
23+
logout,
24+
} = useAuthStore();
25+
const isDarkMode = useThemeStore((state) => state.isDarkMode);
26+
const toggleDarkMode = useThemeStore((state) => state.toggleDarkMode);
27+
const [apiKey, setApiKey] = useState("");
28+
const [host, setHost] = useState(apiHost);
29+
const [initialHost] = useState(apiHost);
30+
31+
const updateCredentialsMutation = useMutation({
32+
mutationFn: async ({ apiKey, host }: { apiKey: string; host: string }) => {
33+
await setCredentials(apiKey, host);
34+
},
35+
onSuccess: () => {
36+
setApiKey("");
37+
setTimeout(() => updateCredentialsMutation.reset(), 3000);
38+
},
39+
});
40+
41+
const handleApiKeyBlur = () => {
42+
// Only update if apiKey was entered (not empty)
43+
if (apiKey.trim()) {
44+
updateCredentialsMutation.mutate({ apiKey, host });
45+
}
46+
};
47+
48+
const handleHostBlur = () => {
49+
// Only update if host changed and is not empty
50+
if (host.trim() && host !== initialHost) {
51+
// Need current API key or new one to update
52+
const keyToUse = apiKey.trim() || currentApiKey;
53+
if (keyToUse) {
54+
updateCredentialsMutation.mutate({ apiKey: keyToUse, host });
55+
}
56+
}
57+
};
58+
59+
const handleLogout = () => {
60+
logout();
61+
setApiKey("");
62+
setHost("https://us.posthog.com");
63+
updateCredentialsMutation.reset();
64+
};
65+
66+
return (
67+
<Box height="100%">
68+
<Flex height="100%">
69+
{/* Left pane - Settings */}
70+
<Box
71+
width="50%"
72+
p="6"
73+
className="border-gray-6 border-r"
74+
overflowY="auto"
75+
>
76+
<Flex direction="column" gap="6">
77+
<Flex direction="column" gap="2">
78+
<Heading size="6">Settings</Heading>
79+
<Text size="2" color="gray">
80+
Manage your PostHog connection and preferences
81+
</Text>
82+
</Flex>
83+
84+
{/* Appearance Section */}
85+
<Flex direction="column" gap="3">
86+
<Heading size="4">Appearance</Heading>
87+
<Card>
88+
<Flex align="center" justify="between">
89+
<Text size="2" weight="medium">
90+
Dark Mode
91+
</Text>
92+
<Switch
93+
checked={isDarkMode}
94+
onCheckedChange={toggleDarkMode}
95+
size="2"
96+
/>
97+
</Flex>
98+
</Card>
99+
</Flex>
100+
101+
<Box className="border-gray-6 border-t" />
102+
103+
{/* Authentication Section */}
104+
<Flex direction="column" gap="3">
105+
<Flex align="center" gap="3">
106+
<Heading size="4">Authentication</Heading>
107+
<Flex align="center" gap="2">
108+
<Box
109+
width="8px"
110+
height="8px"
111+
className={`rounded-full ${isAuthenticated ? "bg-green-9" : "bg-red-9"}`}
112+
/>
113+
<Text size="2" color="gray">
114+
{isAuthenticated ? "Connected" : "Not connected"}
115+
</Text>
116+
</Flex>
117+
</Flex>
118+
119+
<Card>
120+
<Flex direction="column" gap="3">
121+
<Flex direction="column" gap="2">
122+
<Text size="2" weight="medium">
123+
API Key
124+
</Text>
125+
<TextField.Root
126+
type="password"
127+
placeholder="Enter your PostHog API key"
128+
value={apiKey}
129+
onChange={(e) => setApiKey(e.target.value)}
130+
onBlur={handleApiKeyBlur}
131+
disabled={updateCredentialsMutation.isPending}
132+
/>
133+
</Flex>
134+
135+
<Flex direction="column" gap="2">
136+
<Text size="2" weight="medium">
137+
API Host
138+
</Text>
139+
<TextField.Root
140+
type="text"
141+
placeholder="https://us.posthog.com"
142+
value={host}
143+
onChange={(e) => setHost(e.target.value)}
144+
onBlur={handleHostBlur}
145+
disabled={updateCredentialsMutation.isPending}
146+
/>
147+
</Flex>
148+
149+
{updateCredentialsMutation.isError && (
150+
<Text size="2" color="red">
151+
{updateCredentialsMutation.error instanceof Error
152+
? updateCredentialsMutation.error.message
153+
: "Failed to update credentials"}
154+
</Text>
155+
)}
156+
157+
{updateCredentialsMutation.isSuccess && (
158+
<Text size="2" color="green">
159+
Credentials updated successfully
160+
</Text>
161+
)}
162+
163+
<Box>
164+
<Button
165+
variant="classic"
166+
size="2"
167+
onClick={() => {
168+
const keyToUse = apiKey.trim() || currentApiKey;
169+
if (keyToUse && host.trim()) {
170+
updateCredentialsMutation.mutate({
171+
apiKey: keyToUse,
172+
host,
173+
});
174+
}
175+
}}
176+
disabled={updateCredentialsMutation.isPending}
177+
loading={updateCredentialsMutation.isPending}
178+
>
179+
Save credentials
180+
</Button>
181+
</Box>
182+
</Flex>
183+
</Card>
184+
</Flex>
185+
186+
{isAuthenticated && <Box className="border-gray-6 border-t" />}
187+
188+
{/* Account Section */}
189+
{isAuthenticated && (
190+
<Flex direction="column" gap="3">
191+
<Heading size="4">Account</Heading>
192+
<Card>
193+
<Button
194+
variant="soft"
195+
color="red"
196+
onClick={handleLogout}
197+
disabled={updateCredentialsMutation.isPending}
198+
>
199+
Logout
200+
</Button>
201+
</Card>
202+
</Flex>
203+
)}
204+
</Flex>
205+
</Box>
206+
207+
{/* Right pane - ASCII Art */}
208+
<Box
209+
width="50%"
210+
height="100%"
211+
className="bg-panel-solid"
212+
style={{ position: "relative" }}
213+
>
214+
<Box style={{ position: "absolute", inset: 0, zIndex: 0 }}>
215+
<AsciiArt scale={1} opacity={0.1} />
216+
</Box>
217+
</Box>
218+
</Flex>
219+
</Box>
220+
);
221+
}

src/renderer/components/StatusBar.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
1-
import { Badge, Box, Code, Flex, Kbd } from "@radix-ui/themes";
1+
import { GearIcon } from "@radix-ui/react-icons";
2+
import {
3+
Badge,
4+
Box,
5+
Code,
6+
Flex,
7+
IconButton,
8+
Kbd,
9+
Tooltip,
10+
} from "@radix-ui/themes";
211
import { useStatusBarStore } from "../stores/statusBarStore";
312
import { StatusBarMenu } from "./StatusBarMenu";
413

514
interface StatusBarProps {
615
showKeyHints?: boolean;
16+
onOpenSettings?: () => void;
717
}
818

9-
export function StatusBar({ showKeyHints = true }: StatusBarProps) {
19+
export function StatusBar({
20+
showKeyHints = true,
21+
onOpenSettings,
22+
}: StatusBarProps) {
1023
const { statusText, keyHints } = useStatusBarStore();
1124

1225
// Determine if we're in development mode
@@ -55,6 +68,18 @@ export function StatusBar({ showKeyHints = true }: StatusBarProps) {
5568
v{version}
5669
</Code>
5770
</Badge>
71+
{onOpenSettings && (
72+
<Tooltip content="Settings">
73+
<IconButton
74+
size="1"
75+
variant="ghost"
76+
color="gray"
77+
onClick={onOpenSettings}
78+
>
79+
<GearIcon />
80+
</IconButton>
81+
</Tooltip>
82+
)}
5883
</Flex>
5984
</Box>
6085
);

0 commit comments

Comments
 (0)