File size: 7,208 Bytes
921d377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/**
 * ExternalUninstallDialog — confirmation modal for uninstalling external MCP servers.
 *
 * Shows persona dependency warnings before confirming.  Non-destructive:
 * the server files remain on disk and can be reinstalled at any time.
 * Tools are disabled (not deleted) and automatically re-enabled on reinstall.
 *
 * Fully additive — does not modify any existing component.
 */
import React, { useEffect, useState } from 'react';
import { AlertTriangle, Loader2, X, Users, Wrench } from 'lucide-react';
export function ExternalUninstallDialog({ server, backendUrl, apiKey, onClose, onConfirmUninstall, preview: externalPreview, }) {
    const [preview, setPreview] = useState(externalPreview ?? null);
    const [loadingPreview, setLoadingPreview] = useState(!externalPreview);
    // Fetch preview on mount if not provided
    useEffect(() => {
        if (externalPreview)
            return;
        const fetchPreview = async () => {
            setLoadingPreview(true);
            try {
                const headers = {};
                if (apiKey)
                    headers['x-api-key'] = apiKey;
                const res = await fetch(`${backendUrl}/v1/agentic/servers/external/${encodeURIComponent(server.id)}/uninstall-preview`, { headers });
                if (res.ok) {
                    setPreview(await res.json());
                }
            }
            catch {
                // Preview failed — still allow uninstall
            }
            finally {
                setLoadingPreview(false);
            }
        };
        void fetchPreview();
    }, [server.id, backendUrl, apiKey, externalPreview]);
    const affectedCount = preview?.affected_personas?.length ?? 0;
    const toolCount = preview?.tools_to_deactivate?.length ?? server.tools_discovered ?? 0;
    return (<div className="fixed inset-0 z-[60] flex items-center justify-center" onClick={onClose}>
      {/* Backdrop */}
      <div className="absolute inset-0 bg-black/70 backdrop-blur-sm"/>

      {/* Dialog */}
      <div className="relative w-full max-w-md bg-[#0f0f18] border border-white/10 rounded-2xl shadow-2xl overflow-hidden animate-fade-scale-in" onClick={(e) => e.stopPropagation()}>
        {/* Header */}
        <div className="flex items-center justify-between px-5 pt-5 pb-2">
          <div className="flex items-center gap-3">
            <div className={`w-9 h-9 rounded-xl flex items-center justify-center border ${affectedCount > 0
            ? 'bg-amber-500/10 border-amber-500/20'
            : 'bg-red-500/10 border-red-500/20'}`}>
              <AlertTriangle size={18} className={affectedCount > 0 ? 'text-amber-400' : 'text-red-400'}/>
            </div>
            <h3 className="text-sm font-semibold text-white">Uninstall {server.label}?</h3>
          </div>
          <button onClick={onClose} className="p-1.5 text-white/40 hover:text-white/70 hover:bg-white/10 rounded-lg transition-colors">
            <X size={16}/>
          </button>
        </div>

        {/* Body */}
        <div className="px-5 py-4 space-y-3">
          {loadingPreview ? (<div className="flex items-center gap-2 text-sm text-white/40">
              <Loader2 size={14} className="animate-spin"/>
              Checking dependencies...
            </div>) : (<>
              {/* Persona dependency warning */}
              {affectedCount > 0 && (<div className="rounded-xl bg-amber-500/5 border border-amber-500/20 p-4 space-y-2">
                  <div className="flex items-center gap-2 text-amber-300 text-xs font-semibold uppercase tracking-wide">
                    <Users size={14}/>
                    {affectedCount} Persona{affectedCount > 1 ? 's' : ''} Affected
                  </div>
                  <div className="space-y-1.5">
                    {preview.affected_personas.map((p) => (<div key={p.project_id} className="flex items-start gap-2 text-xs">
                        <span className="text-amber-200/80 font-medium shrink-0">{p.project_name}</span>
                        <span className="text-white/30"></span>
                        <span className="text-white/40">
                          {p.tools_affected.length} tool{p.tools_affected.length > 1 ? 's' : ''} will be disabled
                        </span>
                      </div>))}
                  </div>
                  <p className="text-[11px] text-amber-200/50 leading-relaxed mt-1">
                    These personas will lose access to {server.label} tools until the server is reinstalled.
                  </p>
                </div>)}

              {/* Tools to deactivate */}
              {toolCount > 0 && (<div className="flex items-center gap-2 text-xs text-white/50">
                  <Wrench size={12} className="shrink-0 text-white/30"/>
                  <span>{toolCount} tool{toolCount > 1 ? 's' : ''} will be deactivated in Context Forge</span>
                </div>)}

              {/* Description */}
              <p className="text-sm text-white/60 leading-relaxed">
                This will stop <span className="text-white font-medium">{server.label}</span> and
                deactivate its tools from your HomePilot instance.
              </p>

              {/* Safety info */}
              <div className="rounded-lg bg-white/5 border border-white/5 p-3 space-y-1.5">
                <p className="text-xs text-white/50">
                  <span className="text-emerald-400">Non-destructive:</span> Server files remain on disk. No data is deleted.
                </p>
                <p className="text-xs text-white/50">
                  <span className="text-emerald-400">Reversible:</span> Reinstall anytime — disabled tools will be re-enabled automatically.
                </p>
                {affectedCount > 0 && (<p className="text-xs text-white/50">
                    <span className="text-amber-400">Personas:</span> Affected personas will have this tool disabled. Re-importing or reinstalling restores them.
                  </p>)}
              </div>
            </>)}
        </div>

        {/* Actions */}
        <div className="flex items-center gap-3 px-5 pb-5">
          <button onClick={onClose} className="flex-1 px-4 py-2.5 text-sm font-medium text-white/60 bg-white/5 hover:bg-white/10 rounded-xl transition-colors border border-white/10">
            Cancel
          </button>
          <button onClick={onConfirmUninstall} disabled={loadingPreview} className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium rounded-xl transition-colors border disabled:opacity-50 ${affectedCount > 0
            ? 'text-amber-200 bg-amber-500/20 hover:bg-amber-500/30 border-amber-500/30'
            : 'text-red-200 bg-red-500/20 hover:bg-red-500/30 border-red-500/30'}`}>
            {affectedCount > 0 ? 'Uninstall Anyway' : 'Uninstall'}
          </button>
        </div>
      </div>

      <style>{`
        @keyframes fade-scale-in {
          from { opacity: 0; transform: scale(0.95); }
          to { opacity: 1; transform: scale(1); }
        }
        .animate-fade-scale-in {
          animation: fade-scale-in 0.15s ease-out;
        }
      `}</style>
    </div>);
}