Skip to content

Commit 4b45b45

Browse files
committed
feat: enhance cloud configuration management with delete functionality
- Added a tooltip and delete button in PersonalCloudTable and TeamCloudTable for removing all personal and team server configurations, respectively. - Implemented confirmation dialogs to prevent accidental deletions and provide user feedback via toast notifications. - Updated the LocalTable to show an alert dialog when the encryption key is missing, guiding users to the settings for key generation. - Improved error handling in cloud sync hooks to provide clearer messages on failures.
1 parent 04a9fa8 commit 4b45b45

File tree

8 files changed

+206
-63
lines changed

8 files changed

+206
-63
lines changed

tauri-app/src/components/manage/LocalTable/index.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
import { CloudSyncDialog } from "@/components/manage/CloudSyncDialog";
44
import { LocalSyncDialog } from "@/components/manage/LocalSyncDialog";
55
import { RefreshMcpConfig } from "@/components/manage/RefreshMcpConfig";
6+
import {
7+
AlertDialog,
8+
AlertDialogAction,
9+
AlertDialogCancel,
10+
AlertDialogContent,
11+
AlertDialogDescription,
12+
AlertDialogFooter,
13+
AlertDialogHeader,
14+
AlertDialogTitle,
15+
} from "@/components/ui/alert-dialog";
616
import { DataTable } from "@/components/ui/data-table";
717
import { useCloudSync } from "@/hooks/useCloudSync";
818
import { useMcpConfig } from "@/hooks/useMcpConfig";
@@ -12,6 +22,7 @@ import { UserWithTier } from "@/stores/userStore";
1222
import { getEncryptionKey } from "@/utils/encryption";
1323
import { RowSelectionState, Table } from "@tanstack/react-table";
1424
import { useState } from "react";
25+
import { useNavigate } from "react-router";
1526
import { useServerTableColumns } from "../ServerTableColumns";
1627
import { LocalTableHeader } from "./LocalTableHeader";
1728
import { useServersData } from "./useServersData";
@@ -29,7 +40,9 @@ export const LocalTable = ({ isAuthenticated, user }: LocalTableProps) => {
2940
const [isDeleting, _setIsDeleting] = useState(false);
3041
const [_tableInstance, setTableInstance] = useState<Table<any> | null>(null);
3142
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
43+
const [showMissingKeyDialog, setShowMissingKeyDialog] = useState(false);
3244
const showGlobalDialog = useGlobalDialogStore((s) => s.showDialog);
45+
const navigate = useNavigate();
3346

3447
const {
3548
config,
@@ -129,7 +142,8 @@ export const LocalTable = ({ isAuthenticated, user }: LocalTableProps) => {
129142
}
130143
const key = getEncryptionKey();
131144
if (!key) {
132-
showGlobalDialog("login");
145+
setShowMissingKeyDialog(true);
146+
return;
133147
} else {
134148
setCloudSyncDialogOpen(true);
135149
}
@@ -183,6 +197,26 @@ export const LocalTable = ({ isAuthenticated, user }: LocalTableProps) => {
183197
servers={serversData}
184198
onCloudDownloadSuccess={loadConfig}
185199
/>
200+
<AlertDialog
201+
open={showMissingKeyDialog}
202+
onOpenChange={setShowMissingKeyDialog}
203+
>
204+
<AlertDialogContent>
205+
<AlertDialogHeader>
206+
<AlertDialogTitle>Missing Encryption Key</AlertDialogTitle>
207+
<AlertDialogDescription>
208+
To use cloud sync, please go to Settings and generate your
209+
encryption key.
210+
</AlertDialogDescription>
211+
</AlertDialogHeader>
212+
<AlertDialogFooter>
213+
<AlertDialogCancel>Cancel</AlertDialogCancel>
214+
<AlertDialogAction onClick={() => navigate("/settings")}>
215+
Go to Settings
216+
</AlertDialogAction>
217+
</AlertDialogFooter>
218+
</AlertDialogContent>
219+
</AlertDialog>
186220
</>
187221
)}
188222
</div>

tauri-app/src/components/manage/PersonalCloudTable.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@ import { DeleteAlertDialog } from "@/components/common/DeleteAlertDialog";
22
import { Button } from "@/components/ui/button";
33
import { Checkbox } from "@/components/ui/checkbox";
44
import { DataTable } from "@/components/ui/data-table";
5+
import {
6+
Tooltip,
7+
TooltipContent,
8+
TooltipProvider,
9+
TooltipTrigger,
10+
} from "@/components/ui/tooltip";
511
import { useCloudSync } from "@/hooks/useCloudSync";
612
import { api } from "@/lib/api";
713
import { useClientPathStore } from "@/stores/clientPathStore";
814
import { ServerTableData } from "@/types";
915
import { ColumnDef, RowSelectionState, Table } from "@tanstack/react-table";
1016
import { invoke } from "@tauri-apps/api/core";
17+
import { Trash2 } from "lucide-react";
1118
import { useEffect, useState } from "react";
1219
import { toast } from "sonner";
1320
import { CommandDisplay } from "./CommandDisplay";
@@ -111,7 +118,43 @@ export const PersonalCloudTable = () => {
111118

112119
return (
113120
<div className="flex flex-col">
114-
<div className="flex justify-between items-center"></div>
121+
<div className="flex justify-end">
122+
<TooltipProvider>
123+
<Tooltip>
124+
<TooltipTrigger asChild>
125+
<span>
126+
<DeleteAlertDialog
127+
itemName={`all personal server configs`}
128+
onDelete={async () => {
129+
try {
130+
await api.delete(`/user-server-configs/`);
131+
setServersData([]);
132+
toast.success(
133+
`remove personal server configs in the cloud`,
134+
);
135+
} catch (e) {
136+
toast.error(JSON.stringify(e));
137+
}
138+
}}
139+
trigger={
140+
<Button
141+
variant="destructive"
142+
size="sm"
143+
className="flex items-center gap-1"
144+
>
145+
<Trash2 />
146+
Delete all personal Config
147+
</Button>
148+
}
149+
/>
150+
</span>
151+
</TooltipTrigger>
152+
<TooltipContent>
153+
Only use if encryptionKey has changed.
154+
</TooltipContent>
155+
</Tooltip>
156+
</TooltipProvider>
157+
</div>
115158
<DataTable
116159
columns={columns}
117160
data={serversData}

tauri-app/src/components/manage/team/TeamCloudTable.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@ import { DeleteAlertDialog } from "@/components/common/DeleteAlertDialog";
22
import { Button } from "@/components/ui/button";
33
import { Checkbox } from "@/components/ui/checkbox";
44
import { DataTable } from "@/components/ui/data-table";
5+
import {
6+
Tooltip,
7+
TooltipContent,
8+
TooltipProvider,
9+
TooltipTrigger,
10+
} from "@/components/ui/tooltip";
511
import { useTeamCloudSync } from "@/hooks/useTeamCloudSync";
612
import { api } from "@/lib/api";
713
import { useClientPathStore } from "@/stores/clientPathStore";
814
import { useTeamStore } from "@/stores/team";
915
import { ServerTableData } from "@/types";
1016
import { ColumnDef, RowSelectionState, Table } from "@tanstack/react-table";
1117
import { invoke } from "@tauri-apps/api/core";
18+
import { Trash2 } from "lucide-react";
1219
import { useEffect, useState } from "react";
1320
import { toast } from "sonner";
1421
import { CommandDisplay } from "../CommandDisplay";
@@ -19,7 +26,7 @@ export const TeamCloudTable = () => {
1926
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
2027
const [serversData, setServersData] = useState<ServerTableData[]>([]);
2128
const { selectedClient, selectedPath } = useClientPathStore();
22-
const { selectedTeamId } = useTeamStore();
29+
const { selectedTeamId, selectedTeamName } = useTeamStore();
2330

2431
const { fetchCloudDownload } = useTeamCloudSync(serversData);
2532

@@ -132,6 +139,45 @@ export const TeamCloudTable = () => {
132139

133140
return (
134141
<div className="flex flex-col">
142+
<div className="flex flex-col gap-4">
143+
<div className="flex justify-end">
144+
<TooltipProvider>
145+
<Tooltip>
146+
<TooltipTrigger asChild>
147+
<span>
148+
<DeleteAlertDialog
149+
itemName={`all team **(${selectedTeamName})** server configs`}
150+
onDelete={async () => {
151+
try {
152+
await api.delete(`/teams/${selectedTeamId}/configs`);
153+
setServersData([]);
154+
toast.success(
155+
`remove team ${selectedTeamName} server configs in the cloud`,
156+
);
157+
} catch (e) {
158+
toast.error(JSON.stringify(e));
159+
}
160+
}}
161+
trigger={
162+
<Button
163+
variant="destructive"
164+
size="sm"
165+
className="flex items-center gap-1"
166+
>
167+
<Trash2 />
168+
Delete Team Config
169+
</Button>
170+
}
171+
/>
172+
</span>
173+
</TooltipTrigger>
174+
<TooltipContent>
175+
Only use if encryptionKey has changed.
176+
</TooltipContent>
177+
</Tooltip>
178+
</TooltipProvider>
179+
</div>
180+
</div>
135181
<DataTable
136182
columns={columns}
137183
data={serversData}

tauri-app/src/components/settings/EncryptionKeyCard.tsx

Lines changed: 54 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
getEncryptionKey,
1414
storeEncryptionKey,
1515
} from "@/utils/encryption";
16-
import { Copy, Eye, EyeOff, RotateCcw, Save, User, Users } from "lucide-react";
16+
import { Copy, RotateCcw, Save, User, Users } from "lucide-react";
1717
import { useEffect, useState } from "react";
1818
import { toast } from "sonner";
1919

@@ -31,23 +31,29 @@ export default function EncryptionKeyCard({
3131
const [key, setKey] = useState<string>("");
3232
const [showKey, setShowKey] = useState(false);
3333
const [hasViewedGeneratedKey, setHasViewedGeneratedKey] = useState(false);
34+
const [hasStoredKey, setHasStoredKey] = useState(false);
3435

3536
useEffect(() => {
3637
const storedKey = getEncryptionKey(keyId);
3738
if (storedKey) {
38-
setKey("•".repeat(16));
39-
setHasViewedGeneratedKey(false);
39+
setKey("");
40+
setHasStoredKey(true);
41+
} else {
42+
setHasStoredKey(false);
4043
}
4144
}, [keyId]);
4245

4346
const handleGenerateKey = async () => {
4447
try {
48+
setHasStoredKey(false);
4549
const newKey = await generateEncryptionKey();
4650
setKey(newKey);
4751
setShowKey(true);
4852
setHasViewedGeneratedKey(true);
49-
storeEncryptionKey(newKey, keyId);
50-
toast.success(`New encryption key generated and saved for ${keyName}`);
53+
// Do not store or set hasStoredKey here; wait until save
54+
toast.success(
55+
`New encryption key generated. Please copy and store it safely. You won't be able to view it again after saving.`,
56+
);
5157
} catch (error) {
5258
console.error("Error generating key:", error);
5359
toast.error("Failed to generate encryption key");
@@ -62,17 +68,23 @@ export default function EncryptionKeyCard({
6268

6369
try {
6470
storeEncryptionKey(key, keyId);
65-
toast.success("Encryption key saved successfully");
71+
setHasStoredKey(true);
72+
// don't clear key here to allow copy after saving
73+
toast.success(
74+
"Encryption key saved successfully. You won't be able to view it again.",
75+
);
6676
} catch (error) {
6777
console.error("Error saving key:", error);
6878
toast.error("Failed to save encryption key");
6979
}
7080
};
7181

7282
const handleCopyKey = () => {
73-
if (key) {
83+
if (key && (showKey || hasViewedGeneratedKey)) {
7484
navigator.clipboard.writeText(key);
7585
toast.success("Key copied to clipboard");
86+
} else {
87+
toast.error("Please reveal or generate the key before copying");
7688
}
7789
};
7890

@@ -90,51 +102,49 @@ export default function EncryptionKeyCard({
90102
{keyName} Encryption Key
91103
</CardTitle>
92104
<CardDescription>
93-
View, generate or enter encryption key for {keyName}
105+
{hasStoredKey
106+
? "Encryption key is set. You cannot view or retrieve it again. If you wish to change it, generate or enter a new key."
107+
: `View, generate or enter encryption key for ${keyName}`}
94108
</CardDescription>
95109
</CardHeader>
96110
<CardContent className="space-y-4">
97111
<div className="space-y-2">
98112
<Label htmlFor={`key-${keyId}`}>Encryption Key</Label>
99-
<div className="flex gap-2">
100-
<Input
101-
id={`key-${keyId}`}
102-
type={showKey ? "text" : "password"}
103-
value={showKey || hasViewedGeneratedKey ? key : "•".repeat(16)}
104-
onChange={(e) => {
105-
setKey(e.target.value);
106-
setHasViewedGeneratedKey(true);
107-
}}
108-
placeholder="Enter or generate encryption key"
109-
className="font-mono text-sm"
110-
/>
111-
<Button
112-
variant="outline"
113-
size="icon"
114-
onClick={() => setShowKey(!showKey)}
115-
title={showKey ? "Hide key" : "Show key"}
116-
>
117-
{showKey ? (
118-
<EyeOff className="w-4 h-4" />
119-
) : (
120-
<Eye className="w-4 h-4" />
121-
)}
122-
</Button>
123-
<Button
124-
variant="outline"
125-
size="icon"
126-
onClick={handleCopyKey}
127-
disabled={!key}
128-
title="Copy key"
129-
>
130-
<Copy className="w-4 h-4" />
131-
</Button>
132-
</div>
133-
{!key.trim() && !hasViewedGeneratedKey && (
113+
{!hasStoredKey && (
114+
<div className="flex gap-2">
115+
<Input
116+
id={`key-${keyId}`}
117+
type={showKey ? "text" : "password"}
118+
value={key}
119+
onChange={(e) => {
120+
setKey(e.target.value);
121+
setHasViewedGeneratedKey(true);
122+
}}
123+
placeholder="Enter or generate encryption key"
124+
className="font-mono text-sm"
125+
/>
126+
<Button
127+
variant="outline"
128+
size="icon"
129+
onClick={handleCopyKey}
130+
disabled={!key}
131+
title="Copy key"
132+
>
133+
<Copy className="w-4 h-4" />
134+
</Button>
135+
</div>
136+
)}
137+
{!hasStoredKey && !key.trim() && !hasViewedGeneratedKey && (
134138
<p className="text-destructive text-sm mt-1">
135139
Please generate or enter an encryption key to save.
136140
</p>
137141
)}
142+
{hasStoredKey && (
143+
<p className="text-muted-foreground text-sm mt-1">
144+
Key is securely stored. For security, you cannot view it again. To
145+
set a new key, generate or enter and save a new key.
146+
</p>
147+
)}
138148
</div>
139149
<div className="flex gap-2">
140150
<Button
@@ -147,7 +157,7 @@ export default function EncryptionKeyCard({
147157
</Button>
148158
<Button
149159
onClick={handleSaveKey}
150-
disabled={!key.trim() || !hasViewedGeneratedKey}
160+
disabled={hasStoredKey || !key.trim()}
151161
className="flex items-center gap-2"
152162
>
153163
<Save className="w-4 h-4" />

tauri-app/src/hooks/useTeamCloudSync.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const useTeamCloudSync = (servers: ServerTableData[]) => {
2929
const serverConfigs = await fetchTeamConfigs(teamId);
3030
return serverConfigs;
3131
} catch (e) {
32-
toast.error("Cloud fetch failed");
32+
toast.error(e instanceof Error ? e.message : "Cloud fetch failed");
3333
} finally {
3434
setIsSyncing(false);
3535
}

0 commit comments

Comments
 (0)