@anymux/connect 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/GitBrowser-BLgTNQyd.js +905 -0
- package/dist/GitBrowser-BLgTNQyd.js.map +1 -0
- package/dist/GitBrowser-CIyWiuX-.js +3 -0
- package/dist/ObjectStorageBrowser-B2YkUxMl.js +3 -0
- package/dist/ObjectStorageBrowser-B_25Emfu.js +267 -0
- package/dist/ObjectStorageBrowser-B_25Emfu.js.map +1 -0
- package/dist/RepoPicker-BprFGOn7.js +3 -0
- package/dist/RepoPicker-CoHMiJ-3.js +168 -0
- package/dist/RepoPicker-CoHMiJ-3.js.map +1 -0
- package/dist/index.d.ts +697 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2539 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +3 -0
- package/dist/scope-labels-B4VAwoL6.js +582 -0
- package/dist/scope-labels-B4VAwoL6.js.map +1 -0
- package/dist/scope-labels-DvdJLcSL.d.ts +50 -0
- package/dist/scope-labels-DvdJLcSL.d.ts.map +1 -0
- package/package.json +87 -0
- package/src/adapters/adapter-registry.ts +177 -0
- package/src/auth/auth-client.ts +101 -0
- package/src/auth/token-manager.ts +27 -0
- package/src/components/ActionHistoryPanel.tsx +137 -0
- package/src/components/CapabilityCell.tsx +97 -0
- package/src/components/CapabilityError.tsx +50 -0
- package/src/components/CapabilityPanel.tsx +530 -0
- package/src/components/CapabilityPill.tsx +56 -0
- package/src/components/ConnectButton.tsx +149 -0
- package/src/components/ConnectedMenu.tsx +142 -0
- package/src/components/ConnectionStatus.tsx +28 -0
- package/src/components/CredentialForm.tsx +246 -0
- package/src/components/FullScreenBrowser.tsx +84 -0
- package/src/components/GitBrowser.tsx +705 -0
- package/src/components/GitHubRepoPicker.tsx +125 -0
- package/src/components/ObjectStorageBrowser.tsx +176 -0
- package/src/components/RepoPicker.tsx +93 -0
- package/src/components/ServiceCard.tsx +77 -0
- package/src/components/ServiceCardGrid.tsx +141 -0
- package/src/components/ServiceDashboard.tsx +84 -0
- package/src/components/ServiceIcon.tsx +37 -0
- package/src/components/ServiceRow.tsx +50 -0
- package/src/components/useAdapter.ts +33 -0
- package/src/demos/ServiceDashboardDemo.tsx +108 -0
- package/src/index.ts +68 -0
- package/src/models/ActionNotificationModel.ts +72 -0
- package/src/models/ConnectionManagerModel.ts +410 -0
- package/src/models/CredentialFormModel.ts +111 -0
- package/src/models/DashboardModel.ts +157 -0
- package/src/models/GitHostBrowserModel.ts +89 -0
- package/src/models/GitRepoBrowserModel.ts +285 -0
- package/src/models/ObjectStorageBrowserModel.ts +131 -0
- package/src/models/RepoPickerModel.ts +132 -0
- package/src/registry/service-registry.ts +46 -0
- package/src/registry/services/apple.ts +22 -0
- package/src/registry/services/bitbucket.ts +24 -0
- package/src/registry/services/box.ts +22 -0
- package/src/registry/services/browser-fs.ts +19 -0
- package/src/registry/services/dropbox.ts +22 -0
- package/src/registry/services/flickr.ts +22 -0
- package/src/registry/services/gitea.ts +24 -0
- package/src/registry/services/github.ts +24 -0
- package/src/registry/services/gitlab.ts +24 -0
- package/src/registry/services/google.ts +24 -0
- package/src/registry/services/icloud.ts +23 -0
- package/src/registry/services/indexeddb.ts +19 -0
- package/src/registry/services/instagram.ts +22 -0
- package/src/registry/services/microsoft.ts +24 -0
- package/src/registry/services/s3.ts +21 -0
- package/src/registry/services/webdav.ts +21 -0
- package/src/registry.ts +4 -0
- package/src/types/connection-state.ts +33 -0
- package/src/types/connection.ts +11 -0
- package/src/types/optional-deps.d.ts +149 -0
- package/src/types/service.ts +18 -0
- package/src/types/user-profile.ts +21 -0
- package/src/utils/action-toast.ts +53 -0
- package/src/utils/scope-labels.ts +91 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2539 @@
|
|
|
1
|
+
import { bitbucketService, browserFsService, dropboxService, getScopeLabel, getScopeLabels, giteaService, githubService, gitlabService, googleService, indexeddbService, s3Service, serviceRegistry, webdavService } from "./scope-labels-B4VAwoL6.js";
|
|
2
|
+
import { GitHostBrowser, GitHostBrowserModel, GitRepoBrowser, GitRepoBrowserModel, showActionToast, showErrorToast, showInfoToast } from "./GitBrowser-BLgTNQyd.js";
|
|
3
|
+
import { ObjectStorageBrowser, ObjectStorageBrowserModel } from "./ObjectStorageBrowser-B_25Emfu.js";
|
|
4
|
+
import { RepoPicker, RepoPickerModel } from "./RepoPicker-CoHMiJ-3.js";
|
|
5
|
+
import { createAuthClient } from "better-auth/client";
|
|
6
|
+
import { genericOAuthClient } from "better-auth/client/plugins";
|
|
7
|
+
import { flow, makeAutoObservable, runInAction } from "mobx";
|
|
8
|
+
import { match } from "ts-pattern";
|
|
9
|
+
import React, { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
10
|
+
import { observer } from "mobx-react-lite";
|
|
11
|
+
import * as icons from "lucide-react";
|
|
12
|
+
import { AlertTriangle, ArrowLeft, Check, Clock, Copy, ExternalLink, FileEdit, FilePlus, History, Info, Loader2, Lock, LogOut, Minus, MoreHorizontal, Move, RefreshCw, Search, Shield, Trash2, Undo2, Upload, X } from "lucide-react";
|
|
13
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
import { ConnectionBadge } from "@anymux/ui/components/connection-badge";
|
|
15
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@anymux/ui/components/dropdown-menu";
|
|
16
|
+
import { LoadingSpinner } from "@anymux/ui/components/loading-spinner";
|
|
17
|
+
import { PathBreadcrumb } from "@anymux/ui/components/path-breadcrumb";
|
|
18
|
+
import { ErrorDisplay } from "@anymux/ui/components/error-display";
|
|
19
|
+
import { StatusIndicator } from "@anymux/ui/components/status-indicator";
|
|
20
|
+
|
|
21
|
+
//#region src/registry/services/microsoft.ts
|
|
22
|
+
const microsoftService = {
|
|
23
|
+
id: "microsoft",
|
|
24
|
+
name: "Microsoft",
|
|
25
|
+
icon: "AppWindow",
|
|
26
|
+
color: "#00A4EF",
|
|
27
|
+
authProvider: "microsoft",
|
|
28
|
+
grantsUrl: "https://account.live.com/consent/Manage",
|
|
29
|
+
capabilities: [
|
|
30
|
+
{
|
|
31
|
+
id: "file-system",
|
|
32
|
+
supported: true
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "object-storage",
|
|
36
|
+
supported: false
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "git-repo",
|
|
40
|
+
supported: false
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "git-host",
|
|
44
|
+
supported: false
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "media",
|
|
48
|
+
supported: false
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "contacts",
|
|
52
|
+
supported: true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "calendar",
|
|
56
|
+
supported: true
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
scopes: {
|
|
60
|
+
"file-system": ["Files.ReadWrite.All"],
|
|
61
|
+
"contacts": ["Contacts.Read"],
|
|
62
|
+
"calendar": ["Calendars.Read"]
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/registry/services/icloud.ts
|
|
68
|
+
const icloudService = {
|
|
69
|
+
id: "icloud",
|
|
70
|
+
name: "iCloud",
|
|
71
|
+
icon: "Cloud",
|
|
72
|
+
color: "#007AFF",
|
|
73
|
+
authProvider: "icloud",
|
|
74
|
+
grantsUrl: "https://appleid.apple.com/account/manage",
|
|
75
|
+
capabilities: [
|
|
76
|
+
{
|
|
77
|
+
id: "file-system",
|
|
78
|
+
supported: false
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "object-storage",
|
|
82
|
+
supported: false
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "git-repo",
|
|
86
|
+
supported: false
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "git-host",
|
|
90
|
+
supported: false
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "media",
|
|
94
|
+
supported: false
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "contacts",
|
|
98
|
+
supported: true
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "calendar",
|
|
102
|
+
supported: true
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
scopes: {
|
|
106
|
+
contacts: [],
|
|
107
|
+
calendar: []
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/auth/auth-client.ts
|
|
113
|
+
const PENDING_SERVICE_KEY = "anymux-pending-service";
|
|
114
|
+
function createConnectAuthClient(baseURL) {
|
|
115
|
+
const authClient = createAuthClient({
|
|
116
|
+
baseURL,
|
|
117
|
+
plugins: [genericOAuthClient()]
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
async signIn(provider, serviceId) {
|
|
121
|
+
sessionStorage.setItem(PENDING_SERVICE_KEY, serviceId);
|
|
122
|
+
const GENERIC_OAUTH_PROVIDERS = ["box", "bitbucket"];
|
|
123
|
+
if (GENERIC_OAUTH_PROVIDERS.includes(provider)) await authClient.signIn.oauth2({
|
|
124
|
+
providerId: provider,
|
|
125
|
+
callbackURL: window.location.href
|
|
126
|
+
});
|
|
127
|
+
else await authClient.signIn.social({
|
|
128
|
+
provider,
|
|
129
|
+
callbackURL: window.location.href
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
async signOut() {
|
|
133
|
+
await authClient.signOut();
|
|
134
|
+
},
|
|
135
|
+
async getSession() {
|
|
136
|
+
const session = await authClient.getSession();
|
|
137
|
+
if (!session.data) return null;
|
|
138
|
+
return { user: {
|
|
139
|
+
id: session.data.user.id,
|
|
140
|
+
name: session.data.user.name,
|
|
141
|
+
...session.data.user.image ? { image: session.data.user.image } : {}
|
|
142
|
+
} };
|
|
143
|
+
},
|
|
144
|
+
async getAccessToken(providerId) {
|
|
145
|
+
const result = await authClient.getAccessToken({ providerId });
|
|
146
|
+
return result.data?.accessToken ?? null;
|
|
147
|
+
},
|
|
148
|
+
async fetchConfiguredProviders() {
|
|
149
|
+
const res = await fetch(`${baseURL}/api/auth/configured`);
|
|
150
|
+
if (!res.ok) return {
|
|
151
|
+
database: false,
|
|
152
|
+
authSecret: false,
|
|
153
|
+
providers: {}
|
|
154
|
+
};
|
|
155
|
+
return res.json();
|
|
156
|
+
},
|
|
157
|
+
async fetchGrantedScopes() {
|
|
158
|
+
const res = await fetch(`${baseURL}/api/auth/scopes`, { credentials: "include" });
|
|
159
|
+
if (!res.ok) return {};
|
|
160
|
+
return res.json();
|
|
161
|
+
},
|
|
162
|
+
async fetchTestCredentials() {
|
|
163
|
+
const res = await fetch(`${baseURL}/api/auth/test-credentials`);
|
|
164
|
+
if (!res.ok) return {};
|
|
165
|
+
return res.json();
|
|
166
|
+
},
|
|
167
|
+
async fetchUserProfiles() {
|
|
168
|
+
const res = await fetch(`${baseURL}/api/auth/user-profiles`, { credentials: "include" });
|
|
169
|
+
if (!res.ok) return {};
|
|
170
|
+
return res.json();
|
|
171
|
+
},
|
|
172
|
+
async revokeProvider(providerId) {
|
|
173
|
+
const res = await fetch(`${baseURL}/api/auth/revoke-provider?provider=${encodeURIComponent(providerId)}`, {
|
|
174
|
+
method: "DELETE",
|
|
175
|
+
credentials: "include"
|
|
176
|
+
});
|
|
177
|
+
if (!res.ok) {
|
|
178
|
+
const text = await res.text().catch(() => "");
|
|
179
|
+
throw new Error(`revokeProvider ${providerId} failed: ${res.status} ${text}`);
|
|
180
|
+
}
|
|
181
|
+
const data = await res.json().catch(() => ({}));
|
|
182
|
+
console.info(`[AnyMux] revokeProvider(${providerId}): deleted=${data.deleted ?? "?"}`);
|
|
183
|
+
},
|
|
184
|
+
getPendingServiceId() {
|
|
185
|
+
return sessionStorage.getItem(PENDING_SERVICE_KEY);
|
|
186
|
+
},
|
|
187
|
+
clearPendingServiceId() {
|
|
188
|
+
sessionStorage.removeItem(PENDING_SERVICE_KEY);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/auth/token-manager.ts
|
|
195
|
+
const STORAGE_PREFIX = "anymux-connect-token:";
|
|
196
|
+
var TokenManager = class {
|
|
197
|
+
getToken(serviceId) {
|
|
198
|
+
try {
|
|
199
|
+
return localStorage.getItem(STORAGE_PREFIX + serviceId);
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
setToken(serviceId, token) {
|
|
205
|
+
try {
|
|
206
|
+
localStorage.setItem(STORAGE_PREFIX + serviceId, token);
|
|
207
|
+
} catch {}
|
|
208
|
+
}
|
|
209
|
+
removeToken(serviceId) {
|
|
210
|
+
try {
|
|
211
|
+
localStorage.removeItem(STORAGE_PREFIX + serviceId);
|
|
212
|
+
} catch {}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region src/models/ConnectionManagerModel.ts
|
|
218
|
+
const STORAGE_KEY = "anymux-connect-connections";
|
|
219
|
+
const USER_PROFILES_STORAGE_KEY = "anymux-connect-user-profiles";
|
|
220
|
+
var ConnectionManagerModel = class {
|
|
221
|
+
constructor(options) {
|
|
222
|
+
this.connections = new Map();
|
|
223
|
+
this.grantedScopes = new Map();
|
|
224
|
+
this.initialized = false;
|
|
225
|
+
this.configuredProviders = new Set();
|
|
226
|
+
this.configError = null;
|
|
227
|
+
this.userProfiles = new Map();
|
|
228
|
+
this.testCredentials = {};
|
|
229
|
+
this.pendingReconnect = new Set();
|
|
230
|
+
this.tokenManager = new TokenManager();
|
|
231
|
+
this.disconnect = flow(function* (serviceId) {
|
|
232
|
+
const service = serviceRegistry.get(serviceId);
|
|
233
|
+
this.tokenManager.removeToken(serviceId);
|
|
234
|
+
this.connections.set(serviceId, {
|
|
235
|
+
serviceId,
|
|
236
|
+
status: "disconnected",
|
|
237
|
+
scopes: []
|
|
238
|
+
});
|
|
239
|
+
if (service) this.grantedScopes.delete(service.authProvider);
|
|
240
|
+
this.userProfiles.delete(serviceId);
|
|
241
|
+
this.persistToStorage();
|
|
242
|
+
if (this.authClient) {
|
|
243
|
+
if (service) try {
|
|
244
|
+
console.info(`[AnyMux] disconnect(${serviceId}): revoking provider ${service.authProvider}…`);
|
|
245
|
+
yield this.authClient.revokeProvider(service.authProvider);
|
|
246
|
+
console.info(`[AnyMux] disconnect(${serviceId}): revoke succeeded`);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
console.error("[AnyMux] revokeProvider FAILED:", err instanceof Error ? err.message : err);
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
yield this.authClient.signOut();
|
|
252
|
+
console.info(`[AnyMux] disconnect(${serviceId}): signOut succeeded`);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.warn("[AnyMux] signOut failed:", err instanceof Error ? err.message : err);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
this.authClient = options?.authClient ?? null;
|
|
259
|
+
makeAutoObservable(this);
|
|
260
|
+
this.loadFromStorage();
|
|
261
|
+
}
|
|
262
|
+
async initialize() {
|
|
263
|
+
if (this.initialized) return;
|
|
264
|
+
if (this.authClient) {
|
|
265
|
+
const pendingServiceId = this.authClient.getPendingServiceId();
|
|
266
|
+
if (pendingServiceId) await this.handleOAuthReturn(pendingServiceId);
|
|
267
|
+
}
|
|
268
|
+
for (const service of serviceRegistry.getAll()) if (!this.connections.has(service.id)) this.connections.set(service.id, {
|
|
269
|
+
serviceId: service.id,
|
|
270
|
+
status: "loading",
|
|
271
|
+
scopes: []
|
|
272
|
+
});
|
|
273
|
+
if (this.authClient) {
|
|
274
|
+
try {
|
|
275
|
+
const config = await this.authClient.fetchConfiguredProviders();
|
|
276
|
+
runInAction(() => {
|
|
277
|
+
if (!config.database) {
|
|
278
|
+
this.configError = "Database not configured. Set DATABASE_URL in .env.local";
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!config.authSecret) {
|
|
282
|
+
this.configError = "Auth secret not configured. Set BETTER_AUTH_SECRET in .env.local";
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
for (const [provider, configured] of Object.entries(config.providers)) if (configured) this.configuredProviders.add(provider);
|
|
286
|
+
for (const service of serviceRegistry.getAll()) {
|
|
287
|
+
if (service.authProvider === "s3" || service.authProvider === "webdav" || service.authProvider === "gitea" || service.authProvider === "browser-fs" || service.authProvider === "indexeddb") continue;
|
|
288
|
+
if (!this.configuredProviders.has(service.authProvider) && !this.isConnected(service.id)) this.connections.set(service.id, {
|
|
289
|
+
serviceId: service.id,
|
|
290
|
+
status: "not_configured",
|
|
291
|
+
scopes: []
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
} catch (err) {
|
|
296
|
+
console.warn("[AnyMux] Failed to fetch configured providers:", err instanceof Error ? err.message : err);
|
|
297
|
+
runInAction(() => {
|
|
298
|
+
this.configError = "Failed to reach server. Check your connection.";
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
const scopes = await this.authClient.fetchGrantedScopes();
|
|
303
|
+
runInAction(() => {
|
|
304
|
+
for (const [provider, scopeList] of Object.entries(scopes)) this.grantedScopes.set(provider, scopeList);
|
|
305
|
+
});
|
|
306
|
+
} catch (err) {
|
|
307
|
+
console.warn("[AnyMux] Failed to fetch granted scopes:", err instanceof Error ? err.message : err);
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const creds = await this.authClient.fetchTestCredentials();
|
|
311
|
+
runInAction(() => {
|
|
312
|
+
this.testCredentials = creds;
|
|
313
|
+
});
|
|
314
|
+
} catch (err) {
|
|
315
|
+
console.warn("[AnyMux] Failed to fetch test credentials:", err instanceof Error ? err.message : err);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
runInAction(() => {
|
|
319
|
+
for (const [id, conn] of this.connections) if (conn.status === "loading") this.connections.set(id, {
|
|
320
|
+
...conn,
|
|
321
|
+
status: "disconnected"
|
|
322
|
+
});
|
|
323
|
+
this.initialized = true;
|
|
324
|
+
});
|
|
325
|
+
if (this.authClient) this.fetchAndStoreUserProfiles();
|
|
326
|
+
}
|
|
327
|
+
/** Fetch per-provider profiles from the server and update the userProfiles map */
|
|
328
|
+
async fetchAndStoreUserProfiles() {
|
|
329
|
+
if (!this.authClient) return;
|
|
330
|
+
try {
|
|
331
|
+
const profilesByProvider = await this.authClient.fetchUserProfiles();
|
|
332
|
+
runInAction(() => {
|
|
333
|
+
for (const service of serviceRegistry.getAll()) {
|
|
334
|
+
const providerProfile = profilesByProvider[service.authProvider];
|
|
335
|
+
if (providerProfile && this.isConnected(service.id)) this.userProfiles.set(service.id, {
|
|
336
|
+
...providerProfile,
|
|
337
|
+
provider: service.id
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
this.persistToStorage();
|
|
341
|
+
});
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.warn("[AnyMux] Failed to fetch user profiles:", err instanceof Error ? err.message : err);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async handleOAuthReturn(pendingServiceId) {
|
|
347
|
+
if (!this.authClient) return;
|
|
348
|
+
const service = serviceRegistry.get(pendingServiceId);
|
|
349
|
+
if (service) {
|
|
350
|
+
let token = null;
|
|
351
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
352
|
+
try {
|
|
353
|
+
token = await this.authClient.getAccessToken(service.authProvider);
|
|
354
|
+
if (token) break;
|
|
355
|
+
} catch {}
|
|
356
|
+
if (attempt < 3) await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
357
|
+
}
|
|
358
|
+
if (token) {
|
|
359
|
+
let fallbackProfile;
|
|
360
|
+
try {
|
|
361
|
+
const session = await this.authClient.getSession();
|
|
362
|
+
if (session) {
|
|
363
|
+
const profile = {
|
|
364
|
+
id: session.user.id,
|
|
365
|
+
name: session.user.name,
|
|
366
|
+
provider: pendingServiceId
|
|
367
|
+
};
|
|
368
|
+
if (session.user.image) profile.avatarUrl = session.user.image;
|
|
369
|
+
fallbackProfile = profile;
|
|
370
|
+
}
|
|
371
|
+
} catch (err) {
|
|
372
|
+
console.warn(`[AnyMux] Failed to fetch session for ${pendingServiceId}:`, err instanceof Error ? err.message : err);
|
|
373
|
+
}
|
|
374
|
+
runInAction(() => {
|
|
375
|
+
this.tokenManager.setToken(pendingServiceId, token);
|
|
376
|
+
this.connections.set(pendingServiceId, {
|
|
377
|
+
serviceId: pendingServiceId,
|
|
378
|
+
status: "connected",
|
|
379
|
+
accessToken: token,
|
|
380
|
+
scopes: Object.values(service.scopes).flat(),
|
|
381
|
+
connectedAt: new Date()
|
|
382
|
+
});
|
|
383
|
+
if (fallbackProfile) this.userProfiles.set(pendingServiceId, fallbackProfile);
|
|
384
|
+
this.persistToStorage();
|
|
385
|
+
});
|
|
386
|
+
} else try {
|
|
387
|
+
const session = await this.authClient.getSession();
|
|
388
|
+
if (session) console.warn(`[AnyMux] OAuth for ${pendingServiceId}: session exists but getAccessToken returned null after 4 attempts`);
|
|
389
|
+
} catch {}
|
|
390
|
+
}
|
|
391
|
+
this.authClient.clearPendingServiceId();
|
|
392
|
+
}
|
|
393
|
+
loadFromStorage() {
|
|
394
|
+
try {
|
|
395
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
396
|
+
if (stored) {
|
|
397
|
+
const data = JSON.parse(stored);
|
|
398
|
+
for (const [key, value] of data) {
|
|
399
|
+
const token = this.tokenManager.getToken(key);
|
|
400
|
+
if (token) this.connections.set(key, {
|
|
401
|
+
...value,
|
|
402
|
+
accessToken: token
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const storedProfiles = localStorage.getItem(USER_PROFILES_STORAGE_KEY);
|
|
407
|
+
if (storedProfiles) {
|
|
408
|
+
const entries = JSON.parse(storedProfiles);
|
|
409
|
+
for (const [key, value] of entries) this.userProfiles.set(key, value);
|
|
410
|
+
}
|
|
411
|
+
} catch {}
|
|
412
|
+
}
|
|
413
|
+
persistToStorage() {
|
|
414
|
+
try {
|
|
415
|
+
const data = Array.from(this.connections.entries()).map(([key, conn]) => [key, {
|
|
416
|
+
...conn,
|
|
417
|
+
accessToken: void 0
|
|
418
|
+
}]);
|
|
419
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
|
420
|
+
const profileData = Array.from(this.userProfiles.entries());
|
|
421
|
+
localStorage.setItem(USER_PROFILES_STORAGE_KEY, JSON.stringify(profileData));
|
|
422
|
+
} catch {}
|
|
423
|
+
}
|
|
424
|
+
async connect(serviceId) {
|
|
425
|
+
const service = serviceRegistry.get(serviceId);
|
|
426
|
+
if (!service) return;
|
|
427
|
+
if (!this.authClient) {
|
|
428
|
+
this.connections.set(serviceId, {
|
|
429
|
+
serviceId,
|
|
430
|
+
status: "error",
|
|
431
|
+
scopes: []
|
|
432
|
+
});
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
this.connections.set(serviceId, {
|
|
436
|
+
serviceId,
|
|
437
|
+
status: "connecting",
|
|
438
|
+
scopes: []
|
|
439
|
+
});
|
|
440
|
+
await this.authClient.signIn(service.authProvider, serviceId);
|
|
441
|
+
}
|
|
442
|
+
connectWithCredentials(serviceId, credentialToken) {
|
|
443
|
+
const service = serviceRegistry.get(serviceId);
|
|
444
|
+
if (!service) return;
|
|
445
|
+
this.tokenManager.setToken(serviceId, credentialToken);
|
|
446
|
+
this.connections.set(serviceId, {
|
|
447
|
+
serviceId,
|
|
448
|
+
status: "connected",
|
|
449
|
+
accessToken: credentialToken,
|
|
450
|
+
scopes: Object.values(service.scopes).flat(),
|
|
451
|
+
connectedAt: new Date()
|
|
452
|
+
});
|
|
453
|
+
this.persistToStorage();
|
|
454
|
+
}
|
|
455
|
+
requestReconnect(serviceId) {
|
|
456
|
+
this.pendingReconnect.add(serviceId);
|
|
457
|
+
}
|
|
458
|
+
clearReconnectRequest(serviceId) {
|
|
459
|
+
this.pendingReconnect.delete(serviceId);
|
|
460
|
+
}
|
|
461
|
+
isConnected(serviceId) {
|
|
462
|
+
return this.connections.get(serviceId)?.status === "connected";
|
|
463
|
+
}
|
|
464
|
+
getStatus(serviceId) {
|
|
465
|
+
return this.connections.get(serviceId)?.status ?? "disconnected";
|
|
466
|
+
}
|
|
467
|
+
getToken(serviceId) {
|
|
468
|
+
return this.tokenManager.getToken(serviceId);
|
|
469
|
+
}
|
|
470
|
+
/** Refresh OAuth token from better-auth. Returns fresh token or null. */
|
|
471
|
+
async refreshToken(serviceId) {
|
|
472
|
+
const service = serviceRegistry.get(serviceId);
|
|
473
|
+
if (!service || !this.authClient) {
|
|
474
|
+
console.warn(`[AnyMux] refreshToken(${serviceId}): no service or authClient`);
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
if ([
|
|
478
|
+
"s3",
|
|
479
|
+
"webdav",
|
|
480
|
+
"gitea",
|
|
481
|
+
"icloud",
|
|
482
|
+
"browser-fs",
|
|
483
|
+
"indexeddb"
|
|
484
|
+
].includes(service.authProvider)) return this.tokenManager.getToken(serviceId);
|
|
485
|
+
const oldToken = this.tokenManager.getToken(serviceId);
|
|
486
|
+
try {
|
|
487
|
+
const token = await this.authClient.getAccessToken(service.authProvider);
|
|
488
|
+
if (token) {
|
|
489
|
+
const changed = token !== oldToken;
|
|
490
|
+
console.info(`[AnyMux] refreshToken(${serviceId}): got token (${token.slice(0, 8)}…${token.slice(-4)}), changed=${changed}`);
|
|
491
|
+
this.tokenManager.setToken(serviceId, token);
|
|
492
|
+
const conn = this.connections.get(serviceId);
|
|
493
|
+
if (conn) this.connections.set(serviceId, {
|
|
494
|
+
...conn,
|
|
495
|
+
accessToken: token
|
|
496
|
+
});
|
|
497
|
+
return token;
|
|
498
|
+
}
|
|
499
|
+
console.warn(`[AnyMux] refreshToken(${serviceId}): getAccessToken returned null`);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
console.warn(`[AnyMux] refreshToken(${serviceId}): error:`, err instanceof Error ? err.message : err);
|
|
502
|
+
}
|
|
503
|
+
console.warn(`[AnyMux] refreshToken(${serviceId}): falling back to stored token (${oldToken ? oldToken.slice(0, 8) + "…" : "null"})`);
|
|
504
|
+
return oldToken;
|
|
505
|
+
}
|
|
506
|
+
/** Get the full user profile for a connected service */
|
|
507
|
+
getUserProfile(serviceId) {
|
|
508
|
+
return this.userProfiles.get(serviceId);
|
|
509
|
+
}
|
|
510
|
+
/** @deprecated Use getUserProfile() instead. Kept for backward compat. */
|
|
511
|
+
getUserInfo(serviceId) {
|
|
512
|
+
const profile = this.userProfiles.get(serviceId);
|
|
513
|
+
if (!profile) return null;
|
|
514
|
+
return {
|
|
515
|
+
name: profile.name,
|
|
516
|
+
...profile.avatarUrl ? { image: profile.avatarUrl } : {}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
hasCapabilityScopes(serviceId, capabilityId) {
|
|
520
|
+
const service = serviceRegistry.get(serviceId);
|
|
521
|
+
if (!service) return false;
|
|
522
|
+
if (service.authProvider === "s3" || service.authProvider === "webdav" || service.authProvider === "gitea" || service.authProvider === "browser-fs" || service.authProvider === "indexeddb") return true;
|
|
523
|
+
const requiredScopes = service.scopes[capabilityId];
|
|
524
|
+
if (!requiredScopes || requiredScopes.length === 0) return false;
|
|
525
|
+
if (!this.grantedScopes.has(service.authProvider)) return true;
|
|
526
|
+
const granted = this.grantedScopes.get(service.authProvider);
|
|
527
|
+
return requiredScopes.every((s) => granted.includes(s));
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/models/ActionNotificationModel.ts
|
|
533
|
+
let nextId = 1;
|
|
534
|
+
/**
|
|
535
|
+
* Tracks user actions for toast notifications with undo support.
|
|
536
|
+
* Uses sonner's toast() externally — this model just manages the action log.
|
|
537
|
+
*/
|
|
538
|
+
var ActionNotificationModel = class {
|
|
539
|
+
constructor() {
|
|
540
|
+
this.actions = [];
|
|
541
|
+
this.maxHistory = 100;
|
|
542
|
+
makeAutoObservable(this, {});
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Record an action. Returns the action ID for reference.
|
|
546
|
+
* The caller is responsible for showing the toast via sonner's toast().
|
|
547
|
+
*/
|
|
548
|
+
record(type, description, undo) {
|
|
549
|
+
const id = `action-${nextId++}`;
|
|
550
|
+
const action = {
|
|
551
|
+
id,
|
|
552
|
+
type,
|
|
553
|
+
description,
|
|
554
|
+
timestamp: new Date(),
|
|
555
|
+
undo,
|
|
556
|
+
undone: false
|
|
557
|
+
};
|
|
558
|
+
this.actions.unshift(action);
|
|
559
|
+
if (this.actions.length > this.maxHistory) this.actions = this.actions.slice(0, this.maxHistory);
|
|
560
|
+
return id;
|
|
561
|
+
}
|
|
562
|
+
/** Mark an action as undone */
|
|
563
|
+
markUndone(id) {
|
|
564
|
+
const action = this.actions.find((a) => a.id === id);
|
|
565
|
+
if (action) action.undone = true;
|
|
566
|
+
}
|
|
567
|
+
get recentActions() {
|
|
568
|
+
return this.actions.slice(0, 20);
|
|
569
|
+
}
|
|
570
|
+
get undoableActions() {
|
|
571
|
+
return this.actions.filter((a) => a.undo && !a.undone);
|
|
572
|
+
}
|
|
573
|
+
clear() {
|
|
574
|
+
this.actions = [];
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/models/DashboardModel.ts
|
|
580
|
+
const REPO_STORAGE_KEY = "anymux-selected-repos";
|
|
581
|
+
var DashboardModel = class {
|
|
582
|
+
constructor(connectionManager) {
|
|
583
|
+
this.selectedCell = null;
|
|
584
|
+
this.panelOpen = false;
|
|
585
|
+
this.selectedRepos = {};
|
|
586
|
+
this.browserPath = "/";
|
|
587
|
+
this.gitBrowserState = null;
|
|
588
|
+
this.actionNotifications = new ActionNotificationModel();
|
|
589
|
+
this.connectionManager = connectionManager;
|
|
590
|
+
makeAutoObservable(this, {
|
|
591
|
+
onCellChange: false,
|
|
592
|
+
onPathChange: false
|
|
593
|
+
});
|
|
594
|
+
try {
|
|
595
|
+
const stored = localStorage.getItem(REPO_STORAGE_KEY);
|
|
596
|
+
if (stored) this.selectedRepos = JSON.parse(stored);
|
|
597
|
+
const oldGithub = localStorage.getItem("anymux-github-repo");
|
|
598
|
+
if (oldGithub && !this.selectedRepos["github"]) {
|
|
599
|
+
this.selectedRepos["github"] = JSON.parse(oldGithub);
|
|
600
|
+
this.persistRepos();
|
|
601
|
+
localStorage.removeItem("anymux-github-repo");
|
|
602
|
+
}
|
|
603
|
+
} catch {}
|
|
604
|
+
}
|
|
605
|
+
/** @deprecated Use getSelectedRepo('github') instead */
|
|
606
|
+
get githubRepo() {
|
|
607
|
+
return this.selectedRepos["github"] ?? null;
|
|
608
|
+
}
|
|
609
|
+
/** @deprecated Use setSelectedRepo('github', repo) instead */
|
|
610
|
+
setGitHubRepo(repo) {
|
|
611
|
+
this.setSelectedRepo("github", repo);
|
|
612
|
+
}
|
|
613
|
+
/** @deprecated Use clearSelectedRepo('github') instead */
|
|
614
|
+
clearGitHubRepo() {
|
|
615
|
+
this.clearSelectedRepo("github");
|
|
616
|
+
}
|
|
617
|
+
setSelectedRepo(serviceId, repo) {
|
|
618
|
+
this.selectedRepos[serviceId] = repo;
|
|
619
|
+
this.persistRepos();
|
|
620
|
+
}
|
|
621
|
+
clearSelectedRepo(serviceId) {
|
|
622
|
+
delete this.selectedRepos[serviceId];
|
|
623
|
+
this.persistRepos();
|
|
624
|
+
}
|
|
625
|
+
getSelectedRepo(serviceId) {
|
|
626
|
+
return this.selectedRepos[serviceId] ?? null;
|
|
627
|
+
}
|
|
628
|
+
persistRepos() {
|
|
629
|
+
try {
|
|
630
|
+
localStorage.setItem(REPO_STORAGE_KEY, JSON.stringify(this.selectedRepos));
|
|
631
|
+
} catch {}
|
|
632
|
+
}
|
|
633
|
+
selectCell(serviceId, capabilityId) {
|
|
634
|
+
const service = serviceRegistry.get(serviceId);
|
|
635
|
+
if (!service) return;
|
|
636
|
+
const capability = service.capabilities.find((c) => c.id === capabilityId);
|
|
637
|
+
if (!capability?.supported) return;
|
|
638
|
+
if (capabilityId === "git-repo" || capabilityId === "git-host" || capabilityId === "file-system") this.clearSelectedRepo(serviceId);
|
|
639
|
+
if (!this.connectionManager.isConnected(serviceId)) return;
|
|
640
|
+
this.selectedCell = {
|
|
641
|
+
serviceId,
|
|
642
|
+
capabilityId
|
|
643
|
+
};
|
|
644
|
+
this.panelOpen = true;
|
|
645
|
+
this.onCellChange?.({
|
|
646
|
+
serviceId,
|
|
647
|
+
capabilityId
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
/** Open a cell without checking connection/capability (used for URL restore) */
|
|
651
|
+
openCell(serviceId, capabilityId) {
|
|
652
|
+
if (capabilityId === "git-repo" || capabilityId === "git-host" || capabilityId === "file-system") this.clearSelectedRepo(serviceId);
|
|
653
|
+
this.selectedCell = {
|
|
654
|
+
serviceId,
|
|
655
|
+
capabilityId
|
|
656
|
+
};
|
|
657
|
+
this.panelOpen = true;
|
|
658
|
+
}
|
|
659
|
+
setBrowserPath(path) {
|
|
660
|
+
this.browserPath = path;
|
|
661
|
+
this.onPathChange?.(path);
|
|
662
|
+
}
|
|
663
|
+
setGitBrowserState(state) {
|
|
664
|
+
this.gitBrowserState = state;
|
|
665
|
+
}
|
|
666
|
+
closePanel() {
|
|
667
|
+
this.selectedCell = null;
|
|
668
|
+
this.panelOpen = false;
|
|
669
|
+
this.browserPath = "/";
|
|
670
|
+
this.gitBrowserState = null;
|
|
671
|
+
this.onCellChange?.(null);
|
|
672
|
+
}
|
|
673
|
+
/** Close panel without triggering onCellChange (used for URL-driven state sync, e.g. browser back) */
|
|
674
|
+
closePanelSilent() {
|
|
675
|
+
this.selectedCell = null;
|
|
676
|
+
this.panelOpen = false;
|
|
677
|
+
this.browserPath = "/";
|
|
678
|
+
this.gitBrowserState = null;
|
|
679
|
+
}
|
|
680
|
+
/** Set browser path without triggering onPathChange (used for URL-driven state sync) */
|
|
681
|
+
setBrowserPathSilent(path) {
|
|
682
|
+
this.browserPath = path;
|
|
683
|
+
}
|
|
684
|
+
get selectedService() {
|
|
685
|
+
if (!this.selectedCell) return null;
|
|
686
|
+
return serviceRegistry.get(this.selectedCell.serviceId) ?? null;
|
|
687
|
+
}
|
|
688
|
+
get selectedCapability() {
|
|
689
|
+
if (!this.selectedCell || !this.selectedService) return null;
|
|
690
|
+
return this.selectedService.capabilities.find((c) => c.id === this.selectedCell.capabilityId) ?? null;
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
//#endregion
|
|
695
|
+
//#region src/models/CredentialFormModel.ts
|
|
696
|
+
var CredentialFormModel = class {
|
|
697
|
+
constructor() {
|
|
698
|
+
this.open = false;
|
|
699
|
+
this.serviceType = "s3";
|
|
700
|
+
this.accessKeyId = "";
|
|
701
|
+
this.secretAccessKey = "";
|
|
702
|
+
this.region = "us-east-1";
|
|
703
|
+
this.bucket = "";
|
|
704
|
+
this.endpoint = "";
|
|
705
|
+
this.url = "";
|
|
706
|
+
this.username = "";
|
|
707
|
+
this.password = "";
|
|
708
|
+
this.token = "";
|
|
709
|
+
this.owner = "";
|
|
710
|
+
this.repo = "";
|
|
711
|
+
this.email = "";
|
|
712
|
+
this.appPassword = "";
|
|
713
|
+
makeAutoObservable(this);
|
|
714
|
+
}
|
|
715
|
+
openForm(serviceType, prefill) {
|
|
716
|
+
this.serviceType = serviceType;
|
|
717
|
+
this.resetFields();
|
|
718
|
+
if (prefill) this.applyPrefill(prefill);
|
|
719
|
+
this.open = true;
|
|
720
|
+
}
|
|
721
|
+
closeForm() {
|
|
722
|
+
this.open = false;
|
|
723
|
+
}
|
|
724
|
+
setField(field, value) {
|
|
725
|
+
if (field in this) this[field] = value;
|
|
726
|
+
}
|
|
727
|
+
/** Serialize current form state to JSON credential string */
|
|
728
|
+
serialize() {
|
|
729
|
+
switch (this.serviceType) {
|
|
730
|
+
case "s3": {
|
|
731
|
+
const creds = {
|
|
732
|
+
accessKeyId: this.accessKeyId,
|
|
733
|
+
secretAccessKey: this.secretAccessKey,
|
|
734
|
+
region: this.region,
|
|
735
|
+
bucket: this.bucket
|
|
736
|
+
};
|
|
737
|
+
if (this.endpoint) creds.endpoint = this.endpoint;
|
|
738
|
+
return JSON.stringify(creds);
|
|
739
|
+
}
|
|
740
|
+
case "webdav": return JSON.stringify({
|
|
741
|
+
url: this.url,
|
|
742
|
+
username: this.username,
|
|
743
|
+
password: this.password
|
|
744
|
+
});
|
|
745
|
+
case "gitea": return JSON.stringify({
|
|
746
|
+
url: this.url,
|
|
747
|
+
username: this.username,
|
|
748
|
+
password: this.password,
|
|
749
|
+
token: this.token,
|
|
750
|
+
owner: this.owner,
|
|
751
|
+
repo: this.repo
|
|
752
|
+
});
|
|
753
|
+
case "icloud": return JSON.stringify({
|
|
754
|
+
email: this.email,
|
|
755
|
+
appPassword: this.appPassword
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
resetFields() {
|
|
760
|
+
this.accessKeyId = "";
|
|
761
|
+
this.secretAccessKey = "";
|
|
762
|
+
this.region = "us-east-1";
|
|
763
|
+
this.bucket = "";
|
|
764
|
+
this.endpoint = "";
|
|
765
|
+
this.url = "";
|
|
766
|
+
this.username = "";
|
|
767
|
+
this.password = "";
|
|
768
|
+
this.token = "";
|
|
769
|
+
this.owner = "";
|
|
770
|
+
this.repo = "";
|
|
771
|
+
this.email = "";
|
|
772
|
+
this.appPassword = "";
|
|
773
|
+
}
|
|
774
|
+
applyPrefill(values) {
|
|
775
|
+
for (const [k, v] of Object.entries(values)) if (typeof v === "string" && k in this) this[k] = v;
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/components/ServiceIcon.tsx
|
|
781
|
+
/** Custom icons for services not available in lucide-react */
|
|
782
|
+
const AppleIcon = ({ size = 24, color = "currentColor", strokeWidth: _sw,...props }) => /* @__PURE__ */ jsx("svg", {
|
|
783
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
784
|
+
width: size,
|
|
785
|
+
height: size,
|
|
786
|
+
viewBox: "0 0 24 24",
|
|
787
|
+
fill: color,
|
|
788
|
+
...props,
|
|
789
|
+
children: /* @__PURE__ */ jsx("path", { d: "M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" })
|
|
790
|
+
});
|
|
791
|
+
const customIcons = { Apple: AppleIcon };
|
|
792
|
+
const ServiceIcon = ({ name,...props }) => {
|
|
793
|
+
const CustomIcon = customIcons[name];
|
|
794
|
+
if (CustomIcon) return /* @__PURE__ */ jsx(CustomIcon, { ...props });
|
|
795
|
+
const Icon = icons[name];
|
|
796
|
+
if (!Icon) return /* @__PURE__ */ jsx(icons.HelpCircle, { ...props });
|
|
797
|
+
return /* @__PURE__ */ jsx(Icon, { ...props });
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
//#endregion
|
|
801
|
+
//#region src/types/connection-state.ts
|
|
802
|
+
function getServiceConnectionState(connectionManager, serviceId) {
|
|
803
|
+
const status = connectionManager.getStatus(serviceId);
|
|
804
|
+
return match(status).with("loading", () => ({ status: "loading" })).with("not_configured", () => ({ status: "not_configured" })).with("disconnected", () => ({ status: "disconnected" })).with("connecting", () => ({ status: "connecting" })).with("connected", () => ({
|
|
805
|
+
status: "connected",
|
|
806
|
+
token: connectionManager.getToken(serviceId) ?? "",
|
|
807
|
+
user: connectionManager.getUserInfo(serviceId),
|
|
808
|
+
profile: connectionManager.getUserProfile(serviceId)
|
|
809
|
+
})).with("error", () => ({ status: "error" })).with("expired", () => ({ status: "expired" })).exhaustive();
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
//#endregion
|
|
813
|
+
//#region src/components/CredentialForm.tsx
|
|
814
|
+
const CredentialForm = observer(({ model, onSubmit }) => {
|
|
815
|
+
if (!model.open) return null;
|
|
816
|
+
const handleSubmit = (e) => {
|
|
817
|
+
e.preventDefault();
|
|
818
|
+
onSubmit(model.serialize());
|
|
819
|
+
};
|
|
820
|
+
const inputClass = "w-full rounded-md border border-border px-3 py-2 text-base sm:text-sm bg-card text-foreground focus:border-ring focus:outline-none focus:ring-1 focus:ring-ring";
|
|
821
|
+
const labelClass = "block text-sm font-medium text-foreground mb-1";
|
|
822
|
+
const title = model.serviceType === "s3" ? "S3 Credentials" : model.serviceType === "webdav" ? "WebDAV Credentials" : model.serviceType === "icloud" ? "iCloud Credentials" : "Gitea Credentials";
|
|
823
|
+
return /* @__PURE__ */ jsx("div", {
|
|
824
|
+
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50",
|
|
825
|
+
onClick: () => model.closeForm(),
|
|
826
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
827
|
+
className: "w-[calc(100%-2rem)] max-w-md max-h-[80vh] flex flex-col rounded-lg bg-card shadow-xl",
|
|
828
|
+
onClick: (e) => e.stopPropagation(),
|
|
829
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
830
|
+
className: "px-4 sm:px-6 pt-4 sm:pt-6 pb-3 text-lg font-semibold text-foreground flex-shrink-0",
|
|
831
|
+
children: title
|
|
832
|
+
}), /* @__PURE__ */ jsxs("form", {
|
|
833
|
+
onSubmit: handleSubmit,
|
|
834
|
+
className: "flex flex-col flex-1 min-h-0",
|
|
835
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
836
|
+
className: "flex-1 overflow-y-auto px-4 sm:px-6 space-y-3",
|
|
837
|
+
children: model.serviceType === "s3" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
838
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
839
|
+
className: labelClass,
|
|
840
|
+
children: "Access Key ID"
|
|
841
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
842
|
+
type: "text",
|
|
843
|
+
required: true,
|
|
844
|
+
className: inputClass,
|
|
845
|
+
value: model.accessKeyId,
|
|
846
|
+
onChange: (e) => model.setField("accessKeyId", e.target.value)
|
|
847
|
+
})] }),
|
|
848
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
849
|
+
className: labelClass,
|
|
850
|
+
children: "Secret Access Key"
|
|
851
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
852
|
+
type: "password",
|
|
853
|
+
required: true,
|
|
854
|
+
className: inputClass,
|
|
855
|
+
value: model.secretAccessKey,
|
|
856
|
+
onChange: (e) => model.setField("secretAccessKey", e.target.value)
|
|
857
|
+
})] }),
|
|
858
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
859
|
+
className: labelClass,
|
|
860
|
+
children: "Region"
|
|
861
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
862
|
+
type: "text",
|
|
863
|
+
required: true,
|
|
864
|
+
className: inputClass,
|
|
865
|
+
value: model.region,
|
|
866
|
+
onChange: (e) => model.setField("region", e.target.value)
|
|
867
|
+
})] }),
|
|
868
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
869
|
+
className: labelClass,
|
|
870
|
+
children: "Bucket"
|
|
871
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
872
|
+
type: "text",
|
|
873
|
+
required: true,
|
|
874
|
+
className: inputClass,
|
|
875
|
+
value: model.bucket,
|
|
876
|
+
onChange: (e) => model.setField("bucket", e.target.value)
|
|
877
|
+
})] }),
|
|
878
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
879
|
+
className: labelClass,
|
|
880
|
+
children: "Endpoint (optional)"
|
|
881
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
882
|
+
type: "text",
|
|
883
|
+
className: inputClass,
|
|
884
|
+
value: model.endpoint,
|
|
885
|
+
onChange: (e) => model.setField("endpoint", e.target.value),
|
|
886
|
+
placeholder: "https://s3.amazonaws.com"
|
|
887
|
+
})] })
|
|
888
|
+
] }) : model.serviceType === "webdav" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
889
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
890
|
+
className: labelClass,
|
|
891
|
+
children: "URL"
|
|
892
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
893
|
+
type: "url",
|
|
894
|
+
required: true,
|
|
895
|
+
className: inputClass,
|
|
896
|
+
value: model.url,
|
|
897
|
+
onChange: (e) => model.setField("url", e.target.value),
|
|
898
|
+
placeholder: "https://example.com/webdav"
|
|
899
|
+
})] }),
|
|
900
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
901
|
+
className: labelClass,
|
|
902
|
+
children: "Username"
|
|
903
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
904
|
+
type: "text",
|
|
905
|
+
required: true,
|
|
906
|
+
className: inputClass,
|
|
907
|
+
value: model.username,
|
|
908
|
+
onChange: (e) => model.setField("username", e.target.value)
|
|
909
|
+
})] }),
|
|
910
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
911
|
+
className: labelClass,
|
|
912
|
+
children: "Password"
|
|
913
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
914
|
+
type: "password",
|
|
915
|
+
required: true,
|
|
916
|
+
className: inputClass,
|
|
917
|
+
value: model.password,
|
|
918
|
+
onChange: (e) => model.setField("password", e.target.value)
|
|
919
|
+
})] })
|
|
920
|
+
] }) : model.serviceType === "icloud" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
921
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
922
|
+
className: labelClass,
|
|
923
|
+
children: "Apple ID Email"
|
|
924
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
925
|
+
type: "email",
|
|
926
|
+
required: true,
|
|
927
|
+
className: inputClass,
|
|
928
|
+
value: model.email,
|
|
929
|
+
onChange: (e) => model.setField("email", e.target.value),
|
|
930
|
+
placeholder: "you@icloud.com"
|
|
931
|
+
})] }),
|
|
932
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
933
|
+
className: labelClass,
|
|
934
|
+
children: "App-Specific Password"
|
|
935
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
936
|
+
type: "password",
|
|
937
|
+
required: true,
|
|
938
|
+
className: inputClass,
|
|
939
|
+
value: model.appPassword,
|
|
940
|
+
onChange: (e) => model.setField("appPassword", e.target.value),
|
|
941
|
+
placeholder: "xxxx-xxxx-xxxx-xxxx"
|
|
942
|
+
})] }),
|
|
943
|
+
/* @__PURE__ */ jsxs("p", {
|
|
944
|
+
className: "text-xs text-muted-foreground",
|
|
945
|
+
children: [
|
|
946
|
+
"Generate an app-specific password at",
|
|
947
|
+
" ",
|
|
948
|
+
/* @__PURE__ */ jsx("a", {
|
|
949
|
+
href: "https://appleid.apple.com/account/manage",
|
|
950
|
+
target: "_blank",
|
|
951
|
+
rel: "noopener noreferrer",
|
|
952
|
+
className: "underline text-primary hover:text-primary/80",
|
|
953
|
+
children: "appleid.apple.com"
|
|
954
|
+
}),
|
|
955
|
+
" ",
|
|
956
|
+
"under Sign-In and Security → App-Specific Passwords."
|
|
957
|
+
]
|
|
958
|
+
})
|
|
959
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
960
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
961
|
+
className: labelClass,
|
|
962
|
+
children: "URL"
|
|
963
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
964
|
+
type: "url",
|
|
965
|
+
required: true,
|
|
966
|
+
className: inputClass,
|
|
967
|
+
value: model.url,
|
|
968
|
+
onChange: (e) => model.setField("url", e.target.value),
|
|
969
|
+
placeholder: "https://gitea.example.com"
|
|
970
|
+
})] }),
|
|
971
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
972
|
+
className: labelClass,
|
|
973
|
+
children: "Token (or use username/password below)"
|
|
974
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
975
|
+
type: "password",
|
|
976
|
+
className: inputClass,
|
|
977
|
+
value: model.token,
|
|
978
|
+
onChange: (e) => model.setField("token", e.target.value),
|
|
979
|
+
placeholder: "API token"
|
|
980
|
+
})] }),
|
|
981
|
+
!model.token && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
982
|
+
className: labelClass,
|
|
983
|
+
children: "Username"
|
|
984
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
985
|
+
type: "text",
|
|
986
|
+
className: inputClass,
|
|
987
|
+
value: model.username,
|
|
988
|
+
onChange: (e) => model.setField("username", e.target.value)
|
|
989
|
+
})] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
990
|
+
className: labelClass,
|
|
991
|
+
children: "Password"
|
|
992
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
993
|
+
type: "password",
|
|
994
|
+
className: inputClass,
|
|
995
|
+
value: model.password,
|
|
996
|
+
onChange: (e) => model.setField("password", e.target.value)
|
|
997
|
+
})] })] }),
|
|
998
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
999
|
+
className: labelClass,
|
|
1000
|
+
children: "Owner"
|
|
1001
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
1002
|
+
type: "text",
|
|
1003
|
+
required: true,
|
|
1004
|
+
className: inputClass,
|
|
1005
|
+
value: model.owner,
|
|
1006
|
+
onChange: (e) => model.setField("owner", e.target.value)
|
|
1007
|
+
})] }),
|
|
1008
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
|
|
1009
|
+
className: labelClass,
|
|
1010
|
+
children: "Repo"
|
|
1011
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
1012
|
+
type: "text",
|
|
1013
|
+
required: true,
|
|
1014
|
+
className: inputClass,
|
|
1015
|
+
value: model.repo,
|
|
1016
|
+
onChange: (e) => model.setField("repo", e.target.value)
|
|
1017
|
+
})] })
|
|
1018
|
+
] })
|
|
1019
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1020
|
+
className: "flex justify-end gap-2 px-4 sm:px-6 py-3 border-t border-border flex-shrink-0",
|
|
1021
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1022
|
+
type: "button",
|
|
1023
|
+
onClick: () => model.closeForm(),
|
|
1024
|
+
className: "rounded-md px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-muted",
|
|
1025
|
+
children: "Cancel"
|
|
1026
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1027
|
+
type: "submit",
|
|
1028
|
+
className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90",
|
|
1029
|
+
children: "Connect"
|
|
1030
|
+
})]
|
|
1031
|
+
})]
|
|
1032
|
+
})]
|
|
1033
|
+
})
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
//#endregion
|
|
1038
|
+
//#region src/components/ConnectedMenu.tsx
|
|
1039
|
+
function formatConnectedDate(date) {
|
|
1040
|
+
if (!date) return null;
|
|
1041
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
1042
|
+
if (isNaN(d.getTime())) return null;
|
|
1043
|
+
return d.toLocaleDateString(void 0, {
|
|
1044
|
+
month: "short",
|
|
1045
|
+
day: "numeric",
|
|
1046
|
+
year: "numeric"
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
function ConnectedMenu({ service, connectionManager, user, profile, isOAuth }) {
|
|
1050
|
+
const displayName = profile?.name ?? user?.name;
|
|
1051
|
+
const avatarUrl = profile?.avatarUrl ?? user?.image;
|
|
1052
|
+
const profileUrl = profile?.profileUrl;
|
|
1053
|
+
const email = profile?.email;
|
|
1054
|
+
const handleReconnect = async () => {
|
|
1055
|
+
await connectionManager.disconnect(service.id);
|
|
1056
|
+
await connectionManager.connect(service.id);
|
|
1057
|
+
};
|
|
1058
|
+
const handleDisconnect = () => {
|
|
1059
|
+
connectionManager.disconnect(service.id);
|
|
1060
|
+
};
|
|
1061
|
+
const connection = connectionManager.connections.get(service.id);
|
|
1062
|
+
const connectedDate = formatConnectedDate(connection?.connectedAt);
|
|
1063
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1064
|
+
className: "inline-flex items-center gap-1",
|
|
1065
|
+
children: [/* @__PURE__ */ jsx(ConnectionBadge, {
|
|
1066
|
+
status: "connected",
|
|
1067
|
+
...displayName ? { name: displayName } : {},
|
|
1068
|
+
...avatarUrl ? { avatarUrl } : {}
|
|
1069
|
+
}), /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
1070
|
+
asChild: true,
|
|
1071
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
1072
|
+
className: "inline-flex items-center justify-center rounded-md p-1 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors",
|
|
1073
|
+
children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4" })
|
|
1074
|
+
})
|
|
1075
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
1076
|
+
align: "end",
|
|
1077
|
+
className: "min-w-[200px]",
|
|
1078
|
+
children: [
|
|
1079
|
+
/* @__PURE__ */ jsxs(DropdownMenuLabel, {
|
|
1080
|
+
className: "flex items-center gap-2",
|
|
1081
|
+
children: [avatarUrl ? /* @__PURE__ */ jsx("img", {
|
|
1082
|
+
src: avatarUrl,
|
|
1083
|
+
alt: "",
|
|
1084
|
+
className: "h-5 w-5 rounded-full shrink-0"
|
|
1085
|
+
}) : /* @__PURE__ */ jsx(ServiceIcon, {
|
|
1086
|
+
name: service.icon,
|
|
1087
|
+
className: "h-4 w-4 shrink-0",
|
|
1088
|
+
style: { color: service.color }
|
|
1089
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1090
|
+
className: "flex flex-col min-w-0",
|
|
1091
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1092
|
+
className: "truncate",
|
|
1093
|
+
children: displayName ?? service.name
|
|
1094
|
+
}), email && /* @__PURE__ */ jsx("span", {
|
|
1095
|
+
className: "text-xs font-normal text-muted-foreground truncate",
|
|
1096
|
+
children: email
|
|
1097
|
+
})]
|
|
1098
|
+
})]
|
|
1099
|
+
}),
|
|
1100
|
+
connectedDate && /* @__PURE__ */ jsxs("div", {
|
|
1101
|
+
className: "flex items-center gap-2 px-2 pb-1.5 text-xs text-muted-foreground",
|
|
1102
|
+
children: [/* @__PURE__ */ jsx(Clock, { className: "h-3 w-3 shrink-0" }), /* @__PURE__ */ jsxs("span", { children: ["Connected ", connectedDate] })]
|
|
1103
|
+
}),
|
|
1104
|
+
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
1105
|
+
profileUrl && /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
1106
|
+
asChild: true,
|
|
1107
|
+
children: /* @__PURE__ */ jsxs("a", {
|
|
1108
|
+
href: profileUrl,
|
|
1109
|
+
target: "_blank",
|
|
1110
|
+
rel: "noopener noreferrer",
|
|
1111
|
+
children: [/* @__PURE__ */ jsx(ExternalLink, {}), "Open Profile"]
|
|
1112
|
+
})
|
|
1113
|
+
}),
|
|
1114
|
+
service.grantsUrl && /* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
1115
|
+
asChild: true,
|
|
1116
|
+
children: /* @__PURE__ */ jsxs("a", {
|
|
1117
|
+
href: service.grantsUrl,
|
|
1118
|
+
target: "_blank",
|
|
1119
|
+
rel: "noopener noreferrer",
|
|
1120
|
+
children: [/* @__PURE__ */ jsx(Shield, {}), "Manage Permissions"]
|
|
1121
|
+
})
|
|
1122
|
+
}),
|
|
1123
|
+
/* @__PURE__ */ jsx(DropdownMenuItem, {
|
|
1124
|
+
asChild: true,
|
|
1125
|
+
children: /* @__PURE__ */ jsxs("a", {
|
|
1126
|
+
href: `/providers/${service.id}`,
|
|
1127
|
+
children: [
|
|
1128
|
+
/* @__PURE__ */ jsx(Info, {}),
|
|
1129
|
+
"About ",
|
|
1130
|
+
service.name
|
|
1131
|
+
]
|
|
1132
|
+
})
|
|
1133
|
+
}),
|
|
1134
|
+
isOAuth && /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
1135
|
+
onClick: handleReconnect,
|
|
1136
|
+
children: [/* @__PURE__ */ jsx(RefreshCw, {}), "Reconnect"]
|
|
1137
|
+
}),
|
|
1138
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
1139
|
+
variant: "destructive",
|
|
1140
|
+
onClick: handleDisconnect,
|
|
1141
|
+
children: [/* @__PURE__ */ jsx(LogOut, {}), "Disconnect"]
|
|
1142
|
+
})
|
|
1143
|
+
]
|
|
1144
|
+
})] })]
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
//#endregion
|
|
1149
|
+
//#region src/components/ConnectButton.tsx
|
|
1150
|
+
const ConnectButton = observer(({ service, connectionManager }) => {
|
|
1151
|
+
const [formModel] = useState(() => new CredentialFormModel());
|
|
1152
|
+
const state = getServiceConnectionState(connectionManager, service.id);
|
|
1153
|
+
const needsCredentialForm = service.authProvider === "s3" || service.authProvider === "webdav" || service.authProvider === "gitea" || service.authProvider === "icloud";
|
|
1154
|
+
const isBrowserFs = service.authProvider === "browser-fs";
|
|
1155
|
+
const isIndexedDb = service.authProvider === "indexeddb";
|
|
1156
|
+
const pendingReconnect = connectionManager.pendingReconnect.has(service.id);
|
|
1157
|
+
useEffect(() => {
|
|
1158
|
+
if (pendingReconnect) {
|
|
1159
|
+
connectionManager.clearReconnectRequest(service.id);
|
|
1160
|
+
handleConnect();
|
|
1161
|
+
}
|
|
1162
|
+
}, [pendingReconnect]);
|
|
1163
|
+
const handleConnect = async () => {
|
|
1164
|
+
if (isBrowserFs) {
|
|
1165
|
+
try {
|
|
1166
|
+
const { BrowserFileSystemFactory } = await import("@anymux/browser-fs");
|
|
1167
|
+
const factory = new BrowserFileSystemFactory();
|
|
1168
|
+
const [_fs, handleInfo] = await factory.createFromPicker();
|
|
1169
|
+
connectionManager.connectWithCredentials(service.id, JSON.stringify({
|
|
1170
|
+
id: handleInfo.id,
|
|
1171
|
+
name: handleInfo.name
|
|
1172
|
+
}));
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
if (err.name !== "AbortError") console.error("Failed to pick directory:", err);
|
|
1175
|
+
}
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
if (isIndexedDb) {
|
|
1179
|
+
connectionManager.connectWithCredentials(service.id, JSON.stringify({ type: "indexeddb" }));
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
if (needsCredentialForm) formModel.openForm(service.authProvider);
|
|
1183
|
+
else connectionManager.connect(service.id);
|
|
1184
|
+
};
|
|
1185
|
+
const handleCredentialSubmit = (credentialToken) => {
|
|
1186
|
+
connectionManager.connectWithCredentials(service.id, credentialToken);
|
|
1187
|
+
formModel.closeForm();
|
|
1188
|
+
};
|
|
1189
|
+
const credentialFormElement = needsCredentialForm ? /* @__PURE__ */ jsx(CredentialForm, {
|
|
1190
|
+
model: formModel,
|
|
1191
|
+
onSubmit: handleCredentialSubmit
|
|
1192
|
+
}) : null;
|
|
1193
|
+
return match(state).with({ status: "loading" }, () => /* @__PURE__ */ jsx(ConnectionBadge, { status: "loading" })).with({ status: "not_configured" }, () => /* @__PURE__ */ jsx(ConnectionBadge, { status: "not-configured" })).with({ status: "connecting" }, () => /* @__PURE__ */ jsx(ConnectionBadge, { status: "connecting" })).with({ status: "connected" }, (s) => {
|
|
1194
|
+
const isOAuth = !needsCredentialForm && !isBrowserFs && !isIndexedDb;
|
|
1195
|
+
const user = isOAuth ? s.user : null;
|
|
1196
|
+
return /* @__PURE__ */ jsx(ConnectedMenu, {
|
|
1197
|
+
service,
|
|
1198
|
+
connectionManager,
|
|
1199
|
+
user,
|
|
1200
|
+
...s.profile ? { profile: s.profile } : {},
|
|
1201
|
+
isOAuth
|
|
1202
|
+
});
|
|
1203
|
+
}).with({ status: "error" }, () => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
|
|
1204
|
+
onClick: handleConnect,
|
|
1205
|
+
className: "rounded-md px-3 py-1.5 text-xs font-medium text-white bg-destructive hover:bg-destructive/90 transition-colors",
|
|
1206
|
+
children: "Retry"
|
|
1207
|
+
}), credentialFormElement] })).with({ status: "expired" }, () => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
|
|
1208
|
+
onClick: handleConnect,
|
|
1209
|
+
className: "rounded-md px-3 py-1.5 text-xs font-medium text-white bg-destructive hover:bg-destructive/90 transition-colors",
|
|
1210
|
+
children: "Reconnect"
|
|
1211
|
+
}), credentialFormElement] })).with({ status: "disconnected" }, () => {
|
|
1212
|
+
const testCreds = connectionManager.testCredentials[service.authProvider];
|
|
1213
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
1214
|
+
className: "inline-flex items-center gap-1.5",
|
|
1215
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1216
|
+
onClick: handleConnect,
|
|
1217
|
+
className: "rounded-md px-3 py-1.5 text-xs font-medium text-white transition-colors",
|
|
1218
|
+
style: { backgroundColor: service.color },
|
|
1219
|
+
children: "Connect"
|
|
1220
|
+
}), testCreds && /* @__PURE__ */ jsx("button", {
|
|
1221
|
+
onClick: () => {
|
|
1222
|
+
if (needsCredentialForm) {
|
|
1223
|
+
const values = {};
|
|
1224
|
+
for (const [k, v] of Object.entries(testCreds)) if (typeof v === "string") values[k] = v;
|
|
1225
|
+
formModel.openForm(service.authProvider, values);
|
|
1226
|
+
} else connectionManager.connectWithCredentials(service.id, JSON.stringify(testCreds));
|
|
1227
|
+
},
|
|
1228
|
+
className: "rounded-md px-2 py-1.5 text-xs font-medium text-muted-foreground bg-muted hover:bg-muted/80 transition-colors",
|
|
1229
|
+
children: "Quick Test"
|
|
1230
|
+
})]
|
|
1231
|
+
}), credentialFormElement] });
|
|
1232
|
+
}).exhaustive();
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
//#endregion
|
|
1236
|
+
//#region src/components/CapabilityCell.tsx
|
|
1237
|
+
const CapabilityCell = observer(({ service, capabilityId, connectionManager, dashboardModel }) => {
|
|
1238
|
+
const capability = service.capabilities.find((c) => c.id === capabilityId);
|
|
1239
|
+
const gitHostCap = capabilityId === "git-repo" ? service.capabilities.find((c) => c.id === "git-host") : void 0;
|
|
1240
|
+
const supported = (capability?.supported ?? false) || (gitHostCap?.supported ?? false);
|
|
1241
|
+
const effectiveCapabilityId = capabilityId === "git-repo" && !capability?.supported && gitHostCap?.supported ? "git-host" : capabilityId;
|
|
1242
|
+
const status = connectionManager.getStatus(service.id);
|
|
1243
|
+
const isSelected = dashboardModel.selectedCell?.serviceId === service.id && (dashboardModel.selectedCell?.capabilityId === effectiveCapabilityId || capabilityId === "git-repo" && dashboardModel.selectedCell?.capabilityId === "git-host");
|
|
1244
|
+
if (!supported) return /* @__PURE__ */ jsx("td", {
|
|
1245
|
+
className: "px-3 py-2 text-center align-middle",
|
|
1246
|
+
children: /* @__PURE__ */ jsx(Minus, { className: "h-4 w-4 text-muted-foreground/30 mx-auto" })
|
|
1247
|
+
});
|
|
1248
|
+
const handleKeyDown = (e) => {
|
|
1249
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1250
|
+
e.preventDefault();
|
|
1251
|
+
dashboardModel.selectCell(service.id, effectiveCapabilityId);
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
return match(status).with("loading", () => /* @__PURE__ */ jsx("td", {
|
|
1255
|
+
className: "px-3 py-2 text-center align-middle",
|
|
1256
|
+
children: /* @__PURE__ */ jsx("span", { className: "inline-block h-4 w-4 rounded bg-muted animate-pulse mx-auto" })
|
|
1257
|
+
})).with("connected", () => {
|
|
1258
|
+
if (!connectionManager.initialized) return /* @__PURE__ */ jsx("td", {
|
|
1259
|
+
className: "px-3 py-2 text-center align-middle",
|
|
1260
|
+
children: /* @__PURE__ */ jsx("span", { className: "inline-block h-4 w-4 rounded bg-muted animate-pulse mx-auto" })
|
|
1261
|
+
});
|
|
1262
|
+
const hasScopes = connectionManager.hasCapabilityScopes(service.id, effectiveCapabilityId);
|
|
1263
|
+
const hoverBg = hasScopes ? "hover:bg-green-50 dark:hover:bg-green-900/20" : "hover:bg-orange-50 dark:hover:bg-orange-900/20";
|
|
1264
|
+
return /* @__PURE__ */ jsx("td", {
|
|
1265
|
+
className: `px-3 py-2 text-center align-middle cursor-pointer transition-colors ${isSelected ? "bg-blue-100 dark:bg-blue-900/40" : hoverBg}`,
|
|
1266
|
+
role: "button",
|
|
1267
|
+
tabIndex: 0,
|
|
1268
|
+
onClick: () => dashboardModel.selectCell(service.id, effectiveCapabilityId),
|
|
1269
|
+
onKeyDown: handleKeyDown,
|
|
1270
|
+
title: hasScopes ? void 0 : "Scope not granted",
|
|
1271
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1272
|
+
className: `inline-flex items-center justify-center w-5 h-5 rounded-full mx-auto ${hasScopes ? "bg-green-500" : "bg-orange-400"} text-white`,
|
|
1273
|
+
children: /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" })
|
|
1274
|
+
})
|
|
1275
|
+
});
|
|
1276
|
+
}).otherwise(() => /* @__PURE__ */ jsx("td", {
|
|
1277
|
+
className: "px-3 py-2 text-center align-middle",
|
|
1278
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1279
|
+
className: "inline-flex items-center justify-center w-5 h-5 rounded border border-muted-foreground/30 mx-auto",
|
|
1280
|
+
children: /* @__PURE__ */ jsx(Check, { className: "h-3 w-3 text-muted-foreground/50" })
|
|
1281
|
+
})
|
|
1282
|
+
}));
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
//#endregion
|
|
1286
|
+
//#region src/components/ServiceRow.tsx
|
|
1287
|
+
const CAPABILITY_COLUMNS$1 = [
|
|
1288
|
+
"file-system",
|
|
1289
|
+
"object-storage",
|
|
1290
|
+
"git-repo",
|
|
1291
|
+
"media",
|
|
1292
|
+
"contacts",
|
|
1293
|
+
"calendar"
|
|
1294
|
+
];
|
|
1295
|
+
const ServiceRow = observer(({ service, connectionManager, dashboardModel }) => {
|
|
1296
|
+
return /* @__PURE__ */ jsxs("tr", {
|
|
1297
|
+
className: "border-b border-border h-11",
|
|
1298
|
+
children: [
|
|
1299
|
+
/* @__PURE__ */ jsx("td", {
|
|
1300
|
+
className: "px-3 py-2 align-middle",
|
|
1301
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1302
|
+
className: "flex items-center gap-2",
|
|
1303
|
+
children: [/* @__PURE__ */ jsx(ServiceIcon, {
|
|
1304
|
+
name: service.icon,
|
|
1305
|
+
className: "h-4 w-4",
|
|
1306
|
+
style: { color: service.color }
|
|
1307
|
+
}), /* @__PURE__ */ jsx("a", {
|
|
1308
|
+
href: `/providers/${service.id}`,
|
|
1309
|
+
className: "text-sm font-medium truncate hover:underline",
|
|
1310
|
+
title: service.name,
|
|
1311
|
+
children: service.name
|
|
1312
|
+
})]
|
|
1313
|
+
})
|
|
1314
|
+
}),
|
|
1315
|
+
/* @__PURE__ */ jsx("td", {
|
|
1316
|
+
className: "px-3 py-2 align-middle",
|
|
1317
|
+
children: /* @__PURE__ */ jsx(ConnectButton, {
|
|
1318
|
+
service,
|
|
1319
|
+
connectionManager
|
|
1320
|
+
})
|
|
1321
|
+
}),
|
|
1322
|
+
CAPABILITY_COLUMNS$1.map((cap) => /* @__PURE__ */ jsx(CapabilityCell, {
|
|
1323
|
+
service,
|
|
1324
|
+
capabilityId: cap,
|
|
1325
|
+
connectionManager,
|
|
1326
|
+
dashboardModel
|
|
1327
|
+
}, cap))
|
|
1328
|
+
]
|
|
1329
|
+
});
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
//#endregion
|
|
1333
|
+
//#region src/components/CapabilityPill.tsx
|
|
1334
|
+
const CapabilityPill = ({ label, status, isSelected, hasScopes, onSelect, disabled = false }) => {
|
|
1335
|
+
const connected = status === "connected";
|
|
1336
|
+
const loading = status === "loading";
|
|
1337
|
+
const className = `inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium transition-colors ${loading ? "bg-muted text-muted-foreground" : isSelected ? "bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300" : connected ? "bg-muted text-foreground hover:bg-muted/80 cursor-pointer" : "bg-muted/50 text-muted-foreground"}`;
|
|
1338
|
+
const icon = match(status).with("loading", () => /* @__PURE__ */ jsx(LoadingSpinner, {
|
|
1339
|
+
size: "sm",
|
|
1340
|
+
label: "Loading",
|
|
1341
|
+
className: "h-3 w-3"
|
|
1342
|
+
})).with("connected", () => /* @__PURE__ */ jsx(Check, { className: `h-3 w-3 ${hasScopes ? "text-green-500" : "text-orange-400"}` })).otherwise(() => /* @__PURE__ */ jsx(Minus, { className: "h-3 w-3" }));
|
|
1343
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
1344
|
+
onClick: onSelect,
|
|
1345
|
+
disabled: disabled || !connected,
|
|
1346
|
+
className,
|
|
1347
|
+
children: [icon, label]
|
|
1348
|
+
});
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
//#endregion
|
|
1352
|
+
//#region src/components/ServiceCard.tsx
|
|
1353
|
+
const CAPABILITY_COLUMNS = [
|
|
1354
|
+
{
|
|
1355
|
+
id: "file-system",
|
|
1356
|
+
label: "FS"
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
id: "object-storage",
|
|
1360
|
+
label: "Obj"
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
id: "git-repo",
|
|
1364
|
+
label: "Git"
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
id: "media",
|
|
1368
|
+
label: "Media"
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
id: "contacts",
|
|
1372
|
+
label: "Contacts"
|
|
1373
|
+
},
|
|
1374
|
+
{
|
|
1375
|
+
id: "calendar",
|
|
1376
|
+
label: "Cal"
|
|
1377
|
+
}
|
|
1378
|
+
];
|
|
1379
|
+
const ServiceCard = observer(({ service, connectionManager, dashboardModel }) => {
|
|
1380
|
+
const status = connectionManager.getStatus(service.id);
|
|
1381
|
+
const connected = status === "connected";
|
|
1382
|
+
const supportedCaps = CAPABILITY_COLUMNS.filter((cap) => {
|
|
1383
|
+
const supported = service.capabilities.find((c) => c.id === cap.id)?.supported;
|
|
1384
|
+
if (cap.id === "git-repo" && !supported) return service.capabilities.find((c) => c.id === "git-host")?.supported ?? false;
|
|
1385
|
+
return supported;
|
|
1386
|
+
});
|
|
1387
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1388
|
+
className: "rounded-lg border border-border bg-card p-3",
|
|
1389
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1390
|
+
className: "flex items-center justify-between gap-2 mb-2",
|
|
1391
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1392
|
+
className: "flex items-center gap-2 min-w-0",
|
|
1393
|
+
children: [/* @__PURE__ */ jsx(ServiceIcon, {
|
|
1394
|
+
name: service.icon,
|
|
1395
|
+
className: "h-5 w-5 flex-shrink-0",
|
|
1396
|
+
style: { color: service.color }
|
|
1397
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1398
|
+
className: "text-sm font-medium truncate",
|
|
1399
|
+
title: service.name,
|
|
1400
|
+
children: service.name
|
|
1401
|
+
})]
|
|
1402
|
+
}), /* @__PURE__ */ jsx(ConnectButton, {
|
|
1403
|
+
service,
|
|
1404
|
+
connectionManager
|
|
1405
|
+
})]
|
|
1406
|
+
}), supportedCaps.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
1407
|
+
className: "flex flex-wrap gap-1.5",
|
|
1408
|
+
children: supportedCaps.map((cap) => {
|
|
1409
|
+
const isSelected = dashboardModel.selectedCell?.serviceId === service.id && dashboardModel.selectedCell?.capabilityId === cap.id;
|
|
1410
|
+
const hasScopes = connected && connectionManager.hasCapabilityScopes(service.id, cap.id);
|
|
1411
|
+
return /* @__PURE__ */ jsx(CapabilityPill, {
|
|
1412
|
+
label: cap.label,
|
|
1413
|
+
capabilityId: cap.id,
|
|
1414
|
+
status,
|
|
1415
|
+
isSelected,
|
|
1416
|
+
hasScopes,
|
|
1417
|
+
onSelect: () => {
|
|
1418
|
+
if (connected) dashboardModel.selectCell(service.id, cap.id);
|
|
1419
|
+
}
|
|
1420
|
+
}, cap.id);
|
|
1421
|
+
})
|
|
1422
|
+
})]
|
|
1423
|
+
});
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
//#endregion
|
|
1427
|
+
//#region src/adapters/adapter-registry.ts
|
|
1428
|
+
const factories = new Map();
|
|
1429
|
+
const adapterRegistry = {
|
|
1430
|
+
register(serviceId, factory) {
|
|
1431
|
+
factories.set(serviceId, factory);
|
|
1432
|
+
},
|
|
1433
|
+
async create(serviceId, token, capabilityId, context = {}) {
|
|
1434
|
+
const factory = factories.get(serviceId);
|
|
1435
|
+
if (!factory) throw new Error(`No adapter factory registered for service: ${serviceId}`);
|
|
1436
|
+
return factory(token, capabilityId, context);
|
|
1437
|
+
},
|
|
1438
|
+
has(serviceId) {
|
|
1439
|
+
return factories.has(serviceId);
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
adapterRegistry.register("google", async (token, capabilityId) => {
|
|
1443
|
+
const m = await import("@anymux/google-drive");
|
|
1444
|
+
const adapters = {
|
|
1445
|
+
"file-system": () => new m.GoogleDriveFileSystem({ accessToken: token }),
|
|
1446
|
+
"contacts": () => new m.GoogleContactsProvider(token),
|
|
1447
|
+
"calendar": () => new m.GoogleCalendarProvider(token)
|
|
1448
|
+
};
|
|
1449
|
+
return adapters[capabilityId]();
|
|
1450
|
+
});
|
|
1451
|
+
adapterRegistry.register("dropbox", async (token) => {
|
|
1452
|
+
const { DropboxFileSystem } = await import("@anymux/dropbox");
|
|
1453
|
+
return new DropboxFileSystem({ accessToken: token });
|
|
1454
|
+
});
|
|
1455
|
+
adapterRegistry.register("box", async (token) => {
|
|
1456
|
+
const { BoxFileSystem } = await import("@anymux/box");
|
|
1457
|
+
return new BoxFileSystem({ accessToken: token });
|
|
1458
|
+
});
|
|
1459
|
+
adapterRegistry.register("github", async (token, capabilityId, ctx) => {
|
|
1460
|
+
const repo = ctx.selectedRepo;
|
|
1461
|
+
if (!repo) return null;
|
|
1462
|
+
const config = {
|
|
1463
|
+
token,
|
|
1464
|
+
owner: repo.owner,
|
|
1465
|
+
repo: repo.repo,
|
|
1466
|
+
branch: ctx.branch ?? "main"
|
|
1467
|
+
};
|
|
1468
|
+
const m = await import("@anymux/github");
|
|
1469
|
+
const adapters = {
|
|
1470
|
+
"file-system": () => new m.GitHubFileSystem(config),
|
|
1471
|
+
"git-repo": () => new m.GitHubGitRepo(config),
|
|
1472
|
+
"git-host": () => new m.GitHubGitHost(config)
|
|
1473
|
+
};
|
|
1474
|
+
return adapters[capabilityId]();
|
|
1475
|
+
});
|
|
1476
|
+
adapterRegistry.register("gitlab", async (token, capabilityId, ctx) => {
|
|
1477
|
+
const repo = ctx.selectedRepo;
|
|
1478
|
+
if (!repo) return null;
|
|
1479
|
+
const config = {
|
|
1480
|
+
token,
|
|
1481
|
+
projectId: `${repo.owner}/${repo.repo}`,
|
|
1482
|
+
branch: ctx.branch ?? "main"
|
|
1483
|
+
};
|
|
1484
|
+
const m = await import("@anymux/gitlab");
|
|
1485
|
+
const adapters = {
|
|
1486
|
+
"file-system": () => new m.GitLabFileSystem(config),
|
|
1487
|
+
"git-repo": () => new m.GitLabGitRepo(config),
|
|
1488
|
+
"git-host": () => new m.GitLabGitHost(config)
|
|
1489
|
+
};
|
|
1490
|
+
return adapters[capabilityId]();
|
|
1491
|
+
});
|
|
1492
|
+
adapterRegistry.register("bitbucket", async (token, capabilityId, ctx) => {
|
|
1493
|
+
const repo = ctx.selectedRepo;
|
|
1494
|
+
if (!repo) return null;
|
|
1495
|
+
const config = {
|
|
1496
|
+
token,
|
|
1497
|
+
workspace: repo.owner,
|
|
1498
|
+
repo: repo.repo,
|
|
1499
|
+
branch: ctx.branch ?? "main"
|
|
1500
|
+
};
|
|
1501
|
+
const m = await import("@anymux/bitbucket");
|
|
1502
|
+
const adapters = {
|
|
1503
|
+
"file-system": () => new m.BitbucketFileSystem(config),
|
|
1504
|
+
"git-repo": () => new m.BitbucketGitRepo(config),
|
|
1505
|
+
"git-host": () => new m.BitbucketGitHost(config)
|
|
1506
|
+
};
|
|
1507
|
+
return adapters[capabilityId]();
|
|
1508
|
+
});
|
|
1509
|
+
adapterRegistry.register("gitea", async (token, capabilityId, ctx) => {
|
|
1510
|
+
const creds = JSON.parse(token);
|
|
1511
|
+
const proxyUrl = typeof window !== "undefined" ? window.location.origin : void 0;
|
|
1512
|
+
const config = {
|
|
1513
|
+
url: creds.url,
|
|
1514
|
+
token: creds.token,
|
|
1515
|
+
username: creds.username,
|
|
1516
|
+
password: creds.password,
|
|
1517
|
+
owner: creds.owner,
|
|
1518
|
+
repo: creds.repo,
|
|
1519
|
+
branch: ctx.branch ?? creds.branch ?? "main",
|
|
1520
|
+
proxyUrl
|
|
1521
|
+
};
|
|
1522
|
+
const m = await import("@anymux/gitea");
|
|
1523
|
+
const adapters = {
|
|
1524
|
+
"file-system": () => new m.GiteaFileSystem(config),
|
|
1525
|
+
"git-repo": () => new m.GiteaGitRepo(config),
|
|
1526
|
+
"git-host": () => new m.GiteaGitHost(config)
|
|
1527
|
+
};
|
|
1528
|
+
return adapters[capabilityId]();
|
|
1529
|
+
});
|
|
1530
|
+
adapterRegistry.register("webdav", async (token) => {
|
|
1531
|
+
const creds = JSON.parse(token);
|
|
1532
|
+
const { WebDAVFileSystem } = await import("@anymux/webdav");
|
|
1533
|
+
const proxyUrl = typeof window !== "undefined" ? window.location.origin : void 0;
|
|
1534
|
+
return new WebDAVFileSystem({
|
|
1535
|
+
url: creds.url,
|
|
1536
|
+
username: creds.username,
|
|
1537
|
+
password: creds.password,
|
|
1538
|
+
proxyUrl
|
|
1539
|
+
});
|
|
1540
|
+
});
|
|
1541
|
+
adapterRegistry.register("s3", async (token) => {
|
|
1542
|
+
const creds = JSON.parse(token);
|
|
1543
|
+
const proxyUrl = typeof window !== "undefined" ? window.location.origin : void 0;
|
|
1544
|
+
if (proxyUrl) {
|
|
1545
|
+
const { S3ProxyStorage } = await import("@anymux/s3");
|
|
1546
|
+
const storage$1 = new S3ProxyStorage({
|
|
1547
|
+
proxyUrl,
|
|
1548
|
+
endpoint: creds.endpoint,
|
|
1549
|
+
region: creds.region?.trim(),
|
|
1550
|
+
accessKeyId: creds.accessKeyId?.trim(),
|
|
1551
|
+
secretAccessKey: creds.secretAccessKey?.trim(),
|
|
1552
|
+
forcePathStyle: true
|
|
1553
|
+
});
|
|
1554
|
+
return {
|
|
1555
|
+
storage: storage$1,
|
|
1556
|
+
bucket: creds.bucket || "anymux-test"
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
const { S3ObjectStorage } = await import("@anymux/s3");
|
|
1560
|
+
const storage = new S3ObjectStorage({
|
|
1561
|
+
region: creds.region?.trim(),
|
|
1562
|
+
endpoint: creds.endpoint,
|
|
1563
|
+
credentials: {
|
|
1564
|
+
accessKeyId: creds.accessKeyId?.trim(),
|
|
1565
|
+
secretAccessKey: creds.secretAccessKey?.trim()
|
|
1566
|
+
},
|
|
1567
|
+
forcePathStyle: true
|
|
1568
|
+
});
|
|
1569
|
+
return {
|
|
1570
|
+
storage,
|
|
1571
|
+
bucket: creds.bucket || "anymux-test"
|
|
1572
|
+
};
|
|
1573
|
+
});
|
|
1574
|
+
adapterRegistry.register("browser-fs", async () => {
|
|
1575
|
+
const { BrowserFileSystemFactory } = await import("@anymux/browser-fs");
|
|
1576
|
+
const factory = new BrowserFileSystemFactory();
|
|
1577
|
+
const handleInfos = await factory.getAllHandleInfos();
|
|
1578
|
+
if (handleInfos.length > 0) return factory.createFromHandleInfo(handleInfos[0].id);
|
|
1579
|
+
throw new Error("No local file system connected. Please connect first.");
|
|
1580
|
+
});
|
|
1581
|
+
adapterRegistry.register("indexeddb", async () => {
|
|
1582
|
+
const { IndexedDBFileSystem } = await import("@anymux/indexeddb");
|
|
1583
|
+
const fs = new IndexedDBFileSystem();
|
|
1584
|
+
await fs.init();
|
|
1585
|
+
return fs;
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
//#endregion
|
|
1589
|
+
//#region src/components/useAdapter.ts
|
|
1590
|
+
/** Hook to async-create an adapter, run a pre-flight check, and track loading/error state */
|
|
1591
|
+
function useAdapter(factory, deps, preflight) {
|
|
1592
|
+
const [adapter, setAdapter] = useState(null);
|
|
1593
|
+
const [loading, setLoading] = useState(true);
|
|
1594
|
+
const [error, setError] = useState(null);
|
|
1595
|
+
const [retryCount, setRetryCount] = useState(0);
|
|
1596
|
+
useEffect(() => {
|
|
1597
|
+
let cancelled = false;
|
|
1598
|
+
setLoading(true);
|
|
1599
|
+
setError(null);
|
|
1600
|
+
setAdapter(null);
|
|
1601
|
+
factory().then(async (a) => {
|
|
1602
|
+
if (preflight) await preflight(a);
|
|
1603
|
+
if (!cancelled) {
|
|
1604
|
+
setAdapter(a);
|
|
1605
|
+
setLoading(false);
|
|
1606
|
+
}
|
|
1607
|
+
}).catch((e) => {
|
|
1608
|
+
if (!cancelled) {
|
|
1609
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
1610
|
+
setLoading(false);
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
return () => {
|
|
1614
|
+
cancelled = true;
|
|
1615
|
+
};
|
|
1616
|
+
}, [...deps, retryCount]);
|
|
1617
|
+
return {
|
|
1618
|
+
adapter,
|
|
1619
|
+
loading,
|
|
1620
|
+
error,
|
|
1621
|
+
retry: () => setRetryCount((c) => c + 1)
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
//#endregion
|
|
1626
|
+
//#region src/components/CapabilityError.tsx
|
|
1627
|
+
function isAuthError(error) {
|
|
1628
|
+
return /\b(401|403|auth|token|expired|unauthorized|forbidden|denied|scope|permission|sign.?in|log.?in)\b/i.test(error);
|
|
1629
|
+
}
|
|
1630
|
+
/** Render error with contextual actions for capability panels */
|
|
1631
|
+
function CapabilityError({ error, onRetry, onReconnect, onGoBack }) {
|
|
1632
|
+
const authError = isAuthError(error);
|
|
1633
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1634
|
+
className: "flex flex-col items-center justify-center h-64 gap-4 p-4",
|
|
1635
|
+
children: [/* @__PURE__ */ jsx(ErrorDisplay, {
|
|
1636
|
+
error,
|
|
1637
|
+
title: authError ? "Authentication Required" : "Failed to Load",
|
|
1638
|
+
variant: authError ? "warning" : "destructive",
|
|
1639
|
+
className: "max-w-md w-full",
|
|
1640
|
+
...onRetry != null ? { onRetry } : {}
|
|
1641
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1642
|
+
className: "flex items-center gap-2",
|
|
1643
|
+
children: [onReconnect && /* @__PURE__ */ jsxs("button", {
|
|
1644
|
+
onClick: onReconnect,
|
|
1645
|
+
className: "inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors",
|
|
1646
|
+
children: [/* @__PURE__ */ jsx(RefreshCw, { className: "h-3 w-3" }), "Reconnect"]
|
|
1647
|
+
}), onGoBack && /* @__PURE__ */ jsxs("button", {
|
|
1648
|
+
onClick: onGoBack,
|
|
1649
|
+
className: "inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-md bg-muted hover:bg-muted/80 transition-colors",
|
|
1650
|
+
children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-3 w-3" }), "Back to Dashboard"]
|
|
1651
|
+
})]
|
|
1652
|
+
})]
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
//#endregion
|
|
1657
|
+
//#region src/components/CapabilityPanel.tsx
|
|
1658
|
+
const FileBrowser = React.lazy(() => import("@anymux/fs-ui").then((m) => ({ default: m.FileBrowser })));
|
|
1659
|
+
const LazyMediaBrowser = React.lazy(() => import("@anymux/object-ui").then((m) => ({ default: ({ provider }) => {
|
|
1660
|
+
const model = new m.MediaBrowserModel(provider);
|
|
1661
|
+
return React.createElement(m.MediaBrowser, {
|
|
1662
|
+
model,
|
|
1663
|
+
provider,
|
|
1664
|
+
className: "h-full"
|
|
1665
|
+
});
|
|
1666
|
+
} })));
|
|
1667
|
+
const LazyContactBrowser = React.lazy(() => import("@anymux/object-ui").then((m) => ({ default: ({ provider }) => {
|
|
1668
|
+
const model = new m.ContactListModel(provider);
|
|
1669
|
+
return React.createElement(m.ContactBrowser, {
|
|
1670
|
+
model,
|
|
1671
|
+
className: "h-full"
|
|
1672
|
+
});
|
|
1673
|
+
} })));
|
|
1674
|
+
const LazyCalendarBrowser = React.lazy(() => import("@anymux/object-ui").then((m) => ({ default: ({ provider }) => {
|
|
1675
|
+
const model = new m.CalendarModel(provider);
|
|
1676
|
+
return React.createElement(m.CalendarBrowser, {
|
|
1677
|
+
model,
|
|
1678
|
+
className: "h-full"
|
|
1679
|
+
});
|
|
1680
|
+
} })));
|
|
1681
|
+
const RepoPicker$1 = React.lazy(() => import("./RepoPicker-BprFGOn7.js").then((m) => ({ default: m.RepoPicker })));
|
|
1682
|
+
const ObjectStorageBrowserLazy = React.lazy(() => import("./ObjectStorageBrowser-B2YkUxMl.js").then((m) => ({ default: m.ObjectStorageBrowser })));
|
|
1683
|
+
const GitRepoBrowserLazy = React.lazy(() => import("./GitBrowser-CIyWiuX-.js").then((m) => ({ default: m.GitRepoBrowser })));
|
|
1684
|
+
const GitHostBrowserLazy = React.lazy(() => import("./GitBrowser-CIyWiuX-.js").then((m) => ({ default: m.GitHostBrowser })));
|
|
1685
|
+
const CAPABILITY_LABELS = {
|
|
1686
|
+
"file-system": "File System",
|
|
1687
|
+
"object-storage": "Object Storage",
|
|
1688
|
+
"git-repo": "Git",
|
|
1689
|
+
"git-host": "Git",
|
|
1690
|
+
"media": "Media",
|
|
1691
|
+
"contacts": "Contacts",
|
|
1692
|
+
"calendar": "Calendar"
|
|
1693
|
+
};
|
|
1694
|
+
const REPO_SERVICES$1 = new Set([
|
|
1695
|
+
"github",
|
|
1696
|
+
"gitlab",
|
|
1697
|
+
"bitbucket",
|
|
1698
|
+
"gitea"
|
|
1699
|
+
]);
|
|
1700
|
+
const LoadingFallback = () => /* @__PURE__ */ jsx("div", {
|
|
1701
|
+
className: "flex items-center justify-center h-64",
|
|
1702
|
+
children: /* @__PURE__ */ jsx(LoadingSpinner, { label: "Loading..." })
|
|
1703
|
+
});
|
|
1704
|
+
function GitRepoBrowserContent({ serviceId, token, gitRepo, owner, repo, selectedRepo, onError, actionNotifications }) {
|
|
1705
|
+
const createFileSystem = useCallback(async (branch) => {
|
|
1706
|
+
const context = {
|
|
1707
|
+
selectedRepo,
|
|
1708
|
+
branch
|
|
1709
|
+
};
|
|
1710
|
+
const fs = await adapterRegistry.create(serviceId, token, "file-system", context);
|
|
1711
|
+
return fs;
|
|
1712
|
+
}, [
|
|
1713
|
+
serviceId,
|
|
1714
|
+
token,
|
|
1715
|
+
selectedRepo
|
|
1716
|
+
]);
|
|
1717
|
+
const [gitHost, setGitHost] = useState();
|
|
1718
|
+
React.useEffect(() => {
|
|
1719
|
+
let cancelled = false;
|
|
1720
|
+
adapterRegistry.create(serviceId, token, "git-host", { selectedRepo }).then((adapter) => {
|
|
1721
|
+
if (!cancelled) setGitHost(adapter);
|
|
1722
|
+
}).catch(() => {});
|
|
1723
|
+
return () => {
|
|
1724
|
+
cancelled = true;
|
|
1725
|
+
};
|
|
1726
|
+
}, [
|
|
1727
|
+
serviceId,
|
|
1728
|
+
token,
|
|
1729
|
+
selectedRepo
|
|
1730
|
+
]);
|
|
1731
|
+
return /* @__PURE__ */ jsx(GitRepoBrowserLazy, {
|
|
1732
|
+
gitRepo,
|
|
1733
|
+
owner,
|
|
1734
|
+
repo,
|
|
1735
|
+
createFileSystem,
|
|
1736
|
+
gitHost,
|
|
1737
|
+
onError,
|
|
1738
|
+
actionNotifications
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
function GitHostBrowserWithRepo({ serviceId, token, gitHost, owner, repo, selectedRepo, onError, actionNotifications }) {
|
|
1742
|
+
const [gitRepo, setGitRepo] = useState();
|
|
1743
|
+
const [repoLoaded, setRepoLoaded] = useState(false);
|
|
1744
|
+
React.useEffect(() => {
|
|
1745
|
+
let cancelled = false;
|
|
1746
|
+
adapterRegistry.create(serviceId, token, "git-repo", { selectedRepo }).then((adapter) => {
|
|
1747
|
+
if (!cancelled) setGitRepo(adapter);
|
|
1748
|
+
}).catch(() => {}).finally(() => {
|
|
1749
|
+
if (!cancelled) setRepoLoaded(true);
|
|
1750
|
+
});
|
|
1751
|
+
return () => {
|
|
1752
|
+
cancelled = true;
|
|
1753
|
+
};
|
|
1754
|
+
}, [
|
|
1755
|
+
serviceId,
|
|
1756
|
+
token,
|
|
1757
|
+
selectedRepo
|
|
1758
|
+
]);
|
|
1759
|
+
const createFileSystem = useCallback(async (branch) => {
|
|
1760
|
+
const context = {
|
|
1761
|
+
selectedRepo,
|
|
1762
|
+
branch
|
|
1763
|
+
};
|
|
1764
|
+
const fs = await adapterRegistry.create(serviceId, token, "file-system", context);
|
|
1765
|
+
return fs;
|
|
1766
|
+
}, [
|
|
1767
|
+
serviceId,
|
|
1768
|
+
token,
|
|
1769
|
+
selectedRepo
|
|
1770
|
+
]);
|
|
1771
|
+
if (!repoLoaded) return /* @__PURE__ */ jsx(LoadingFallback, {});
|
|
1772
|
+
if (gitRepo) return /* @__PURE__ */ jsx(GitRepoBrowserLazy, {
|
|
1773
|
+
gitRepo,
|
|
1774
|
+
owner,
|
|
1775
|
+
repo,
|
|
1776
|
+
createFileSystem,
|
|
1777
|
+
gitHost,
|
|
1778
|
+
onError,
|
|
1779
|
+
actionNotifications
|
|
1780
|
+
});
|
|
1781
|
+
return /* @__PURE__ */ jsx(GitHostBrowserLazy, { gitHost });
|
|
1782
|
+
}
|
|
1783
|
+
const GenericCapabilityContent = observer(function GenericCapabilityContent$1({ serviceId, capabilityId, token, dashboardModel, connectionManager }) {
|
|
1784
|
+
const selectedRepo = dashboardModel.getSelectedRepo(serviceId);
|
|
1785
|
+
const context = { selectedRepo: selectedRepo ?? void 0 };
|
|
1786
|
+
const { adapter, loading, error, retry } = useAdapter(
|
|
1787
|
+
async () => {
|
|
1788
|
+
console.info(`[AnyMux] CapabilityPanel: refreshing token for ${serviceId}/${capabilityId}`);
|
|
1789
|
+
const freshToken = await connectionManager.refreshToken(serviceId) ?? token;
|
|
1790
|
+
console.info(`[AnyMux] CapabilityPanel: creating adapter with token ${freshToken.slice(0, 8)}…${freshToken.slice(-4)}`);
|
|
1791
|
+
const a = adapterRegistry.create(serviceId, freshToken, capabilityId, context);
|
|
1792
|
+
return a;
|
|
1793
|
+
},
|
|
1794
|
+
[
|
|
1795
|
+
serviceId,
|
|
1796
|
+
capabilityId,
|
|
1797
|
+
token,
|
|
1798
|
+
selectedRepo?.owner,
|
|
1799
|
+
selectedRepo?.repo
|
|
1800
|
+
],
|
|
1801
|
+
// Pre-flight: verify the adapter actually works before mounting the browser UI.
|
|
1802
|
+
// If we get a 401, try one more refresh — better-auth may not have detected
|
|
1803
|
+
// the token as expired (e.g. missing accessTokenExpiresAt in the DB).
|
|
1804
|
+
async (a) => {
|
|
1805
|
+
const runCheck = async (adapter$1) => {
|
|
1806
|
+
if (capabilityId === "file-system") await adapter$1.readdir("/");
|
|
1807
|
+
else if (capabilityId === "git-host") {
|
|
1808
|
+
if (adapter$1.listRepositories) await adapter$1.listRepositories({
|
|
1809
|
+
page: 1,
|
|
1810
|
+
perPage: 1
|
|
1811
|
+
});
|
|
1812
|
+
} else if (capabilityId === "git-repo") {
|
|
1813
|
+
if (adapter$1.listBranches) await adapter$1.listBranches();
|
|
1814
|
+
} else if (capabilityId === "calendar") {
|
|
1815
|
+
if (adapter$1.getCalendars) await adapter$1.getCalendars();
|
|
1816
|
+
} else if (capabilityId === "contacts") {
|
|
1817
|
+
if (adapter$1.getContacts) await adapter$1.getContacts({ limit: 1 });
|
|
1818
|
+
} else if (capabilityId === "media") {
|
|
1819
|
+
if (adapter$1.getAlbums) await adapter$1.getAlbums();
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
try {
|
|
1823
|
+
console.info(`[AnyMux] CapabilityPanel: running pre-flight for ${serviceId}/${capabilityId}`);
|
|
1824
|
+
await runCheck(a);
|
|
1825
|
+
console.info(`[AnyMux] CapabilityPanel: pre-flight passed`);
|
|
1826
|
+
} catch (err) {
|
|
1827
|
+
console.warn(`[AnyMux] CapabilityPanel: pre-flight failed for ${serviceId}/${capabilityId}:`, err instanceof Error ? err.message : err);
|
|
1828
|
+
if (err instanceof Error && isAuthError(err.message)) {
|
|
1829
|
+
console.info(`[AnyMux] CapabilityPanel: auth error detected, attempting second refresh`);
|
|
1830
|
+
const retryToken = await connectionManager.refreshToken(serviceId);
|
|
1831
|
+
if (retryToken && retryToken !== token) {
|
|
1832
|
+
console.info(`[AnyMux] CapabilityPanel: got different token on retry, rebuilding adapter`);
|
|
1833
|
+
const retryAdapter = await adapterRegistry.create(serviceId, retryToken, capabilityId, context);
|
|
1834
|
+
await runCheck(retryAdapter);
|
|
1835
|
+
Object.assign(a, retryAdapter);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
console.warn(`[AnyMux] CapabilityPanel: retry token same as original, giving up`);
|
|
1839
|
+
}
|
|
1840
|
+
throw err;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
);
|
|
1844
|
+
const handleReconnect = async () => {
|
|
1845
|
+
const service = dashboardModel.selectedService;
|
|
1846
|
+
await connectionManager.disconnect(serviceId);
|
|
1847
|
+
dashboardModel.closePanel();
|
|
1848
|
+
const isOAuth = service && ![
|
|
1849
|
+
"s3",
|
|
1850
|
+
"webdav",
|
|
1851
|
+
"gitea",
|
|
1852
|
+
"icloud",
|
|
1853
|
+
"browser-fs",
|
|
1854
|
+
"indexeddb"
|
|
1855
|
+
].includes(service.authProvider);
|
|
1856
|
+
if (isOAuth) await connectionManager.connect(serviceId);
|
|
1857
|
+
else connectionManager.requestReconnect(serviceId);
|
|
1858
|
+
};
|
|
1859
|
+
const handleGoBack = () => {
|
|
1860
|
+
dashboardModel.closePanel();
|
|
1861
|
+
};
|
|
1862
|
+
const [runtimeError, setRuntimeError] = useState(null);
|
|
1863
|
+
const handleRuntimeError = (err) => {
|
|
1864
|
+
if (isAuthError(err.message)) setRuntimeError(err.message);
|
|
1865
|
+
};
|
|
1866
|
+
const handleNotify = useCallback((type, message) => {
|
|
1867
|
+
if (type === "success") showActionToast(dashboardModel.actionNotifications, "create", message);
|
|
1868
|
+
else showErrorToast(message);
|
|
1869
|
+
}, [dashboardModel.actionNotifications]);
|
|
1870
|
+
const handleAction = useCallback((action) => {
|
|
1871
|
+
showActionToast(dashboardModel.actionNotifications, action.type, action.message, { undo: action.undo });
|
|
1872
|
+
}, [dashboardModel.actionNotifications]);
|
|
1873
|
+
if (loading) return /* @__PURE__ */ jsx(LoadingFallback, {});
|
|
1874
|
+
if (error) return /* @__PURE__ */ jsx(CapabilityError, {
|
|
1875
|
+
error,
|
|
1876
|
+
onRetry: retry,
|
|
1877
|
+
onReconnect: handleReconnect,
|
|
1878
|
+
onGoBack: handleGoBack
|
|
1879
|
+
});
|
|
1880
|
+
if (runtimeError) return /* @__PURE__ */ jsx(CapabilityError, {
|
|
1881
|
+
error: runtimeError,
|
|
1882
|
+
onReconnect: handleReconnect,
|
|
1883
|
+
onGoBack: handleGoBack
|
|
1884
|
+
});
|
|
1885
|
+
if (adapter === null) return null;
|
|
1886
|
+
return match(capabilityId).with("file-system", () => /* @__PURE__ */ jsx(Suspense, {
|
|
1887
|
+
fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
|
|
1888
|
+
children: /* @__PURE__ */ jsx(FileBrowser, {
|
|
1889
|
+
fileSystem: adapter,
|
|
1890
|
+
className: "h-full",
|
|
1891
|
+
initialPath: dashboardModel.browserPath !== "/" ? dashboardModel.browserPath : void 0,
|
|
1892
|
+
onPathChange: (path) => dashboardModel.setBrowserPath(path),
|
|
1893
|
+
onError: handleRuntimeError,
|
|
1894
|
+
onNotify: handleNotify,
|
|
1895
|
+
onAction: handleAction,
|
|
1896
|
+
showBreadcrumbs: false
|
|
1897
|
+
})
|
|
1898
|
+
})).with("object-storage", () => {
|
|
1899
|
+
const s3Result = adapter;
|
|
1900
|
+
return /* @__PURE__ */ jsx(Suspense, {
|
|
1901
|
+
fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
|
|
1902
|
+
children: /* @__PURE__ */ jsx(ObjectStorageBrowserLazy, {
|
|
1903
|
+
storage: s3Result.storage,
|
|
1904
|
+
bucket: s3Result.bucket
|
|
1905
|
+
})
|
|
1906
|
+
});
|
|
1907
|
+
}).with("git-repo", () => {
|
|
1908
|
+
const repo = selectedRepo ?? {
|
|
1909
|
+
owner: "",
|
|
1910
|
+
repo: ""
|
|
1911
|
+
};
|
|
1912
|
+
return /* @__PURE__ */ jsx(Suspense, {
|
|
1913
|
+
fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
|
|
1914
|
+
children: /* @__PURE__ */ jsx(GitRepoBrowserContent, {
|
|
1915
|
+
serviceId,
|
|
1916
|
+
token,
|
|
1917
|
+
gitRepo: adapter,
|
|
1918
|
+
owner: repo.owner,
|
|
1919
|
+
repo: repo.repo,
|
|
1920
|
+
selectedRepo: selectedRepo ?? void 0,
|
|
1921
|
+
onError: handleRuntimeError,
|
|
1922
|
+
actionNotifications: dashboardModel.actionNotifications
|
|
1923
|
+
})
|
|
1924
|
+
});
|
|
1925
|
+
}).with("git-host", () => {
|
|
1926
|
+
const repo = selectedRepo ?? {
|
|
1927
|
+
owner: "",
|
|
1928
|
+
repo: ""
|
|
1929
|
+
};
|
|
1930
|
+
return /* @__PURE__ */ jsx(Suspense, {
|
|
1931
|
+
fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
|
|
1932
|
+
children: /* @__PURE__ */ jsx(GitHostBrowserWithRepo, {
|
|
1933
|
+
serviceId,
|
|
1934
|
+
token,
|
|
1935
|
+
gitHost: adapter,
|
|
1936
|
+
owner: repo.owner,
|
|
1937
|
+
repo: repo.repo,
|
|
1938
|
+
selectedRepo: selectedRepo ?? void 0,
|
|
1939
|
+
onError: handleRuntimeError,
|
|
1940
|
+
actionNotifications: dashboardModel.actionNotifications
|
|
1941
|
+
})
|
|
1942
|
+
});
|
|
1943
|
+
}).with("media", () => /* @__PURE__ */ jsx(Suspense, {
|
|
1944
|
+
fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
|
|
1945
|
+
children: /* @__PURE__ */ jsx(LazyMediaBrowser, { provider: adapter })
|
|
1946
|
+
})).with("contacts", () => /* @__PURE__ */ jsx(Suspense, {
|
|
1947
|
+
fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
|
|
1948
|
+
children: /* @__PURE__ */ jsx(LazyContactBrowser, { provider: adapter })
|
|
1949
|
+
})).with("calendar", () => /* @__PURE__ */ jsx(Suspense, {
|
|
1950
|
+
fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
|
|
1951
|
+
children: /* @__PURE__ */ jsx(LazyCalendarBrowser, { provider: adapter })
|
|
1952
|
+
})).exhaustive();
|
|
1953
|
+
});
|
|
1954
|
+
const CapabilityContent = observer(({ serviceId, capabilityId, connectionManager, dashboardModel }) => {
|
|
1955
|
+
const service = dashboardModel.selectedService;
|
|
1956
|
+
if (!service) return null;
|
|
1957
|
+
const token = connectionManager.getToken(serviceId);
|
|
1958
|
+
if (!token) return /* @__PURE__ */ jsx(CapabilityError, { error: "Not connected. Please connect the service first." });
|
|
1959
|
+
if (REPO_SERVICES$1.has(serviceId) && (capabilityId === "file-system" || capabilityId === "git-repo" || capabilityId === "git-host")) {
|
|
1960
|
+
let selectedRepo = dashboardModel.getSelectedRepo(serviceId);
|
|
1961
|
+
if (!selectedRepo) try {
|
|
1962
|
+
const creds = JSON.parse(token);
|
|
1963
|
+
if (creds.owner && creds.repo) {
|
|
1964
|
+
dashboardModel.setSelectedRepo(serviceId, {
|
|
1965
|
+
owner: creds.owner,
|
|
1966
|
+
repo: creds.repo
|
|
1967
|
+
});
|
|
1968
|
+
selectedRepo = {
|
|
1969
|
+
owner: creds.owner,
|
|
1970
|
+
repo: creds.repo
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
} catch {}
|
|
1974
|
+
if (!selectedRepo) return /* @__PURE__ */ jsx(Suspense, {
|
|
1975
|
+
fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
|
|
1976
|
+
children: /* @__PURE__ */ jsx(RepoPicker$1, {
|
|
1977
|
+
serviceId,
|
|
1978
|
+
accessToken: token,
|
|
1979
|
+
onSelectRepo: (repo) => dashboardModel.setSelectedRepo(serviceId, repo)
|
|
1980
|
+
})
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
return /* @__PURE__ */ jsx(GenericCapabilityContent, {
|
|
1984
|
+
serviceId,
|
|
1985
|
+
capabilityId,
|
|
1986
|
+
token,
|
|
1987
|
+
dashboardModel,
|
|
1988
|
+
connectionManager
|
|
1989
|
+
});
|
|
1990
|
+
});
|
|
1991
|
+
const ScopeWarningBanner = observer(({ serviceId, capabilityId, connectionManager }) => {
|
|
1992
|
+
const token = connectionManager.getToken(serviceId);
|
|
1993
|
+
if (!token) return null;
|
|
1994
|
+
const hasScopes = connectionManager.hasCapabilityScopes(serviceId, capabilityId);
|
|
1995
|
+
if (hasScopes) return null;
|
|
1996
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1997
|
+
className: "flex items-center gap-2 px-4 py-2 bg-yellow-50 dark:bg-yellow-900/20 border-b border-yellow-200 dark:border-yellow-800",
|
|
1998
|
+
children: [/* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4 text-yellow-500 flex-shrink-0" }), /* @__PURE__ */ jsx("span", {
|
|
1999
|
+
className: "text-xs text-yellow-700 dark:text-yellow-400",
|
|
2000
|
+
children: "Required scopes not granted. Some features may not work. Try reconnecting with the needed permissions."
|
|
2001
|
+
})]
|
|
2002
|
+
});
|
|
2003
|
+
});
|
|
2004
|
+
const CapabilityPanel = observer(({ dashboardModel, connectionManager }) => {
|
|
2005
|
+
if (!dashboardModel.panelOpen || !dashboardModel.selectedCell) return null;
|
|
2006
|
+
const { serviceId, capabilityId } = dashboardModel.selectedCell;
|
|
2007
|
+
const service = dashboardModel.selectedService;
|
|
2008
|
+
const selectedRepo = dashboardModel.getSelectedRepo(serviceId);
|
|
2009
|
+
const hasRepo = REPO_SERVICES$1.has(serviceId) && selectedRepo;
|
|
2010
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2011
|
+
className: "border border-border rounded-lg mt-4 overflow-hidden bg-card flex flex-col h-[70vh]",
|
|
2012
|
+
children: [
|
|
2013
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2014
|
+
className: "flex items-center justify-between px-4 py-2 bg-muted border-b border-border",
|
|
2015
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2016
|
+
className: "flex items-center gap-2",
|
|
2017
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
2018
|
+
className: "text-sm font-medium truncate",
|
|
2019
|
+
title: `${service?.name} — ${CAPABILITY_LABELS[capabilityId]}`,
|
|
2020
|
+
children: [
|
|
2021
|
+
service?.name,
|
|
2022
|
+
" — ",
|
|
2023
|
+
CAPABILITY_LABELS[capabilityId]
|
|
2024
|
+
]
|
|
2025
|
+
}), hasRepo && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
|
|
2026
|
+
className: "text-xs text-muted-foreground truncate",
|
|
2027
|
+
title: `${selectedRepo.owner}/${selectedRepo.repo}`,
|
|
2028
|
+
children: [
|
|
2029
|
+
"(",
|
|
2030
|
+
selectedRepo.owner,
|
|
2031
|
+
"/",
|
|
2032
|
+
selectedRepo.repo,
|
|
2033
|
+
")"
|
|
2034
|
+
]
|
|
2035
|
+
}), /* @__PURE__ */ jsxs("button", {
|
|
2036
|
+
onClick: () => dashboardModel.clearSelectedRepo(serviceId),
|
|
2037
|
+
className: "inline-flex items-center gap-1 px-2 py-0.5 text-[11px] font-medium rounded bg-muted hover:bg-muted/80 transition-colors",
|
|
2038
|
+
children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-3 w-3" }), "Change repo"]
|
|
2039
|
+
})] })]
|
|
2040
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
2041
|
+
onClick: () => dashboardModel.closePanel(),
|
|
2042
|
+
className: "p-1 rounded hover:bg-muted transition-colors",
|
|
2043
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
|
|
2044
|
+
})]
|
|
2045
|
+
}),
|
|
2046
|
+
/* @__PURE__ */ jsx(ScopeWarningBanner, {
|
|
2047
|
+
serviceId,
|
|
2048
|
+
capabilityId,
|
|
2049
|
+
connectionManager
|
|
2050
|
+
}),
|
|
2051
|
+
/* @__PURE__ */ jsx("div", {
|
|
2052
|
+
className: "flex-1 min-h-0 overflow-hidden",
|
|
2053
|
+
children: /* @__PURE__ */ jsx(Suspense, {
|
|
2054
|
+
fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
|
|
2055
|
+
children: /* @__PURE__ */ jsx(CapabilityContent, {
|
|
2056
|
+
serviceId,
|
|
2057
|
+
capabilityId,
|
|
2058
|
+
connectionManager,
|
|
2059
|
+
dashboardModel
|
|
2060
|
+
})
|
|
2061
|
+
})
|
|
2062
|
+
})
|
|
2063
|
+
]
|
|
2064
|
+
});
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
//#endregion
|
|
2068
|
+
//#region src/components/ActionHistoryPanel.tsx
|
|
2069
|
+
const ACTION_ICONS = {
|
|
2070
|
+
delete: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5 text-destructive" }),
|
|
2071
|
+
rename: /* @__PURE__ */ jsx(FileEdit, { className: "h-3.5 w-3.5 text-blue-500" }),
|
|
2072
|
+
create: /* @__PURE__ */ jsx(FilePlus, { className: "h-3.5 w-3.5 text-green-500" }),
|
|
2073
|
+
upload: /* @__PURE__ */ jsx(Upload, { className: "h-3.5 w-3.5 text-purple-500" }),
|
|
2074
|
+
copy: /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5 text-muted-foreground" }),
|
|
2075
|
+
move: /* @__PURE__ */ jsx(Move, { className: "h-3.5 w-3.5 text-orange-500" })
|
|
2076
|
+
};
|
|
2077
|
+
function formatTimeAgo(date) {
|
|
2078
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
|
|
2079
|
+
if (seconds < 60) return "just now";
|
|
2080
|
+
const minutes = Math.floor(seconds / 60);
|
|
2081
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
2082
|
+
const hours = Math.floor(minutes / 60);
|
|
2083
|
+
if (hours < 24) return `${hours}h ago`;
|
|
2084
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
2085
|
+
}
|
|
2086
|
+
const ActionItem = observer(({ action, model }) => {
|
|
2087
|
+
const [undoing, setUndoing] = useState(false);
|
|
2088
|
+
const handleUndo = async () => {
|
|
2089
|
+
if (!action.undo || action.undone || undoing) return;
|
|
2090
|
+
setUndoing(true);
|
|
2091
|
+
try {
|
|
2092
|
+
await action.undo();
|
|
2093
|
+
model.markUndone(action.id);
|
|
2094
|
+
} catch {} finally {
|
|
2095
|
+
setUndoing(false);
|
|
2096
|
+
}
|
|
2097
|
+
};
|
|
2098
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2099
|
+
className: `flex items-start gap-2 px-3 py-2 text-xs ${action.undone ? "opacity-50" : ""}`,
|
|
2100
|
+
children: [
|
|
2101
|
+
/* @__PURE__ */ jsx("span", {
|
|
2102
|
+
className: "mt-0.5 flex-shrink-0",
|
|
2103
|
+
children: ACTION_ICONS[action.type]
|
|
2104
|
+
}),
|
|
2105
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2106
|
+
className: "flex-1 min-w-0",
|
|
2107
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
2108
|
+
className: `text-foreground truncate ${action.undone ? "line-through" : ""}`,
|
|
2109
|
+
title: action.description,
|
|
2110
|
+
children: action.description
|
|
2111
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
2112
|
+
className: "text-muted-foreground mt-0.5",
|
|
2113
|
+
children: formatTimeAgo(action.timestamp)
|
|
2114
|
+
})]
|
|
2115
|
+
}),
|
|
2116
|
+
action.undo && !action.undone && /* @__PURE__ */ jsx("button", {
|
|
2117
|
+
onClick: handleUndo,
|
|
2118
|
+
disabled: undoing,
|
|
2119
|
+
className: "flex-shrink-0 p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50",
|
|
2120
|
+
title: "Undo",
|
|
2121
|
+
children: /* @__PURE__ */ jsx(Undo2, { className: "h-3.5 w-3.5" })
|
|
2122
|
+
})
|
|
2123
|
+
]
|
|
2124
|
+
});
|
|
2125
|
+
});
|
|
2126
|
+
const ActionHistoryPanel = observer(({ model }) => {
|
|
2127
|
+
const [open, setOpen] = useState(false);
|
|
2128
|
+
const panelRef = useRef(null);
|
|
2129
|
+
const actions = model.recentActions;
|
|
2130
|
+
useEffect(() => {
|
|
2131
|
+
if (!open) return;
|
|
2132
|
+
const handleClick = (e) => {
|
|
2133
|
+
if (panelRef.current && !panelRef.current.contains(e.target)) setOpen(false);
|
|
2134
|
+
};
|
|
2135
|
+
document.addEventListener("mousedown", handleClick);
|
|
2136
|
+
return () => document.removeEventListener("mousedown", handleClick);
|
|
2137
|
+
}, [open]);
|
|
2138
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2139
|
+
className: "relative",
|
|
2140
|
+
ref: panelRef,
|
|
2141
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
2142
|
+
onClick: () => setOpen(!open),
|
|
2143
|
+
className: "relative p-1.5 rounded-md hover:bg-background/80 text-muted-foreground hover:text-foreground transition-colors",
|
|
2144
|
+
title: "Action history",
|
|
2145
|
+
children: [/* @__PURE__ */ jsx(History, { className: "h-4 w-4" }), actions.length > 0 && /* @__PURE__ */ jsx("span", {
|
|
2146
|
+
className: "absolute -top-0.5 -right-0.5 h-3.5 w-3.5 rounded-full bg-primary text-primary-foreground text-[9px] font-medium flex items-center justify-center",
|
|
2147
|
+
children: actions.length > 9 ? "9+" : actions.length
|
|
2148
|
+
})]
|
|
2149
|
+
}), open && /* @__PURE__ */ jsxs("div", {
|
|
2150
|
+
className: "absolute right-0 top-full mt-1 w-72 rounded-lg border border-border bg-card shadow-lg z-50 animate-in fade-in slide-in-from-top-1 duration-150",
|
|
2151
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2152
|
+
className: "flex items-center justify-between px-3 py-2 border-b border-border",
|
|
2153
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2154
|
+
className: "text-xs font-medium text-foreground",
|
|
2155
|
+
children: "Recent Actions"
|
|
2156
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2157
|
+
className: "flex items-center gap-1",
|
|
2158
|
+
children: [actions.length > 0 && /* @__PURE__ */ jsx("button", {
|
|
2159
|
+
onClick: () => model.clear(),
|
|
2160
|
+
className: "text-[10px] text-muted-foreground hover:text-foreground px-1.5 py-0.5 rounded hover:bg-muted transition-colors",
|
|
2161
|
+
children: "Clear"
|
|
2162
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
2163
|
+
onClick: () => setOpen(false),
|
|
2164
|
+
className: "p-0.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors",
|
|
2165
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
|
|
2166
|
+
})]
|
|
2167
|
+
})]
|
|
2168
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2169
|
+
className: "max-h-64 overflow-y-auto divide-y divide-border",
|
|
2170
|
+
children: actions.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
2171
|
+
className: "px-3 py-6 text-center text-xs text-muted-foreground",
|
|
2172
|
+
children: "No actions yet"
|
|
2173
|
+
}) : actions.map((action) => /* @__PURE__ */ jsx(ActionItem, {
|
|
2174
|
+
action,
|
|
2175
|
+
model
|
|
2176
|
+
}, action.id))
|
|
2177
|
+
})]
|
|
2178
|
+
})]
|
|
2179
|
+
});
|
|
2180
|
+
});
|
|
2181
|
+
|
|
2182
|
+
//#endregion
|
|
2183
|
+
//#region src/components/FullScreenBrowser.tsx
|
|
2184
|
+
const REPO_SERVICES = new Set([
|
|
2185
|
+
"github",
|
|
2186
|
+
"gitlab",
|
|
2187
|
+
"bitbucket",
|
|
2188
|
+
"gitea"
|
|
2189
|
+
]);
|
|
2190
|
+
const FullScreenBrowser = observer(({ dashboardModel, connectionManager }) => {
|
|
2191
|
+
const cell = dashboardModel.selectedCell;
|
|
2192
|
+
if (!cell) return null;
|
|
2193
|
+
const { serviceId, capabilityId } = cell;
|
|
2194
|
+
const service = dashboardModel.selectedService;
|
|
2195
|
+
if (!service) return null;
|
|
2196
|
+
const selectedRepo = dashboardModel.getSelectedRepo(serviceId);
|
|
2197
|
+
const hasRepo = REPO_SERVICES.has(serviceId) && selectedRepo;
|
|
2198
|
+
const browserPath = dashboardModel.browserPath;
|
|
2199
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2200
|
+
className: "flex flex-col h-full flex-1 min-h-0",
|
|
2201
|
+
children: [
|
|
2202
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2203
|
+
className: "flex items-center gap-1 px-2 sm:px-4 py-2 bg-muted border-b border-border text-xs sm:text-sm flex-shrink-0 min-h-[40px]",
|
|
2204
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2205
|
+
className: "flex items-center gap-1 flex-1 min-w-0 flex-wrap",
|
|
2206
|
+
children: [/* @__PURE__ */ jsx(PathBreadcrumb, {
|
|
2207
|
+
path: browserPath,
|
|
2208
|
+
onNavigate: (path) => dashboardModel.setBrowserPath(path),
|
|
2209
|
+
showHome: false,
|
|
2210
|
+
editable: true,
|
|
2211
|
+
prefixSegments: [
|
|
2212
|
+
{
|
|
2213
|
+
label: "Dashboard",
|
|
2214
|
+
onClick: () => dashboardModel.closePanel()
|
|
2215
|
+
},
|
|
2216
|
+
{
|
|
2217
|
+
label: service.name,
|
|
2218
|
+
onClick: () => dashboardModel.closePanel()
|
|
2219
|
+
},
|
|
2220
|
+
{
|
|
2221
|
+
label: CAPABILITY_LABELS[capabilityId],
|
|
2222
|
+
onClick: () => dashboardModel.setBrowserPath("/")
|
|
2223
|
+
},
|
|
2224
|
+
...hasRepo ? [{ label: `${selectedRepo.owner}/${selectedRepo.repo}` }] : []
|
|
2225
|
+
]
|
|
2226
|
+
}), hasRepo && /* @__PURE__ */ jsxs("button", {
|
|
2227
|
+
onClick: () => dashboardModel.clearSelectedRepo(serviceId),
|
|
2228
|
+
className: "ml-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-muted hover:bg-muted/80 transition-colors",
|
|
2229
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2230
|
+
className: "hidden sm:inline",
|
|
2231
|
+
children: "Change"
|
|
2232
|
+
}), /* @__PURE__ */ jsx(ArrowLeft, { className: "h-3 w-3 sm:hidden" })]
|
|
2233
|
+
})]
|
|
2234
|
+
}), /* @__PURE__ */ jsx(ActionHistoryPanel, { model: dashboardModel.actionNotifications })]
|
|
2235
|
+
}),
|
|
2236
|
+
/* @__PURE__ */ jsx(ScopeWarningBanner, {
|
|
2237
|
+
serviceId,
|
|
2238
|
+
capabilityId,
|
|
2239
|
+
connectionManager
|
|
2240
|
+
}),
|
|
2241
|
+
/* @__PURE__ */ jsx("div", {
|
|
2242
|
+
className: "flex-1 min-h-0 overflow-hidden",
|
|
2243
|
+
children: /* @__PURE__ */ jsx(Suspense, {
|
|
2244
|
+
fallback: /* @__PURE__ */ jsx("div", {
|
|
2245
|
+
className: "flex items-center justify-center h-64",
|
|
2246
|
+
children: /* @__PURE__ */ jsx(LoadingSpinner, { label: "Loading..." })
|
|
2247
|
+
}),
|
|
2248
|
+
children: /* @__PURE__ */ jsx(CapabilityContent, {
|
|
2249
|
+
serviceId,
|
|
2250
|
+
capabilityId,
|
|
2251
|
+
connectionManager,
|
|
2252
|
+
dashboardModel
|
|
2253
|
+
})
|
|
2254
|
+
})
|
|
2255
|
+
})
|
|
2256
|
+
]
|
|
2257
|
+
});
|
|
2258
|
+
});
|
|
2259
|
+
|
|
2260
|
+
//#endregion
|
|
2261
|
+
//#region src/components/ServiceDashboard.tsx
|
|
2262
|
+
const COLUMN_HEADERS = [
|
|
2263
|
+
{
|
|
2264
|
+
label: "Service",
|
|
2265
|
+
className: "text-left"
|
|
2266
|
+
},
|
|
2267
|
+
{
|
|
2268
|
+
label: "Status",
|
|
2269
|
+
className: "text-left"
|
|
2270
|
+
},
|
|
2271
|
+
{
|
|
2272
|
+
label: "FS",
|
|
2273
|
+
className: "text-center"
|
|
2274
|
+
},
|
|
2275
|
+
{
|
|
2276
|
+
label: "Obj Storage",
|
|
2277
|
+
className: "text-center"
|
|
2278
|
+
},
|
|
2279
|
+
{
|
|
2280
|
+
label: "Git",
|
|
2281
|
+
className: "text-center"
|
|
2282
|
+
},
|
|
2283
|
+
{
|
|
2284
|
+
label: "Media",
|
|
2285
|
+
className: "text-center"
|
|
2286
|
+
},
|
|
2287
|
+
{
|
|
2288
|
+
label: "Contacts",
|
|
2289
|
+
className: "text-center"
|
|
2290
|
+
},
|
|
2291
|
+
{
|
|
2292
|
+
label: "Calendar",
|
|
2293
|
+
className: "text-center"
|
|
2294
|
+
}
|
|
2295
|
+
];
|
|
2296
|
+
const ServiceDashboard = observer(({ connectionManager, dashboardModel }) => {
|
|
2297
|
+
const services = serviceRegistry.getAll();
|
|
2298
|
+
if (dashboardModel.panelOpen && dashboardModel.selectedCell) return /* @__PURE__ */ jsx("div", {
|
|
2299
|
+
className: "animate-in fade-in duration-200 h-full flex flex-col min-h-0",
|
|
2300
|
+
children: /* @__PURE__ */ jsx(FullScreenBrowser, {
|
|
2301
|
+
dashboardModel,
|
|
2302
|
+
connectionManager
|
|
2303
|
+
})
|
|
2304
|
+
});
|
|
2305
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
|
|
2306
|
+
className: "hidden md:block",
|
|
2307
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
2308
|
+
className: "overflow-x-auto rounded-lg border border-border",
|
|
2309
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
2310
|
+
className: "w-full text-sm",
|
|
2311
|
+
children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", {
|
|
2312
|
+
className: "bg-muted border-b border-border",
|
|
2313
|
+
children: COLUMN_HEADERS.map((col) => /* @__PURE__ */ jsx("th", {
|
|
2314
|
+
className: `px-3 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider ${col.className}`,
|
|
2315
|
+
children: col.label
|
|
2316
|
+
}, col.label))
|
|
2317
|
+
}) }), /* @__PURE__ */ jsx("tbody", {
|
|
2318
|
+
className: "bg-card divide-y divide-border",
|
|
2319
|
+
children: services.map((service) => /* @__PURE__ */ jsx(ServiceRow, {
|
|
2320
|
+
service,
|
|
2321
|
+
connectionManager,
|
|
2322
|
+
dashboardModel
|
|
2323
|
+
}, service.id))
|
|
2324
|
+
})]
|
|
2325
|
+
})
|
|
2326
|
+
})
|
|
2327
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2328
|
+
className: "md:hidden space-y-3",
|
|
2329
|
+
children: services.map((service) => /* @__PURE__ */ jsx(ServiceCard, {
|
|
2330
|
+
service,
|
|
2331
|
+
connectionManager,
|
|
2332
|
+
dashboardModel
|
|
2333
|
+
}, service.id))
|
|
2334
|
+
})] });
|
|
2335
|
+
});
|
|
2336
|
+
|
|
2337
|
+
//#endregion
|
|
2338
|
+
//#region src/components/ConnectionStatus.tsx
|
|
2339
|
+
const ConnectionStatusIndicator = ({ status }) => {
|
|
2340
|
+
const config = match(status).with("disconnected", () => ({
|
|
2341
|
+
status: "neutral",
|
|
2342
|
+
label: "Disconnected"
|
|
2343
|
+
})).with("connecting", () => ({
|
|
2344
|
+
status: "warning",
|
|
2345
|
+
label: "Connecting...",
|
|
2346
|
+
pulse: true
|
|
2347
|
+
})).with("connected", () => ({
|
|
2348
|
+
status: "success",
|
|
2349
|
+
label: "Connected"
|
|
2350
|
+
})).with("expired", () => ({
|
|
2351
|
+
status: "warning",
|
|
2352
|
+
label: "Expired"
|
|
2353
|
+
})).with("error", () => ({
|
|
2354
|
+
status: "error",
|
|
2355
|
+
label: "Error"
|
|
2356
|
+
})).with("not_configured", () => ({
|
|
2357
|
+
status: "neutral",
|
|
2358
|
+
label: "Not Configured"
|
|
2359
|
+
})).with("loading", () => ({
|
|
2360
|
+
status: "neutral",
|
|
2361
|
+
label: "Loading..."
|
|
2362
|
+
})).exhaustive();
|
|
2363
|
+
return /* @__PURE__ */ jsx(StatusIndicator, {
|
|
2364
|
+
status: config.status,
|
|
2365
|
+
label: config.label,
|
|
2366
|
+
pulse: config.status === "warning" && status === "connecting"
|
|
2367
|
+
});
|
|
2368
|
+
};
|
|
2369
|
+
|
|
2370
|
+
//#endregion
|
|
2371
|
+
//#region src/components/GitHubRepoPicker.tsx
|
|
2372
|
+
function GitHubRepoPicker({ accessToken, onSelectRepo }) {
|
|
2373
|
+
const [repos, setRepos] = useState([]);
|
|
2374
|
+
const [loading, setLoading] = useState(true);
|
|
2375
|
+
const [error, setError] = useState(null);
|
|
2376
|
+
const [search, setSearch] = useState("");
|
|
2377
|
+
useEffect(() => {
|
|
2378
|
+
let cancelled = false;
|
|
2379
|
+
async function fetchRepos() {
|
|
2380
|
+
setLoading(true);
|
|
2381
|
+
setError(null);
|
|
2382
|
+
try {
|
|
2383
|
+
const res = await fetch("https://api.github.com/user/repos?sort=updated&per_page=50", { headers: { Authorization: `Bearer ${accessToken}` } });
|
|
2384
|
+
if (!res.ok) throw new Error(`GitHub API error: ${res.status}`);
|
|
2385
|
+
const data = await res.json();
|
|
2386
|
+
if (!cancelled) setRepos(data);
|
|
2387
|
+
} catch (err) {
|
|
2388
|
+
if (!cancelled) setError(err instanceof Error ? err.message : "Failed to fetch repos");
|
|
2389
|
+
} finally {
|
|
2390
|
+
if (!cancelled) setLoading(false);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
fetchRepos();
|
|
2394
|
+
return () => {
|
|
2395
|
+
cancelled = true;
|
|
2396
|
+
};
|
|
2397
|
+
}, [accessToken]);
|
|
2398
|
+
const filtered = useMemo(() => repos.filter((r) => r.full_name.toLowerCase().includes(search.toLowerCase())), [repos, search]);
|
|
2399
|
+
if (loading) return /* @__PURE__ */ jsxs("div", {
|
|
2400
|
+
className: "flex items-center justify-center py-8 text-gray-400",
|
|
2401
|
+
children: [/* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5 animate-spin mr-2" }), "Loading repositories..."]
|
|
2402
|
+
});
|
|
2403
|
+
if (error) return /* @__PURE__ */ jsx("div", {
|
|
2404
|
+
className: "rounded-lg border border-red-800 bg-red-950/50 p-4 text-red-300 text-sm",
|
|
2405
|
+
children: error
|
|
2406
|
+
});
|
|
2407
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2408
|
+
className: "flex flex-col gap-2",
|
|
2409
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2410
|
+
className: "relative",
|
|
2411
|
+
children: [/* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" }), /* @__PURE__ */ jsx("input", {
|
|
2412
|
+
type: "text",
|
|
2413
|
+
placeholder: "Search repositories...",
|
|
2414
|
+
value: search,
|
|
2415
|
+
onChange: (e) => setSearch(e.target.value),
|
|
2416
|
+
className: "w-full rounded-md border border-gray-700 bg-gray-900 py-2 pl-9 pr-3 text-sm text-gray-200 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
2417
|
+
})]
|
|
2418
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2419
|
+
className: "max-h-64 overflow-y-auto rounded-md border border-gray-700",
|
|
2420
|
+
children: filtered.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
2421
|
+
className: "px-4 py-6 text-center text-sm text-gray-500",
|
|
2422
|
+
children: "No repositories found"
|
|
2423
|
+
}) : filtered.map((repo) => {
|
|
2424
|
+
const parts = repo.full_name.split("/");
|
|
2425
|
+
const owner = parts[0] ?? "";
|
|
2426
|
+
const name = parts[1] ?? "";
|
|
2427
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
2428
|
+
type: "button",
|
|
2429
|
+
onClick: () => onSelectRepo({
|
|
2430
|
+
owner,
|
|
2431
|
+
repo: name
|
|
2432
|
+
}),
|
|
2433
|
+
className: "flex w-full items-start gap-3 border-b border-gray-800 px-4 py-3 text-left hover:bg-gray-800/60 last:border-b-0 transition-colors",
|
|
2434
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2435
|
+
className: "min-w-0 flex-1",
|
|
2436
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2437
|
+
className: "flex items-center gap-2",
|
|
2438
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2439
|
+
className: "text-sm font-medium text-blue-400 truncate",
|
|
2440
|
+
title: repo.full_name,
|
|
2441
|
+
children: repo.full_name
|
|
2442
|
+
}), repo.private && /* @__PURE__ */ jsx(Lock, { className: "h-3 w-3 flex-shrink-0 text-gray-500" })]
|
|
2443
|
+
}), repo.description && /* @__PURE__ */ jsx("p", {
|
|
2444
|
+
className: "mt-0.5 text-xs text-gray-500 truncate",
|
|
2445
|
+
title: repo.description,
|
|
2446
|
+
children: repo.description.length > 80 ? `${repo.description.slice(0, 80)}...` : repo.description
|
|
2447
|
+
})]
|
|
2448
|
+
}), repo.language && /* @__PURE__ */ jsx("span", {
|
|
2449
|
+
className: "flex-shrink-0 rounded-full bg-gray-800 px-2 py-0.5 text-xs text-gray-400",
|
|
2450
|
+
children: repo.language
|
|
2451
|
+
})]
|
|
2452
|
+
}, repo.full_name);
|
|
2453
|
+
})
|
|
2454
|
+
})]
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
//#endregion
|
|
2459
|
+
//#region src/demos/ServiceDashboardDemo.tsx
|
|
2460
|
+
const ServiceDashboardDemo = observer(({ authBaseURL, initialService, initialCapability, initialPath, onCellChange, onPathChange }) => {
|
|
2461
|
+
const connectionManager = useMemo(() => {
|
|
2462
|
+
const authClient = createConnectAuthClient(authBaseURL);
|
|
2463
|
+
return new ConnectionManagerModel({ authClient });
|
|
2464
|
+
}, [authBaseURL]);
|
|
2465
|
+
const dashboardModel = useMemo(() => {
|
|
2466
|
+
const model = new DashboardModel(connectionManager);
|
|
2467
|
+
if (initialService && initialCapability && connectionManager.isConnected(initialService)) {
|
|
2468
|
+
if (initialPath) model.setBrowserPathSilent(initialPath);
|
|
2469
|
+
model.openCell(initialService, initialCapability);
|
|
2470
|
+
}
|
|
2471
|
+
return model;
|
|
2472
|
+
}, [connectionManager]);
|
|
2473
|
+
dashboardModel.onCellChange = onCellChange;
|
|
2474
|
+
dashboardModel.onPathChange = onPathChange;
|
|
2475
|
+
useEffect(() => {
|
|
2476
|
+
connectionManager.initialize();
|
|
2477
|
+
}, [connectionManager]);
|
|
2478
|
+
const isConnected = initialService ? connectionManager.isConnected(initialService) : false;
|
|
2479
|
+
useEffect(() => {
|
|
2480
|
+
if (!initialService || !initialCapability) {
|
|
2481
|
+
if (dashboardModel.panelOpen) dashboardModel.closePanelSilent();
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
if (!connectionManager.initialized || !isConnected) return;
|
|
2485
|
+
const modelCell = dashboardModel.selectedCell;
|
|
2486
|
+
if (!dashboardModel.panelOpen || modelCell?.serviceId !== initialService || modelCell?.capabilityId !== initialCapability) {
|
|
2487
|
+
if (initialPath) dashboardModel.setBrowserPathSilent(initialPath);
|
|
2488
|
+
dashboardModel.openCell(initialService, initialCapability);
|
|
2489
|
+
}
|
|
2490
|
+
}, [
|
|
2491
|
+
initialService,
|
|
2492
|
+
initialCapability,
|
|
2493
|
+
initialPath,
|
|
2494
|
+
isConnected,
|
|
2495
|
+
connectionManager.initialized,
|
|
2496
|
+
dashboardModel
|
|
2497
|
+
]);
|
|
2498
|
+
if (connectionManager.configError) return /* @__PURE__ */ jsx("div", {
|
|
2499
|
+
className: "p-4",
|
|
2500
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2501
|
+
className: "rounded-lg border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950/30 p-4",
|
|
2502
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
2503
|
+
className: "text-sm font-medium text-red-800 dark:text-red-300",
|
|
2504
|
+
children: "Dashboard cannot start"
|
|
2505
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
2506
|
+
className: "mt-1 text-sm text-red-600 dark:text-red-400",
|
|
2507
|
+
children: connectionManager.configError
|
|
2508
|
+
})]
|
|
2509
|
+
})
|
|
2510
|
+
});
|
|
2511
|
+
const showNotConnectedBanner = initialService && initialCapability && connectionManager.initialized && !connectionManager.isConnected(initialService) && !dashboardModel.panelOpen;
|
|
2512
|
+
const bannerService = showNotConnectedBanner ? serviceRegistry.get(initialService) : null;
|
|
2513
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2514
|
+
className: dashboardModel.panelOpen ? "flex-1 flex flex-col min-h-0" : "flex-1 min-h-0 overflow-auto p-4",
|
|
2515
|
+
children: [showNotConnectedBanner && bannerService && /* @__PURE__ */ jsxs("div", {
|
|
2516
|
+
className: "mb-4 rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/30 p-4",
|
|
2517
|
+
children: [/* @__PURE__ */ jsxs("p", {
|
|
2518
|
+
className: "text-sm font-medium text-amber-800 dark:text-amber-300",
|
|
2519
|
+
children: [bannerService.name, " is not connected"]
|
|
2520
|
+
}), /* @__PURE__ */ jsxs("p", {
|
|
2521
|
+
className: "mt-1 text-sm text-amber-600 dark:text-amber-400",
|
|
2522
|
+
children: [
|
|
2523
|
+
"Click the ",
|
|
2524
|
+
/* @__PURE__ */ jsx("strong", { children: "Connect" }),
|
|
2525
|
+
" button next to ",
|
|
2526
|
+
bannerService.name,
|
|
2527
|
+
" in the table below to sign in. The browser will open automatically once connected."
|
|
2528
|
+
]
|
|
2529
|
+
})]
|
|
2530
|
+
}), /* @__PURE__ */ jsx(ServiceDashboard, {
|
|
2531
|
+
connectionManager,
|
|
2532
|
+
dashboardModel
|
|
2533
|
+
})]
|
|
2534
|
+
});
|
|
2535
|
+
});
|
|
2536
|
+
|
|
2537
|
+
//#endregion
|
|
2538
|
+
export { ActionHistoryPanel, ActionNotificationModel, CapabilityCell, CapabilityPanel, CapabilityPill, ConnectButton, ConnectedMenu, ConnectionManagerModel, ConnectionStatusIndicator, CredentialForm, CredentialFormModel, DashboardModel, FullScreenBrowser, GitHostBrowser, GitHostBrowserModel, GitHubRepoPicker, GitRepoBrowser, GitRepoBrowserModel, ObjectStorageBrowser, ObjectStorageBrowserModel, RepoPicker, RepoPickerModel, ServiceCard, ServiceDashboard, ServiceDashboardDemo, ServiceIcon, ServiceRow, TokenManager, bitbucketService, browserFsService, createConnectAuthClient, dropboxService, getScopeLabel, getScopeLabels, giteaService, githubService, gitlabService, googleService, icloudService, indexeddbService, microsoftService, s3Service, serviceRegistry, showActionToast, showErrorToast, showInfoToast, webdavService };
|
|
2539
|
+
//# sourceMappingURL=index.js.map
|