@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GitBrowser-BLgTNQyd.js","names":["gitRepo: IGitRepo","_gitHost: IGitHost | undefined","createFileSystem: (branch: string) => Promise<IFileSystem>","onError?: (err: { message: string }) => void","fs: IFileSystem","ref: string","commitList: GitCommit[]","commit: GitCommit","entries: GitDiffEntry[]","data: PullRequest[]","data: Issue[]","host: IGitHost","tab: SidebarTab","branch: GitBranch","gitHost: IGitHost","data: PullRequest[]","data: Issue[]","tab: HostTab","model: ActionNotificationModel","type: ActionType","description: string","options?: {\n undo?: () => Promise<void>;\n /** Duration in ms before auto-dismiss. Default 5000. */\n duration?: number;\n }","message: string","context?: string","date: Date","e: MouseEvent","items: RefItem[]","GitRepoBrowser: React.FC<GitRepoBrowserProps>","type: 'success' | 'error' | 'warning'","message: string","sidebarTabs: Array<{ id: SidebarTab; label: string; icon: React.ReactNode }>","status: string","GitHostBrowser: React.FC<GitHostBrowserProps>","tabDefs: Array<{ id: 'prs' | 'issues'; label: string; icon: React.ReactNode }>"],"sources":["../src/models/GitRepoBrowserModel.ts","../src/models/GitHostBrowserModel.ts","../src/utils/action-toast.ts","../src/components/GitBrowser.tsx"],"sourcesContent":["import { makeAutoObservable, flow } from 'mobx';\nimport type {\n IGitRepo,\n IGitHost,\n GitBranch,\n GitTag,\n GitCommit,\n GitDiffEntry,\n IFileSystem,\n PullRequest,\n Issue,\n} from '@anymux/file-system';\n\nexport type SidebarTab = 'files' | 'branches' | 'commits' | 'prs' | 'issues';\n\nconst HOST_PAGE_SIZE = 25;\nconst COMMITS_PAGE_SIZE = 25;\n\nexport class GitRepoBrowserModel {\n // --- Core repo state ---\n branches: GitBranch[] = [];\n tags: GitTag[] = [];\n currentRef = '';\n fileSystem: IFileSystem | null = null;\n loading = true;\n error: string | null = null;\n\n // --- Tab state ---\n activeTab: SidebarTab = 'files';\n\n // --- Commits state ---\n commits: GitCommit[] = [];\n commitsLoading = false;\n commitsPage = 0;\n hasMoreCommits = false;\n\n // --- Diff state ---\n selectedCommitSha: string | undefined = undefined;\n diffEntries: GitDiffEntry[] = [];\n diffLoading = false;\n\n // --- Host data (PRs / Issues) ---\n prs: PullRequest[] = [];\n issues: Issue[] = [];\n hostLoading = false;\n hostError: string | null = null;\n prPage = 0;\n issuePage = 0;\n hasMorePrs = false;\n hasMoreIssues = false;\n\n constructor(\n private gitRepo: IGitRepo,\n private _gitHost: IGitHost | undefined,\n private createFileSystem: (branch: string) => Promise<IFileSystem>,\n private onError?: (err: { message: string }) => void,\n ) {\n makeAutoObservable(this, {\n // Mark constructor deps as non-observable (they are stable references)\n });\n }\n\n /** Update the gitHost adapter (e.g. when loaded asynchronously after construction) */\n setGitHost(host: IGitHost): void {\n this._gitHost = host;\n }\n\n // --- Computed ---\n\n get headSha(): string | undefined {\n return this.branches.find((b) => b.name === this.currentRef)?.sha;\n }\n\n get hasGitHost(): boolean {\n return !!this._gitHost;\n }\n\n // --- Actions ---\n\n setActiveTab(tab: SidebarTab): void {\n this.activeTab = tab;\n\n // Trigger data loading for the newly active tab\n if (tab === 'commits' && this.currentRef) {\n this.loadCommits();\n } else if (tab === 'prs' && this._gitHost) {\n this.loadPRs();\n } else if (tab === 'issues' && this._gitHost) {\n this.loadIssues();\n }\n }\n\n // --- Async flows ---\n\n initialize = flow(function* (this: GitRepoBrowserModel) {\n this.loading = true;\n this.error = null;\n\n try {\n const [branchList, tagList]: [GitBranch[], GitTag[]] = yield Promise.all([\n this.gitRepo.listBranches(),\n this.gitRepo.listTags().catch(() => [] as GitTag[]),\n ]);\n\n this.branches = branchList;\n this.tags = tagList;\n\n // Pick default branch\n const defaultBranch = branchList.find((b) => b.isDefault);\n const initialRef = defaultBranch?.name ?? branchList[0]?.name ?? 'main';\n this.currentRef = initialRef;\n\n // Create file system for the default branch\n const fs: IFileSystem = yield this.createFileSystem(initialRef);\n this.fileSystem = fs;\n } catch (err) {\n const msg = err instanceof Error ? err.message : 'Failed to load repository';\n this.error = msg;\n this.onError?.({ message: msg });\n } finally {\n this.loading = false;\n }\n });\n\n switchRef = flow(function* (this: GitRepoBrowserModel, ref: string) {\n if (ref === this.currentRef) return;\n\n this.currentRef = ref;\n this.loading = true;\n this.error = null;\n this.selectedCommitSha = undefined;\n this.diffEntries = [];\n this.commitsPage = 0;\n\n try {\n const fs: IFileSystem = yield this.createFileSystem(ref);\n this.fileSystem = fs;\n } catch (err) {\n const msg = err instanceof Error ? err.message : 'Failed to switch ref';\n this.error = msg;\n this.onError?.({ message: msg });\n } finally {\n this.loading = false;\n }\n\n // Reload commits if the commits tab is active\n if (this.activeTab === 'commits') {\n this.loadCommits();\n }\n });\n\n loadCommits = flow(function* (this: GitRepoBrowserModel) {\n if (!this.currentRef) return;\n\n this.commitsLoading = true;\n try {\n const fetchCount = COMMITS_PAGE_SIZE * (this.commitsPage + 1);\n const commitList: GitCommit[] = yield this.gitRepo.listCommits({\n ref: this.currentRef,\n maxCount: fetchCount + 1, // Fetch one extra to detect if more exist\n });\n const start = this.commitsPage * COMMITS_PAGE_SIZE;\n this.commits = commitList.slice(start, start + COMMITS_PAGE_SIZE);\n this.hasMoreCommits = commitList.length > start + COMMITS_PAGE_SIZE;\n } catch (err) {\n console.error('[GitRepoBrowserModel] Failed to load commits:', err);\n this.commits = [];\n this.hasMoreCommits = false;\n } finally {\n this.commitsLoading = false;\n }\n });\n\n selectCommit = flow(function* (this: GitRepoBrowserModel, commit: GitCommit) {\n this.selectedCommitSha = commit.sha;\n\n if (commit.parents.length === 0) {\n this.diffEntries = [];\n return;\n }\n\n this.diffLoading = true;\n try {\n const entries: GitDiffEntry[] = yield this.gitRepo.diff(\n commit.parents[0]!,\n commit.sha,\n );\n this.diffEntries = entries;\n } catch {\n this.diffEntries = [];\n } finally {\n this.diffLoading = false;\n }\n });\n\n loadPRs = flow(function* (this: GitRepoBrowserModel) {\n if (!this._gitHost) return;\n\n this.hostLoading = true;\n this.hostError = null;\n try {\n const data: PullRequest[] = yield this._gitHost.listPullRequests({\n state: 'all',\n maxResults: HOST_PAGE_SIZE * (this.prPage + 1),\n });\n const start = this.prPage * HOST_PAGE_SIZE;\n this.prs = data.slice(start, start + HOST_PAGE_SIZE);\n this.hasMorePrs = data.length > start + HOST_PAGE_SIZE;\n } catch (err) {\n this.hostError = err instanceof Error ? err.message : 'Failed to load';\n } finally {\n this.hostLoading = false;\n }\n });\n\n loadIssues = flow(function* (this: GitRepoBrowserModel) {\n if (!this._gitHost) return;\n\n this.hostLoading = true;\n this.hostError = null;\n try {\n const data: Issue[] = yield this._gitHost.listIssues({\n state: 'all',\n maxResults: HOST_PAGE_SIZE * (this.issuePage + 1),\n });\n const start = this.issuePage * HOST_PAGE_SIZE;\n this.issues = data.slice(start, start + HOST_PAGE_SIZE);\n this.hasMoreIssues = data.length > start + HOST_PAGE_SIZE;\n } catch (err) {\n this.hostError = err instanceof Error ? err.message : 'Failed to load';\n } finally {\n this.hostLoading = false;\n }\n });\n\n // --- Pagination helpers ---\n\n nextCommitsPage(): void {\n this.commitsPage += 1;\n this.loadCommits();\n }\n\n prevCommitsPage(): void {\n this.commitsPage = Math.max(0, this.commitsPage - 1);\n this.loadCommits();\n }\n\n nextPrPage(): void {\n this.prPage += 1;\n this.loadPRs();\n }\n\n prevPrPage(): void {\n this.prPage = Math.max(0, this.prPage - 1);\n this.loadPRs();\n }\n\n nextIssuePage(): void {\n this.issuePage += 1;\n this.loadIssues();\n }\n\n prevIssuePage(): void {\n this.issuePage = Math.max(0, this.issuePage - 1);\n this.loadIssues();\n }\n\n retryPRs(): void {\n this.hostError = null;\n this.prPage = 0;\n this.loadPRs();\n }\n\n retryIssues(): void {\n this.hostError = null;\n this.issuePage = 0;\n this.loadIssues();\n }\n\n // --- Branch selection from BranchList tab ---\n\n selectBranch(branch: GitBranch): void {\n this.switchRef(branch.name);\n }\n}\n","import { makeAutoObservable, flow } from 'mobx';\nimport type { IGitHost, PullRequest, Issue } from '@anymux/file-system';\n\nexport type HostTab = 'prs' | 'issues';\n\nconst PAGE_SIZE = 25;\n\nexport class GitHostBrowserModel {\n gitHost: IGitHost;\n\n activeTab: HostTab = 'prs';\n prs: PullRequest[] = [];\n issues: Issue[] = [];\n hasMorePrs = false;\n hasMoreIssues = false;\n page = 0;\n loading = false;\n error: string | null = null;\n\n constructor(gitHost: IGitHost) {\n this.gitHost = gitHost;\n makeAutoObservable(this, { gitHost: false });\n }\n\n get currentItems() {\n return this.activeTab === 'prs' ? this.prs : this.issues;\n }\n\n get hasMore() {\n return this.activeTab === 'prs' ? this.hasMorePrs : this.hasMoreIssues;\n }\n\n get showPagination() {\n return this.page > 0 || this.hasMore;\n }\n\n setActiveTab(tab: HostTab) {\n this.activeTab = tab;\n this.page = 0;\n this.loadData();\n }\n\n nextPage() {\n if (!this.hasMore) return;\n this.page += 1;\n this.loadData();\n }\n\n prevPage() {\n if (this.page <= 0) return;\n this.page -= 1;\n this.loadData();\n }\n\n retry() {\n this.error = null;\n this.page = 0;\n this.loadData();\n }\n\n loadData = flow(function* loadData(this: GitHostBrowserModel) {\n this.loading = true;\n this.error = null;\n\n try {\n if (this.activeTab === 'prs') {\n const data: PullRequest[] = yield this.gitHost.listPullRequests({\n state: 'all',\n maxResults: PAGE_SIZE * (this.page + 1),\n });\n const start = this.page * PAGE_SIZE;\n this.prs = data.slice(start, start + PAGE_SIZE);\n this.hasMorePrs = data.length > start + PAGE_SIZE;\n } else {\n const data: Issue[] = yield this.gitHost.listIssues({\n state: 'all',\n maxResults: PAGE_SIZE * (this.page + 1),\n });\n const start = this.page * PAGE_SIZE;\n this.issues = data.slice(start, start + PAGE_SIZE);\n this.hasMoreIssues = data.length > start + PAGE_SIZE;\n }\n } catch (err) {\n this.error = err instanceof Error ? err.message : 'Failed to load data';\n } finally {\n this.loading = false;\n }\n });\n}\n","import { toast } from 'sonner';\nimport type { ActionNotificationModel, ActionType } from '../models/ActionNotificationModel';\n\n/**\n * Show a toast notification for a user action, with optional undo.\n * Records the action in the ActionNotificationModel for history.\n */\nexport function showActionToast(\n model: ActionNotificationModel,\n type: ActionType,\n description: string,\n options?: {\n undo?: () => Promise<void>;\n /** Duration in ms before auto-dismiss. Default 5000. */\n duration?: number;\n },\n): string {\n const actionId = model.record(type, description, options?.undo);\n\n if (options?.undo) {\n toast(description, {\n duration: options.duration ?? 5000,\n action: {\n label: 'Undo',\n onClick: async () => {\n try {\n await options.undo!();\n model.markUndone(actionId);\n toast.success('Action undone');\n } catch (err) {\n toast.error(`Undo failed: ${err instanceof Error ? err.message : 'Unknown error'}`);\n }\n },\n },\n });\n } else {\n toast.success(description, {\n duration: options?.duration ?? 3000,\n });\n }\n\n return actionId;\n}\n\n/** Show an error toast */\nexport function showErrorToast(message: string, context?: string) {\n toast.error(context ? `${context}: ${message}` : message);\n}\n\n/** Show an info toast */\nexport function showInfoToast(message: string) {\n toast.info(message);\n}\n","import React, { useState, useEffect, useRef, useCallback, Suspense } from 'react';\nimport { observer } from 'mobx-react-lite';\nimport {\n GitBranch as GitBranchIcon,\n Tag as TagIcon,\n GitPullRequest,\n AlertCircle,\n Loader2,\n ChevronDown,\n ChevronLeft,\n ChevronRight,\n Check,\n FolderTree,\n GitCommit as GitCommitIcon,\n Diff,\n} from 'lucide-react';\nimport { toast } from 'sonner';\nimport type { ActionNotificationModel } from '../models/ActionNotificationModel';\nimport { showActionToast, showErrorToast } from '../utils/action-toast';\nimport { BrowserError } from '@anymux/ui/components/browser-error';\nimport type {\n IGitRepo,\n GitBranch,\n GitTag,\n IGitHost,\n IFileSystem,\n} from '@anymux/file-system';\nimport { GitRepoBrowserModel, type SidebarTab } from '../models/GitRepoBrowserModel';\nimport { GitHostBrowserModel } from '../models/GitHostBrowserModel';\n\n// Lazy-load the heavy FileBrowser from fs-ui\nconst FileBrowser = React.lazy(() =>\n import('@anymux/fs-ui').then((m) => ({ default: m.FileBrowser }))\n);\n\nconst CommitList = React.lazy(() =>\n import('@anymux/fs-ui').then((m) => ({ default: m.CommitList }))\n);\n\nconst BranchList = React.lazy(() =>\n import('@anymux/fs-ui').then((m) => ({ default: m.BranchList }))\n);\n\nconst DiffViewer = React.lazy(() =>\n import('@anymux/fs-ui').then((m) => ({ default: m.DiffViewer }))\n);\n\n// ---- Helpers ----\n\nfunction formatRelativeTime(date: Date): string {\n const now = Date.now();\n const diffMs = now - new Date(date).getTime();\n const seconds = Math.floor(diffMs / 1000);\n const minutes = Math.floor(seconds / 60);\n const hours = Math.floor(minutes / 60);\n const days = Math.floor(hours / 24);\n const weeks = Math.floor(days / 7);\n const months = Math.floor(days / 30);\n\n if (seconds < 60) return 'just now';\n if (minutes < 60) return `${minutes}m ago`;\n if (hours < 24) return `${hours}h ago`;\n if (days < 7) return `${days}d ago`;\n if (weeks < 5) return `${weeks}w ago`;\n if (months < 12) return `${months}mo ago`;\n return new Date(date).toLocaleDateString();\n}\n\nfunction LoadingSpinner() {\n return (\n <div className=\"flex items-center justify-center py-8\">\n <Loader2 className=\"h-5 w-5 text-gray-400 animate-spin\" />\n </div>\n );\n}\n\nfunction ErrorMessage({ message }: { message: string }) {\n return (\n <div className=\"flex items-center gap-2 px-4 py-6 text-sm text-red-600 dark:text-red-400 justify-center\">\n <AlertCircle className=\"h-4 w-4 shrink-0\" />\n <span>{message}</span>\n </div>\n );\n}\n\n// ---- Branch/Tag Selector Dropdown ----\n\ntype RefType = 'branch' | 'tag';\ninterface RefItem {\n type: RefType;\n name: string;\n sha: string;\n isDefault?: boolean;\n}\n\ninterface RefSelectorProps {\n branches: GitBranch[];\n tags: GitTag[];\n currentRef: string;\n onSelectRef: (ref: string, type: RefType) => void;\n}\n\nfunction RefSelector({ branches, tags, currentRef, onSelectRef }: RefSelectorProps) {\n const [isOpen, setIsOpen] = useState(false);\n const [filterTab, setFilterTab] = useState<'branches' | 'tags'>('branches');\n const [search, setSearch] = useState('');\n const dropdownRef = useRef<HTMLDivElement>(null);\n\n // Close on click outside\n useEffect(() => {\n function handleClickOutside(e: MouseEvent) {\n if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {\n setIsOpen(false);\n }\n }\n if (isOpen) {\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }\n }, [isOpen]);\n\n const items: RefItem[] = filterTab === 'branches'\n ? branches.map((b) => ({ type: 'branch' as const, name: b.name, sha: b.sha, isDefault: b.isDefault }))\n : tags.map((t) => ({ type: 'tag' as const, name: t.name, sha: t.sha }));\n\n const filtered = search\n ? items.filter((i) => i.name.toLowerCase().includes(search.toLowerCase()))\n : items;\n\n return (\n <div ref={dropdownRef} className=\"relative\">\n <button\n onClick={() => setIsOpen(!isOpen)}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors\"\n >\n <GitBranchIcon className=\"h-3.5 w-3.5 text-gray-500\" />\n <span className=\"max-w-[160px] truncate\" title={currentRef}>{currentRef}</span>\n <ChevronDown className=\"h-3 w-3 text-gray-400\" />\n </button>\n\n {isOpen && (\n <div className=\"absolute left-0 top-full mt-1 z-50 w-72 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden\">\n {/* Search */}\n <div className=\"p-2 border-b border-gray-200 dark:border-gray-700\">\n <input\n type=\"text\"\n placeholder=\"Filter branches/tags...\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n className=\"w-full px-2.5 py-1.5 text-xs rounded-md border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 focus:outline-none focus:ring-1 focus:ring-blue-500\"\n autoFocus\n />\n </div>\n\n {/* Tabs */}\n <div className=\"flex border-b border-gray-200 dark:border-gray-700\">\n <button\n onClick={() => { setFilterTab('branches'); setSearch(''); }}\n className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors ${\n filterTab === 'branches'\n ? 'border-b-2 border-blue-500 text-blue-600 dark:text-blue-400'\n : 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'\n }`}\n >\n <GitBranchIcon className=\"h-3 w-3\" />\n Branches\n </button>\n <button\n onClick={() => { setFilterTab('tags'); setSearch(''); }}\n className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors ${\n filterTab === 'tags'\n ? 'border-b-2 border-blue-500 text-blue-600 dark:text-blue-400'\n : 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'\n }`}\n >\n <TagIcon className=\"h-3 w-3\" />\n Tags\n </button>\n </div>\n\n {/* Items */}\n <div className=\"max-h-64 overflow-auto\">\n {filtered.length === 0 ? (\n <div className=\"px-3 py-4 text-center text-xs text-gray-400\">\n {search ? 'No matches found' : `No ${filterTab} found`}\n </div>\n ) : (\n filtered.map((item) => (\n <button\n key={`${item.type}-${item.name}`}\n onClick={() => {\n onSelectRef(item.name, item.type);\n setIsOpen(false);\n setSearch('');\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors\"\n >\n <span className=\"w-4 flex-shrink-0\">\n {item.name === currentRef && (\n <Check className=\"h-3.5 w-3.5 text-blue-500\" />\n )}\n </span>\n <span className=\"text-xs truncate flex-1\" title={item.name}>{item.name}</span>\n {item.isDefault && (\n <span className=\"px-1.5 py-0.5 text-[9px] font-medium bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 rounded flex-shrink-0\">\n default\n </span>\n )}\n </button>\n ))\n )}\n </div>\n </div>\n )}\n </div>\n );\n}\n\n// ---- Git Repository File Browser ----\n\nexport interface GitRepoBrowserProps {\n gitRepo: IGitRepo;\n owner: string;\n repo: string;\n /** Creates an IFileSystem scoped to a particular branch/ref */\n createFileSystem: (branch: string) => Promise<IFileSystem>;\n /** Optional git host adapter for PRs/Issues tabs */\n gitHost?: IGitHost;\n onError?: (err: { message: string }) => void;\n /** Optional action notifications model for recording actions */\n actionNotifications?: ActionNotificationModel;\n}\n\nexport const GitRepoBrowser: React.FC<GitRepoBrowserProps> = observer(({\n gitRepo,\n owner,\n repo,\n createFileSystem,\n gitHost,\n onError,\n actionNotifications,\n}) => {\n const [model] = useState(\n () => new GitRepoBrowserModel(gitRepo, gitHost, createFileSystem, onError)\n );\n\n // When gitHost arrives asynchronously after model construction, inject it\n useEffect(() => {\n if (gitHost && !model.hasGitHost) {\n model.setGitHost(gitHost);\n }\n }, [gitHost, model]);\n\n const handleNotify = useCallback((type: 'success' | 'error' | 'warning', message: string) => {\n if (type === 'success' && actionNotifications) {\n const actionType = message.startsWith('Renamed') ? 'rename' as const\n : message.startsWith('Deleted') ? 'delete' as const\n : message.startsWith('Created') ? 'create' as const\n : message.startsWith('Uploaded') || message.includes('upload') ? 'upload' as const\n : 'create' as const;\n showActionToast(actionNotifications, actionType, message);\n } else if (type === 'error') {\n showErrorToast(message);\n } else {\n toast[type](message);\n }\n }, [actionNotifications]);\n\n useEffect(() => {\n model.initialize();\n }, [model]);\n\n if (model.loading && !model.fileSystem) {\n return <LoadingSpinner />;\n }\n\n if (model.error && !model.fileSystem) {\n return (\n <BrowserError\n error={model.error}\n context={`${owner}/${repo}`}\n onRetry={() => window.location.reload()}\n />\n );\n }\n\n const sidebarTabs: Array<{ id: SidebarTab; label: string; icon: React.ReactNode }> = [\n { id: 'files', label: 'Files', icon: <FolderTree className=\"h-3.5 w-3.5\" /> },\n { id: 'branches', label: 'Branches', icon: <GitBranchIcon className=\"h-3.5 w-3.5\" /> },\n { id: 'commits', label: 'Commits', icon: <GitCommitIcon className=\"h-3.5 w-3.5\" /> },\n ...(model.hasGitHost ? [\n { id: 'prs' as const, label: 'PRs', icon: <GitPullRequest className=\"h-3.5 w-3.5\" /> },\n { id: 'issues' as const, label: 'Issues', icon: <AlertCircle className=\"h-3.5 w-3.5\" /> },\n ] : []),\n ];\n\n return (\n <div className=\"flex flex-col h-full\">\n {/* Header with branch selector */}\n <div className=\"flex items-center gap-3 px-3 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 flex-shrink-0\">\n <RefSelector\n branches={model.branches}\n tags={model.tags}\n currentRef={model.currentRef}\n onSelectRef={(ref) => model.switchRef(ref)}\n />\n <div className=\"flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400 min-w-0\">\n <span className=\"font-medium text-gray-700 dark:text-gray-300 truncate\" title={owner}>\n {owner}\n </span>\n <span>/</span>\n <span className=\"font-medium text-gray-700 dark:text-gray-300 truncate\" title={repo}>\n {repo}\n </span>\n </div>\n </div>\n\n {/* Sidebar tabs */}\n <div className=\"flex border-b border-gray-200 dark:border-gray-700 flex-shrink-0\">\n {sidebarTabs.map((tab) => (\n <button\n key={tab.id}\n onClick={() => model.setActiveTab(tab.id)}\n className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium border-b-2 transition-colors ${\n model.activeTab === tab.id\n ? 'border-blue-500 text-blue-600 dark:text-blue-400'\n : 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'\n }`}\n >\n {tab.icon}\n {tab.label}\n </button>\n ))}\n </div>\n\n {/* Tab content */}\n <div className=\"flex-1 min-h-0 relative flex flex-col\">\n {model.loading && (\n <div className=\"absolute inset-0 bg-white/60 dark:bg-gray-900/60 z-10 flex items-center justify-center\">\n <Loader2 className=\"h-5 w-5 text-gray-400 animate-spin\" />\n </div>\n )}\n {model.error && model.fileSystem && (\n <div className=\"px-4 py-2 bg-red-50 dark:bg-red-900/20 text-xs text-red-600 dark:text-red-400 border-b border-red-200 dark:border-red-800 flex-shrink-0\">\n {model.error}\n </div>\n )}\n\n {/* Files tab */}\n {model.activeTab === 'files' && model.fileSystem && (\n <div className=\"flex-1 min-h-0\">\n <Suspense fallback={<LoadingSpinner />}>\n <FileBrowser\n key={model.currentRef}\n fileSystem={model.fileSystem}\n className=\"h-full\"\n onError={onError}\n onNotify={handleNotify}\n />\n </Suspense>\n </div>\n )}\n\n {/* Branches tab */}\n {model.activeTab === 'branches' && (\n <div className=\"flex-1 min-h-0\">\n <Suspense fallback={<LoadingSpinner />}>\n <BranchList\n branches={model.branches}\n currentBranch={model.currentRef}\n onSelectBranch={(branch) => model.selectBranch(branch)}\n className=\"h-full\"\n />\n </Suspense>\n </div>\n )}\n\n {/* Commits tab */}\n {model.activeTab === 'commits' && (\n <div className=\"flex-1 min-h-0 flex flex-col\">\n {model.commitsLoading ? (\n <LoadingSpinner />\n ) : (\n <>\n <Suspense fallback={<LoadingSpinner />}>\n <CommitList\n commits={model.commits}\n headSha={model.headSha}\n selectedSha={model.selectedCommitSha}\n onSelectCommit={(commit) => model.selectCommit(commit)}\n className={model.selectedCommitSha ? 'max-h-[50%] overflow-auto flex-shrink-0' : 'flex-1 overflow-auto'}\n />\n </Suspense>\n\n {/* Commits pagination */}\n {(model.commitsPage > 0 || model.hasMoreCommits) && (\n <div className=\"flex items-center justify-between px-4 py-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 flex-shrink-0\">\n <button\n onClick={() => model.prevCommitsPage()}\n disabled={model.commitsPage === 0}\n className=\"inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:pointer-events-none\"\n >\n <ChevronLeft className=\"h-3.5 w-3.5\" /> Previous\n </button>\n <span className=\"text-xs text-gray-500\">Page {model.commitsPage + 1}</span>\n <button\n onClick={() => model.nextCommitsPage()}\n disabled={!model.hasMoreCommits}\n className=\"inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:pointer-events-none\"\n >\n Next <ChevronRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n )}\n\n {/* Diff panel below commits when a commit is selected */}\n {model.selectedCommitSha && (\n <div className=\"flex-1 min-h-0 border-t border-gray-200 dark:border-gray-700 overflow-auto\">\n <div className=\"flex items-center gap-2 px-3 py-1.5 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex-shrink-0\">\n <Diff className=\"h-3.5 w-3.5 text-gray-400\" />\n <span className=\"text-[10px] font-medium text-gray-600 dark:text-gray-300\">\n Changes in{' '}\n <code className=\"font-mono text-blue-600 dark:text-blue-400\">\n {model.selectedCommitSha.slice(0, 7)}\n </code>\n </span>\n </div>\n {model.diffLoading ? (\n <LoadingSpinner />\n ) : (\n <Suspense fallback={<LoadingSpinner />}>\n <DiffViewer entries={model.diffEntries} className=\"p-2\" />\n </Suspense>\n )}\n </div>\n )}\n </>\n )}\n </div>\n )}\n\n {/* PRs tab */}\n {model.activeTab === 'prs' && model.hasGitHost && (\n <div className=\"flex-1 min-h-0 flex flex-col\">\n {model.hostLoading && <LoadingSpinner />}\n {!model.hostLoading && model.hostError && (\n <BrowserError error={model.hostError} context=\"Pull Requests\" onRetry={() => model.retryPRs()} />\n )}\n {!model.hostLoading && !model.hostError && (\n model.prs.length === 0 ? (\n <div className=\"flex items-center justify-center py-8 text-xs text-gray-400\">No pull requests found</div>\n ) : (\n <>\n <div className=\"flex-1 overflow-auto divide-y divide-gray-100 dark:divide-gray-800\">\n {model.prs.map((pr) => (\n <div key={pr.number} className=\"flex items-start gap-3 px-4 py-2.5 hover:bg-gray-50 dark:hover:bg-gray-800/50\">\n <GitPullRequest className=\"h-4 w-4 text-gray-400 shrink-0 mt-0.5\" />\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm truncate\" title={pr.title}>{pr.title}</span>\n <span className={`px-1.5 py-0.5 text-[10px] font-medium rounded ${\n pr.state === 'open' ? 'text-green-600 bg-green-50 dark:text-green-400 dark:bg-green-900/30' :\n pr.state === 'merged' ? 'text-purple-600 bg-purple-50 dark:text-purple-400 dark:bg-purple-900/30' :\n 'text-red-600 bg-red-50 dark:text-red-400 dark:bg-red-900/30'\n }`}>\n {pr.state}\n </span>\n </div>\n <span className=\"text-xs text-gray-500\">\n #{pr.number} · {pr.author} · {formatRelativeTime(pr.createdAt)}\n </span>\n </div>\n </div>\n ))}\n </div>\n {(model.prPage > 0 || model.hasMorePrs) && (\n <div className=\"flex items-center justify-between px-4 py-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 flex-shrink-0\">\n <button\n onClick={() => model.prevPrPage()}\n disabled={model.prPage === 0}\n className=\"inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:pointer-events-none\"\n >\n <ChevronLeft className=\"h-3.5 w-3.5\" /> Previous\n </button>\n <span className=\"text-xs text-gray-500\">Page {model.prPage + 1}</span>\n <button\n onClick={() => model.nextPrPage()}\n disabled={!model.hasMorePrs}\n className=\"inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:pointer-events-none\"\n >\n Next <ChevronRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n )}\n </>\n )\n )}\n </div>\n )}\n\n {/* Issues tab */}\n {model.activeTab === 'issues' && model.hasGitHost && (\n <div className=\"flex-1 min-h-0 flex flex-col\">\n {model.hostLoading && <LoadingSpinner />}\n {!model.hostLoading && model.hostError && (\n <BrowserError error={model.hostError} context=\"Issues\" onRetry={() => model.retryIssues()} />\n )}\n {!model.hostLoading && !model.hostError && (\n model.issues.length === 0 ? (\n <div className=\"flex items-center justify-center py-8 text-xs text-gray-400\">No issues found</div>\n ) : (\n <>\n <div className=\"flex-1 overflow-auto divide-y divide-gray-100 dark:divide-gray-800\">\n {model.issues.map((issue) => (\n <div key={issue.number} className=\"flex items-start gap-3 px-4 py-2.5 hover:bg-gray-50 dark:hover:bg-gray-800/50\">\n <AlertCircle className={`h-4 w-4 shrink-0 mt-0.5 ${issue.state === 'open' ? 'text-green-500' : 'text-red-400'}`} />\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm truncate\" title={issue.title}>{issue.title}</span>\n {issue.labels.map((l) => (\n <span key={l} className=\"px-1.5 py-0.5 text-[10px] font-medium rounded bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400\">\n {l}\n </span>\n ))}\n </div>\n <span className=\"text-xs text-gray-500\">\n #{issue.number} · {issue.author} · {formatRelativeTime(issue.createdAt)}\n </span>\n </div>\n </div>\n ))}\n </div>\n {(model.issuePage > 0 || model.hasMoreIssues) && (\n <div className=\"flex items-center justify-between px-4 py-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 flex-shrink-0\">\n <button\n onClick={() => model.prevIssuePage()}\n disabled={model.issuePage === 0}\n className=\"inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:pointer-events-none\"\n >\n <ChevronLeft className=\"h-3.5 w-3.5\" /> Previous\n </button>\n <span className=\"text-xs text-gray-500\">Page {model.issuePage + 1}</span>\n <button\n onClick={() => model.nextIssuePage()}\n disabled={!model.hasMoreIssues}\n className=\"inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:pointer-events-none\"\n >\n Next <ChevronRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n )}\n </>\n )\n )}\n </div>\n )}\n </div>\n </div>\n );\n});\n\n// ---- Git Host Browser (PRs / Issues) ----\n\ninterface GitHostBrowserProps {\n gitHost: IGitHost;\n}\n\nfunction EmptyState({ message }: { message: string }) {\n return (\n <div className=\"flex items-center justify-center py-8 text-xs text-gray-400\">\n {message}\n </div>\n );\n}\n\nfunction statusColor(status: string) {\n switch (status) {\n case 'open': return 'text-green-600 bg-green-50 dark:text-green-400 dark:bg-green-900/30';\n case 'merged': return 'text-purple-600 bg-purple-50 dark:text-purple-400 dark:bg-purple-900/30';\n case 'closed': return 'text-red-600 bg-red-50 dark:text-red-400 dark:bg-red-900/30';\n default: return 'text-gray-600 bg-gray-50';\n }\n}\n\nexport const GitHostBrowser: React.FC<GitHostBrowserProps> = observer(({ gitHost }) => {\n const [model] = useState(() => new GitHostBrowserModel(gitHost));\n\n useEffect(() => {\n model.loadData();\n }, [model]);\n\n const tabDefs: Array<{ id: 'prs' | 'issues'; label: string; icon: React.ReactNode }> = [\n { id: 'prs', label: 'Pull Requests', icon: <GitPullRequest className=\"h-3.5 w-3.5\" /> },\n { id: 'issues', label: 'Issues', icon: <AlertCircle className=\"h-3.5 w-3.5\" /> },\n ];\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex items-center gap-2 px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800\">\n <GitPullRequest className=\"h-4 w-4 text-gray-500\" />\n <span className=\"text-xs font-medium text-gray-700 dark:text-gray-300\">\n Git Host Browser\n </span>\n </div>\n\n <div className=\"flex border-b border-gray-200 dark:border-gray-700\">\n {tabDefs.map((tab) => (\n <button\n key={tab.id}\n onClick={() => model.setActiveTab(tab.id)}\n className={`flex items-center gap-1.5 px-4 py-2 text-xs font-medium border-b-2 transition-colors ${\n model.activeTab === tab.id\n ? 'border-blue-500 text-blue-600 dark:text-blue-400'\n : 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'\n }`}\n >\n {tab.icon}\n {tab.label}\n </button>\n ))}\n </div>\n\n <div className=\"flex-1 overflow-auto\">\n {model.loading && <LoadingSpinner />}\n {!model.loading && model.error && (\n <BrowserError\n error={model.error}\n context=\"Git Host\"\n onRetry={() => model.retry()}\n />\n )}\n\n {!model.loading && !model.error && model.activeTab === 'prs' && (\n model.prs.length === 0 ? <EmptyState message=\"No pull requests found\" /> : (\n <div className=\"divide-y divide-gray-100 dark:divide-gray-800\">\n {model.prs.map((pr) => (\n <div key={pr.number} className=\"flex items-start gap-3 px-4 py-2.5 hover:bg-gray-50 dark:hover:bg-gray-800/50\">\n <GitPullRequest className=\"h-4 w-4 text-gray-400 shrink-0 mt-0.5\" />\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm truncate\" title={pr.title}>{pr.title}</span>\n <span className={`px-1.5 py-0.5 text-[10px] font-medium rounded ${statusColor(pr.state)}`}>\n {pr.state}\n </span>\n </div>\n <span className=\"text-xs text-gray-500\">\n #{pr.number} · {pr.author} · {formatRelativeTime(pr.createdAt)}\n </span>\n </div>\n </div>\n ))}\n </div>\n )\n )}\n\n {!model.loading && !model.error && model.activeTab === 'issues' && (\n model.issues.length === 0 ? <EmptyState message=\"No issues found\" /> : (\n <div className=\"divide-y divide-gray-100 dark:divide-gray-800\">\n {model.issues.map((issue) => (\n <div key={issue.number} className=\"flex items-start gap-3 px-4 py-2.5 hover:bg-gray-50 dark:hover:bg-gray-800/50\">\n <AlertCircle className=\"h-4 w-4 text-green-500 shrink-0 mt-0.5\" />\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm truncate\" title={issue.title}>{issue.title}</span>\n {issue.labels.map((l) => (\n <span key={l} className=\"px-1.5 py-0.5 text-[10px] font-medium rounded bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400\">\n {l}\n </span>\n ))}\n </div>\n <span className=\"text-xs text-gray-500\">\n #{issue.number} · {issue.author} · {formatRelativeTime(issue.createdAt)}\n </span>\n </div>\n </div>\n ))}\n </div>\n )\n )}\n </div>\n\n {model.showPagination && !model.loading && !model.error && model.currentItems.length > 0 && (\n <div className=\"flex items-center justify-between px-4 py-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800\">\n <button\n onClick={() => model.prevPage()}\n disabled={model.page === 0}\n className=\"inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:pointer-events-none\"\n >\n <ChevronLeft className=\"h-3.5 w-3.5\" />\n Previous\n </button>\n <span className=\"text-xs text-gray-500\">Page {model.page + 1}</span>\n <button\n onClick={() => model.nextPage()}\n disabled={!model.hasMore}\n className=\"inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:pointer-events-none\"\n >\n Next\n <ChevronRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n )}\n </div>\n );\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { flow, makeAutoObservable } from "mobx";
|
|
2
|
+
import React, { useEffect, useState } from "react";
|
|
3
|
+
import { observer } from "mobx-react-lite";
|
|
4
|
+
import { ArrowLeft, ChevronLeft, ChevronRight, Database, Download, File, Folder, RefreshCw } from "lucide-react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { BrowserError } from "@anymux/ui/components/browser-error";
|
|
7
|
+
|
|
8
|
+
//#region src/models/ObjectStorageBrowserModel.ts
|
|
9
|
+
const OBJ_PAGE_SIZE = 100;
|
|
10
|
+
var ObjectStorageBrowserModel = class {
|
|
11
|
+
constructor(storage, bucket) {
|
|
12
|
+
this.prefix = "";
|
|
13
|
+
this.objects = [];
|
|
14
|
+
this.loading = false;
|
|
15
|
+
this.error = null;
|
|
16
|
+
this.continuationToken = void 0;
|
|
17
|
+
this.tokenHistory = [];
|
|
18
|
+
this.page = 0;
|
|
19
|
+
this.hasMore = false;
|
|
20
|
+
this.loadObjects = flow(function* loadObjects(token) {
|
|
21
|
+
this.loading = true;
|
|
22
|
+
this.error = null;
|
|
23
|
+
try {
|
|
24
|
+
const result = yield this.storage.listObjects(this.bucket, {
|
|
25
|
+
prefix: this.prefix || void 0,
|
|
26
|
+
delimiter: "/",
|
|
27
|
+
maxKeys: OBJ_PAGE_SIZE,
|
|
28
|
+
continuationToken: token
|
|
29
|
+
});
|
|
30
|
+
const prefixItems = result.commonPrefixes.map((p) => ({
|
|
31
|
+
key: p,
|
|
32
|
+
isPrefix: true
|
|
33
|
+
}));
|
|
34
|
+
const objectItems = result.objects.map((obj) => ({
|
|
35
|
+
key: obj.key,
|
|
36
|
+
size: obj.metadata.size,
|
|
37
|
+
lastModified: obj.metadata.lastModified,
|
|
38
|
+
isPrefix: false
|
|
39
|
+
}));
|
|
40
|
+
this.objects = [...prefixItems, ...objectItems];
|
|
41
|
+
this.hasMore = result.isTruncated;
|
|
42
|
+
this.continuationToken = result.nextContinuationToken;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
this.error = err instanceof Error ? err.message : "Failed to load objects";
|
|
45
|
+
} finally {
|
|
46
|
+
this.loading = false;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
this.initialize = flow(function* initialize() {
|
|
50
|
+
this.page = 0;
|
|
51
|
+
this.tokenHistory = [];
|
|
52
|
+
this.continuationToken = void 0;
|
|
53
|
+
yield this.loadObjects();
|
|
54
|
+
});
|
|
55
|
+
this.download = flow(function* download(key) {
|
|
56
|
+
try {
|
|
57
|
+
const url = yield this.storage.getPresignedUrl(this.bucket, key);
|
|
58
|
+
window.open(url, "_blank");
|
|
59
|
+
} catch (err) {
|
|
60
|
+
this.error = err instanceof Error ? err.message : "Failed to generate download URL";
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
this.storage = storage;
|
|
64
|
+
this.bucket = bucket;
|
|
65
|
+
makeAutoObservable(this, { storage: false });
|
|
66
|
+
}
|
|
67
|
+
get breadcrumbs() {
|
|
68
|
+
return this.prefix.split("/").filter(Boolean).reduce((acc, part, i) => {
|
|
69
|
+
const prev = acc[i - 1]?.prefix || "";
|
|
70
|
+
acc.push({
|
|
71
|
+
label: part,
|
|
72
|
+
prefix: prev + part + "/"
|
|
73
|
+
});
|
|
74
|
+
return acc;
|
|
75
|
+
}, []);
|
|
76
|
+
}
|
|
77
|
+
displayName(key) {
|
|
78
|
+
return key.slice(this.prefix.length).replace(/\/$/, "");
|
|
79
|
+
}
|
|
80
|
+
navigateToPrefix(newPrefix) {
|
|
81
|
+
this.prefix = newPrefix;
|
|
82
|
+
this.page = 0;
|
|
83
|
+
this.tokenHistory = [];
|
|
84
|
+
this.continuationToken = void 0;
|
|
85
|
+
this.loadObjects();
|
|
86
|
+
}
|
|
87
|
+
navigateUp() {
|
|
88
|
+
const parts = this.prefix.replace(/\/$/, "").split("/");
|
|
89
|
+
parts.pop();
|
|
90
|
+
this.navigateToPrefix(parts.length > 0 ? parts.join("/") + "/" : "");
|
|
91
|
+
}
|
|
92
|
+
nextPage() {
|
|
93
|
+
if (!this.hasMore || !this.continuationToken) return;
|
|
94
|
+
this.tokenHistory = [...this.tokenHistory, this.continuationToken];
|
|
95
|
+
this.page += 1;
|
|
96
|
+
this.loadObjects(this.continuationToken);
|
|
97
|
+
}
|
|
98
|
+
prevPage() {
|
|
99
|
+
if (this.page <= 0) return;
|
|
100
|
+
const newHistory = [...this.tokenHistory];
|
|
101
|
+
newHistory.pop();
|
|
102
|
+
const prevToken = newHistory.length > 0 ? newHistory[newHistory.length - 1] : void 0;
|
|
103
|
+
this.tokenHistory = newHistory;
|
|
104
|
+
this.page -= 1;
|
|
105
|
+
this.loadObjects(prevToken);
|
|
106
|
+
}
|
|
107
|
+
refresh() {
|
|
108
|
+
this.loadObjects();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/components/ObjectStorageBrowser.tsx
|
|
114
|
+
const formatSize = (bytes) => {
|
|
115
|
+
if (bytes === void 0) return "";
|
|
116
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
117
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
118
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
119
|
+
};
|
|
120
|
+
const ObjectStorageBrowser = observer(({ storage, bucket }) => {
|
|
121
|
+
const [model] = useState(() => new ObjectStorageBrowserModel(storage, bucket));
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
model.initialize();
|
|
124
|
+
}, [model]);
|
|
125
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
126
|
+
className: "flex flex-col h-full",
|
|
127
|
+
children: [
|
|
128
|
+
/* @__PURE__ */ jsxs("div", {
|
|
129
|
+
className: "flex items-center gap-2 px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800",
|
|
130
|
+
children: [
|
|
131
|
+
/* @__PURE__ */ jsx(Database, { className: "h-4 w-4 text-gray-500" }),
|
|
132
|
+
/* @__PURE__ */ jsx("span", {
|
|
133
|
+
className: "text-xs font-medium text-gray-700 dark:text-gray-300",
|
|
134
|
+
children: model.bucket
|
|
135
|
+
}),
|
|
136
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
137
|
+
model.prefix && /* @__PURE__ */ jsx("button", {
|
|
138
|
+
onClick: () => model.navigateUp(),
|
|
139
|
+
className: "p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors",
|
|
140
|
+
title: "Go up",
|
|
141
|
+
children: /* @__PURE__ */ jsx(ArrowLeft, { className: "h-3.5 w-3.5" })
|
|
142
|
+
}),
|
|
143
|
+
/* @__PURE__ */ jsx("button", {
|
|
144
|
+
onClick: () => model.refresh(),
|
|
145
|
+
disabled: model.loading,
|
|
146
|
+
className: "p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors",
|
|
147
|
+
title: "Refresh",
|
|
148
|
+
children: /* @__PURE__ */ jsx(RefreshCw, { className: `h-3.5 w-3.5 ${model.loading ? "animate-spin" : ""}` })
|
|
149
|
+
})
|
|
150
|
+
]
|
|
151
|
+
}),
|
|
152
|
+
/* @__PURE__ */ jsxs("div", {
|
|
153
|
+
className: "flex items-center gap-1 px-4 py-1.5 text-xs text-gray-500 dark:text-gray-400 border-b border-gray-100 dark:border-gray-800",
|
|
154
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
155
|
+
onClick: () => model.navigateToPrefix(""),
|
|
156
|
+
className: "hover:text-gray-900 dark:hover:text-white",
|
|
157
|
+
children: "/"
|
|
158
|
+
}), model.breadcrumbs.map((crumb) => /* @__PURE__ */ jsxs(React.Fragment, { children: [/* @__PURE__ */ jsx("span", { children: "/" }), /* @__PURE__ */ jsx("button", {
|
|
159
|
+
onClick: () => model.navigateToPrefix(crumb.prefix),
|
|
160
|
+
className: "hover:text-gray-900 dark:hover:text-white",
|
|
161
|
+
children: crumb.label
|
|
162
|
+
})] }, crumb.prefix))]
|
|
163
|
+
}),
|
|
164
|
+
/* @__PURE__ */ jsxs("div", {
|
|
165
|
+
className: "flex-1 overflow-auto",
|
|
166
|
+
children: [
|
|
167
|
+
model.error && model.objects.length === 0 ? /* @__PURE__ */ jsx(BrowserError, {
|
|
168
|
+
error: model.error,
|
|
169
|
+
context: `Object Storage · ${model.bucket}`,
|
|
170
|
+
onRetry: () => model.refresh(),
|
|
171
|
+
onGoBack: model.prefix ? () => model.navigateUp() : void 0
|
|
172
|
+
}) : model.error ? /* @__PURE__ */ jsx("div", {
|
|
173
|
+
className: "px-4 py-2 bg-red-50 dark:bg-red-900/20 text-xs text-red-600 dark:text-red-400 border-b border-red-200 dark:border-red-800",
|
|
174
|
+
children: model.error
|
|
175
|
+
}) : null,
|
|
176
|
+
model.loading && model.objects.length === 0 && !model.error && /* @__PURE__ */ jsx("div", {
|
|
177
|
+
className: "flex items-center justify-center h-32 text-sm text-gray-400",
|
|
178
|
+
children: "Loading..."
|
|
179
|
+
}),
|
|
180
|
+
!model.error && model.objects.length === 0 && !model.loading && /* @__PURE__ */ jsx("div", {
|
|
181
|
+
className: "flex items-center justify-center h-32 text-sm text-gray-400",
|
|
182
|
+
children: "No objects found"
|
|
183
|
+
}),
|
|
184
|
+
model.objects.length > 0 && /* @__PURE__ */ jsxs("table", {
|
|
185
|
+
className: "w-full text-sm",
|
|
186
|
+
children: [/* @__PURE__ */ jsx("thead", {
|
|
187
|
+
className: "text-xs text-gray-500 dark:text-gray-400 border-b border-gray-100 dark:border-gray-800 sticky top-0 bg-white dark:bg-gray-900",
|
|
188
|
+
children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
189
|
+
/* @__PURE__ */ jsx("th", {
|
|
190
|
+
className: "text-left px-4 py-2 font-medium",
|
|
191
|
+
children: "Name"
|
|
192
|
+
}),
|
|
193
|
+
/* @__PURE__ */ jsx("th", {
|
|
194
|
+
className: "text-right px-4 py-2 font-medium w-24",
|
|
195
|
+
children: "Size"
|
|
196
|
+
}),
|
|
197
|
+
/* @__PURE__ */ jsx("th", {
|
|
198
|
+
className: "text-right px-4 py-2 font-medium w-40",
|
|
199
|
+
children: "Modified"
|
|
200
|
+
}),
|
|
201
|
+
/* @__PURE__ */ jsx("th", { className: "w-10" })
|
|
202
|
+
] })
|
|
203
|
+
}), /* @__PURE__ */ jsx("tbody", { children: model.objects.map((obj) => /* @__PURE__ */ jsxs("tr", {
|
|
204
|
+
className: "border-b border-gray-50 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/50 cursor-pointer",
|
|
205
|
+
onClick: () => obj.isPrefix && model.navigateToPrefix(obj.key),
|
|
206
|
+
children: [
|
|
207
|
+
/* @__PURE__ */ jsxs("td", {
|
|
208
|
+
className: "px-4 py-2 flex items-center gap-2",
|
|
209
|
+
children: [obj.isPrefix ? /* @__PURE__ */ jsx(Folder, { className: "h-4 w-4 text-blue-500 shrink-0" }) : /* @__PURE__ */ jsx(File, { className: "h-4 w-4 text-gray-400 shrink-0" }), /* @__PURE__ */ jsx("span", {
|
|
210
|
+
className: "truncate",
|
|
211
|
+
title: obj.key,
|
|
212
|
+
children: model.displayName(obj.key)
|
|
213
|
+
})]
|
|
214
|
+
}),
|
|
215
|
+
/* @__PURE__ */ jsx("td", {
|
|
216
|
+
className: "px-4 py-2 text-right text-gray-500",
|
|
217
|
+
children: !obj.isPrefix && formatSize(obj.size)
|
|
218
|
+
}),
|
|
219
|
+
/* @__PURE__ */ jsx("td", {
|
|
220
|
+
className: "px-4 py-2 text-right text-gray-500",
|
|
221
|
+
children: obj.lastModified && obj.lastModified.toLocaleDateString()
|
|
222
|
+
}),
|
|
223
|
+
/* @__PURE__ */ jsx("td", {
|
|
224
|
+
className: "px-2 py-2",
|
|
225
|
+
children: !obj.isPrefix && /* @__PURE__ */ jsx("button", {
|
|
226
|
+
onClick: (e) => {
|
|
227
|
+
e.stopPropagation();
|
|
228
|
+
model.download(obj.key);
|
|
229
|
+
},
|
|
230
|
+
className: "p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700",
|
|
231
|
+
title: "Download",
|
|
232
|
+
children: /* @__PURE__ */ jsx(Download, { className: "h-3.5 w-3.5" })
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
]
|
|
236
|
+
}, obj.key)) })]
|
|
237
|
+
})
|
|
238
|
+
]
|
|
239
|
+
}),
|
|
240
|
+
(model.page > 0 || model.hasMore) && !model.loading && model.objects.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
241
|
+
className: "flex items-center justify-between px-4 py-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800",
|
|
242
|
+
children: [
|
|
243
|
+
/* @__PURE__ */ jsxs("button", {
|
|
244
|
+
onClick: () => model.prevPage(),
|
|
245
|
+
disabled: model.page === 0,
|
|
246
|
+
className: "inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:pointer-events-none",
|
|
247
|
+
children: [/* @__PURE__ */ jsx(ChevronLeft, { className: "h-3.5 w-3.5" }), "Previous"]
|
|
248
|
+
}),
|
|
249
|
+
/* @__PURE__ */ jsxs("span", {
|
|
250
|
+
className: "text-xs text-gray-500",
|
|
251
|
+
children: ["Page ", model.page + 1]
|
|
252
|
+
}),
|
|
253
|
+
/* @__PURE__ */ jsxs("button", {
|
|
254
|
+
onClick: () => model.nextPage(),
|
|
255
|
+
disabled: !model.hasMore,
|
|
256
|
+
className: "inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:pointer-events-none",
|
|
257
|
+
children: ["Next", /* @__PURE__ */ jsx(ChevronRight, { className: "h-3.5 w-3.5" })]
|
|
258
|
+
})
|
|
259
|
+
]
|
|
260
|
+
})
|
|
261
|
+
]
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
//#endregion
|
|
266
|
+
export { ObjectStorageBrowser, ObjectStorageBrowserModel };
|
|
267
|
+
//# sourceMappingURL=ObjectStorageBrowser-B_25Emfu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ObjectStorageBrowser-B_25Emfu.js","names":["storage: IObjectStorage","bucket: string","token?: string","prefixItems: DisplayObject[]","p: string","objectItems: DisplayObject[]","obj: any","key: string","url: string","newPrefix: string","bytes?: number","ObjectStorageBrowser: React.FC<ObjectStorageBrowserProps>"],"sources":["../src/models/ObjectStorageBrowserModel.ts","../src/components/ObjectStorageBrowser.tsx"],"sourcesContent":["import { makeAutoObservable, flow } from 'mobx';\nimport type { IObjectStorage } from '@anymux/file-system';\n\nconst OBJ_PAGE_SIZE = 100;\n\nexport interface DisplayObject {\n key: string;\n size?: number;\n lastModified?: Date;\n isPrefix: boolean;\n}\n\nexport class ObjectStorageBrowserModel {\n storage: IObjectStorage;\n bucket: string;\n\n prefix = '';\n objects: DisplayObject[] = [];\n loading = false;\n error: string | null = null;\n continuationToken: string | undefined = undefined;\n tokenHistory: (string | undefined)[] = [];\n page = 0;\n hasMore = false;\n\n constructor(storage: IObjectStorage, bucket: string) {\n this.storage = storage;\n this.bucket = bucket;\n makeAutoObservable(this, { storage: false });\n }\n\n get breadcrumbs(): Array<{ label: string; prefix: string }> {\n return this.prefix\n .split('/')\n .filter(Boolean)\n .reduce<Array<{ label: string; prefix: string }>>((acc, part, i) => {\n const prev = acc[i - 1]?.prefix || '';\n acc.push({ label: part, prefix: prev + part + '/' });\n return acc;\n }, []);\n }\n\n displayName(key: string): string {\n return key.slice(this.prefix.length).replace(/\\/$/, '');\n }\n\n loadObjects = flow(function* loadObjects(this: ObjectStorageBrowserModel, token?: string) {\n this.loading = true;\n this.error = null;\n\n try {\n const result = yield this.storage.listObjects(this.bucket, {\n prefix: this.prefix || undefined,\n delimiter: '/',\n maxKeys: OBJ_PAGE_SIZE,\n continuationToken: token,\n });\n\n const prefixItems: DisplayObject[] = result.commonPrefixes.map((p: string) => ({\n key: p,\n isPrefix: true,\n }));\n\n const objectItems: DisplayObject[] = result.objects.map((obj: any) => ({\n key: obj.key,\n size: obj.metadata.size,\n lastModified: obj.metadata.lastModified,\n isPrefix: false,\n }));\n\n this.objects = [...prefixItems, ...objectItems];\n this.hasMore = result.isTruncated;\n this.continuationToken = result.nextContinuationToken;\n } catch (err) {\n this.error = err instanceof Error ? err.message : 'Failed to load objects';\n } finally {\n this.loading = false;\n }\n });\n\n initialize = flow(function* initialize(this: ObjectStorageBrowserModel) {\n this.page = 0;\n this.tokenHistory = [];\n this.continuationToken = undefined;\n yield this.loadObjects();\n });\n\n navigateToPrefix(newPrefix: string) {\n this.prefix = newPrefix;\n this.page = 0;\n this.tokenHistory = [];\n this.continuationToken = undefined;\n this.loadObjects();\n }\n\n navigateUp() {\n const parts = this.prefix.replace(/\\/$/, '').split('/');\n parts.pop();\n this.navigateToPrefix(parts.length > 0 ? parts.join('/') + '/' : '');\n }\n\n nextPage() {\n if (!this.hasMore || !this.continuationToken) return;\n this.tokenHistory = [...this.tokenHistory, this.continuationToken];\n this.page += 1;\n this.loadObjects(this.continuationToken);\n }\n\n prevPage() {\n if (this.page <= 0) return;\n const newHistory = [...this.tokenHistory];\n newHistory.pop();\n const prevToken = newHistory.length > 0 ? newHistory[newHistory.length - 1] : undefined;\n this.tokenHistory = newHistory;\n this.page -= 1;\n this.loadObjects(prevToken);\n }\n\n download = flow(function* download(this: ObjectStorageBrowserModel, key: string) {\n try {\n const url: string = yield this.storage.getPresignedUrl(this.bucket, key);\n window.open(url, '_blank');\n } catch (err) {\n this.error = err instanceof Error ? err.message : 'Failed to generate download URL';\n }\n });\n\n refresh() {\n this.loadObjects();\n }\n}\n","import React, { useState, useEffect } from 'react';\nimport { observer } from 'mobx-react-lite';\nimport { Database, ArrowLeft, Download, RefreshCw, Folder, File, ChevronLeft, ChevronRight } from 'lucide-react';\nimport { BrowserError } from '@anymux/ui/components/browser-error';\nimport type { IObjectStorage } from '@anymux/file-system';\nimport { ObjectStorageBrowserModel } from '../models/ObjectStorageBrowserModel';\n\ninterface ObjectStorageBrowserProps {\n storage: IObjectStorage;\n bucket: string;\n}\n\nconst formatSize = (bytes?: number) => {\n if (bytes === undefined) return '';\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n};\n\nexport const ObjectStorageBrowser: React.FC<ObjectStorageBrowserProps> = observer(({ storage, bucket }) => {\n const [model] = useState(() => new ObjectStorageBrowserModel(storage, bucket));\n\n useEffect(() => {\n model.initialize();\n }, [model]);\n\n return (\n <div className=\"flex flex-col h-full\">\n {/* Toolbar */}\n <div className=\"flex items-center gap-2 px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800\">\n <Database className=\"h-4 w-4 text-gray-500\" />\n <span className=\"text-xs font-medium text-gray-700 dark:text-gray-300\">{model.bucket}</span>\n <div className=\"flex-1\" />\n {model.prefix && (\n <button\n onClick={() => model.navigateUp()}\n className=\"p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors\"\n title=\"Go up\"\n >\n <ArrowLeft className=\"h-3.5 w-3.5\" />\n </button>\n )}\n <button\n onClick={() => model.refresh()}\n disabled={model.loading}\n className=\"p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors\"\n title=\"Refresh\"\n >\n <RefreshCw className={`h-3.5 w-3.5 ${model.loading ? 'animate-spin' : ''}`} />\n </button>\n </div>\n\n {/* Breadcrumbs */}\n <div className=\"flex items-center gap-1 px-4 py-1.5 text-xs text-gray-500 dark:text-gray-400 border-b border-gray-100 dark:border-gray-800\">\n <button\n onClick={() => model.navigateToPrefix('')}\n className=\"hover:text-gray-900 dark:hover:text-white\"\n >\n /\n </button>\n {model.breadcrumbs.map((crumb) => (\n <React.Fragment key={crumb.prefix}>\n <span>/</span>\n <button\n onClick={() => model.navigateToPrefix(crumb.prefix)}\n className=\"hover:text-gray-900 dark:hover:text-white\"\n >\n {crumb.label}\n </button>\n </React.Fragment>\n ))}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 overflow-auto\">\n {model.error && model.objects.length === 0 ? (\n <BrowserError\n error={model.error}\n context={`Object Storage · ${model.bucket}`}\n onRetry={() => model.refresh()}\n onGoBack={model.prefix ? () => model.navigateUp() : undefined}\n />\n ) : model.error ? (\n <div className=\"px-4 py-2 bg-red-50 dark:bg-red-900/20 text-xs text-red-600 dark:text-red-400 border-b border-red-200 dark:border-red-800\">\n {model.error}\n </div>\n ) : null}\n\n {model.loading && model.objects.length === 0 && !model.error && (\n <div className=\"flex items-center justify-center h-32 text-sm text-gray-400\">\n Loading...\n </div>\n )}\n\n {!model.error && model.objects.length === 0 && !model.loading && (\n <div className=\"flex items-center justify-center h-32 text-sm text-gray-400\">\n No objects found\n </div>\n )}\n\n {model.objects.length > 0 && (\n <table className=\"w-full text-sm\">\n <thead className=\"text-xs text-gray-500 dark:text-gray-400 border-b border-gray-100 dark:border-gray-800 sticky top-0 bg-white dark:bg-gray-900\">\n <tr>\n <th className=\"text-left px-4 py-2 font-medium\">Name</th>\n <th className=\"text-right px-4 py-2 font-medium w-24\">Size</th>\n <th className=\"text-right px-4 py-2 font-medium w-40\">Modified</th>\n <th className=\"w-10\" />\n </tr>\n </thead>\n <tbody>\n {model.objects.map((obj) => (\n <tr\n key={obj.key}\n className=\"border-b border-gray-50 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/50 cursor-pointer\"\n onClick={() => obj.isPrefix && model.navigateToPrefix(obj.key)}\n >\n <td className=\"px-4 py-2 flex items-center gap-2\">\n {obj.isPrefix ? (\n <Folder className=\"h-4 w-4 text-blue-500 shrink-0\" />\n ) : (\n <File className=\"h-4 w-4 text-gray-400 shrink-0\" />\n )}\n <span className=\"truncate\" title={obj.key}>{model.displayName(obj.key)}</span>\n </td>\n <td className=\"px-4 py-2 text-right text-gray-500\">\n {!obj.isPrefix && formatSize(obj.size)}\n </td>\n <td className=\"px-4 py-2 text-right text-gray-500\">\n {obj.lastModified && obj.lastModified.toLocaleDateString()}\n </td>\n <td className=\"px-2 py-2\">\n {!obj.isPrefix && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n model.download(obj.key);\n }}\n className=\"p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700\"\n title=\"Download\"\n >\n <Download className=\"h-3.5 w-3.5\" />\n </button>\n )}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n )}\n </div>\n\n {(model.page > 0 || model.hasMore) && !model.loading && model.objects.length > 0 && (\n <div className=\"flex items-center justify-between px-4 py-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800\">\n <button\n onClick={() => model.prevPage()}\n disabled={model.page === 0}\n className=\"inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:pointer-events-none\"\n >\n <ChevronLeft className=\"h-3.5 w-3.5\" />\n Previous\n </button>\n <span className=\"text-xs text-gray-500\">Page {model.page + 1}</span>\n <button\n onClick={() => model.nextPage()}\n disabled={!model.hasMore}\n className=\"inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:pointer-events-none\"\n >\n Next\n <ChevronRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n )}\n </div>\n );\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { flow, makeAutoObservable } from "mobx";
|
|
2
|
+
import { match } from "ts-pattern";
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { observer } from "mobx-react-lite";
|
|
5
|
+
import { AlertCircle, Loader2, Lock, Search } from "lucide-react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
|
|
8
|
+
//#region src/models/RepoPickerModel.ts
|
|
9
|
+
var RepoPickerModel = class {
|
|
10
|
+
constructor(serviceId, accessToken) {
|
|
11
|
+
this.repos = [];
|
|
12
|
+
this.loading = true;
|
|
13
|
+
this.error = null;
|
|
14
|
+
this.search = "";
|
|
15
|
+
this.loadRepos = flow(function* loadRepos() {
|
|
16
|
+
this.loading = true;
|
|
17
|
+
this.error = null;
|
|
18
|
+
try {
|
|
19
|
+
const data = yield fetchRepos(this.serviceId, this.accessToken);
|
|
20
|
+
this.repos = data;
|
|
21
|
+
} catch (err) {
|
|
22
|
+
this.error = err instanceof Error ? err.message : "Failed to fetch repos";
|
|
23
|
+
} finally {
|
|
24
|
+
this.loading = false;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
this.serviceId = serviceId;
|
|
28
|
+
this.accessToken = accessToken;
|
|
29
|
+
makeAutoObservable(this, {});
|
|
30
|
+
}
|
|
31
|
+
get filtered() {
|
|
32
|
+
const q = this.search.toLowerCase();
|
|
33
|
+
if (!q) return this.repos;
|
|
34
|
+
return this.repos.filter((r) => r.fullName.toLowerCase().includes(q));
|
|
35
|
+
}
|
|
36
|
+
setSearch(value) {
|
|
37
|
+
this.search = value;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
async function fetchRepos(serviceId, token) {
|
|
41
|
+
return match(serviceId).with("github", () => fetchGitHubRepos(token)).with("gitlab", () => fetchGitLabRepos(token)).with("bitbucket", () => fetchBitbucketRepos(token)).with("gitea", () => fetchGiteaRepos(token)).otherwise(() => {
|
|
42
|
+
throw new Error(`Repo picker not supported for: ${serviceId}`);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async function fetchGitHubRepos(token) {
|
|
46
|
+
const res = await fetch("https://api.github.com/user/repos?sort=updated&per_page=50", { headers: { Authorization: `Bearer ${token}` } });
|
|
47
|
+
if (!res.ok) throw new Error(`GitHub API error: ${res.status}`);
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
return data.map((r) => ({
|
|
50
|
+
fullName: r.full_name,
|
|
51
|
+
description: r.description,
|
|
52
|
+
language: r.language,
|
|
53
|
+
isPrivate: r.private
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
async function fetchGitLabRepos(token) {
|
|
57
|
+
const res = await fetch("https://gitlab.com/api/v4/projects?membership=true&order_by=updated_at&per_page=50", { headers: { "PRIVATE-TOKEN": token } });
|
|
58
|
+
if (!res.ok) throw new Error(`GitLab API error: ${res.status}`);
|
|
59
|
+
const data = await res.json();
|
|
60
|
+
return data.map((r) => ({
|
|
61
|
+
fullName: r.path_with_namespace,
|
|
62
|
+
description: r.description,
|
|
63
|
+
language: null,
|
|
64
|
+
isPrivate: r.visibility === "private"
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
async function fetchBitbucketRepos(token) {
|
|
68
|
+
const res = await fetch("https://api.bitbucket.org/2.0/repositories?role=member&pagelen=50", { headers: { Authorization: `Bearer ${token}` } });
|
|
69
|
+
if (!res.ok) throw new Error(`Bitbucket API error: ${res.status}`);
|
|
70
|
+
const data = await res.json();
|
|
71
|
+
return data.values.map((r) => ({
|
|
72
|
+
fullName: r.full_name,
|
|
73
|
+
description: r.description || null,
|
|
74
|
+
language: r.language || null,
|
|
75
|
+
isPrivate: r.is_private
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
async function fetchGiteaRepos(token) {
|
|
79
|
+
const creds = JSON.parse(token);
|
|
80
|
+
const headers = { "Accept": "application/json" };
|
|
81
|
+
if (creds.token) headers["Authorization"] = `token ${creds.token}`;
|
|
82
|
+
else if (creds.username && creds.password) headers["Authorization"] = "Basic " + btoa(`${creds.username}:${creds.password}`);
|
|
83
|
+
const proxyUrl = typeof window !== "undefined" ? window.location.origin : void 0;
|
|
84
|
+
let url;
|
|
85
|
+
if (proxyUrl) {
|
|
86
|
+
url = `${proxyUrl}/api/gitea/api/v1/user/repos?limit=50`;
|
|
87
|
+
headers["x-gitea-url"] = creds.url;
|
|
88
|
+
} else url = `${creds.url}/api/v1/user/repos?limit=50`;
|
|
89
|
+
const res = await fetch(url, { headers });
|
|
90
|
+
if (!res.ok) throw new Error(`Gitea API error: ${res.status}`);
|
|
91
|
+
const data = await res.json();
|
|
92
|
+
return data.map((r) => ({
|
|
93
|
+
fullName: r.full_name,
|
|
94
|
+
description: r.description || null,
|
|
95
|
+
language: r.language || null,
|
|
96
|
+
isPrivate: r.private
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/components/RepoPicker.tsx
|
|
102
|
+
const RepoPicker = observer(({ serviceId, accessToken, onSelectRepo }) => {
|
|
103
|
+
const [model] = useState(() => new RepoPickerModel(serviceId, accessToken));
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
model.loadRepos();
|
|
106
|
+
}, [model]);
|
|
107
|
+
if (model.loading) return /* @__PURE__ */ jsxs("div", {
|
|
108
|
+
className: "flex items-center justify-center py-8 text-gray-400",
|
|
109
|
+
children: [/* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5 animate-spin mr-2" }), "Loading repositories..."]
|
|
110
|
+
});
|
|
111
|
+
if (model.error) return /* @__PURE__ */ jsxs("div", {
|
|
112
|
+
className: "flex items-center gap-2 justify-center py-8 text-sm text-red-600 dark:text-red-400",
|
|
113
|
+
children: [/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4" }), model.error]
|
|
114
|
+
});
|
|
115
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
116
|
+
className: "flex flex-col gap-2",
|
|
117
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
118
|
+
className: "relative",
|
|
119
|
+
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", {
|
|
120
|
+
type: "text",
|
|
121
|
+
placeholder: "Search repositories...",
|
|
122
|
+
value: model.search,
|
|
123
|
+
onChange: (e) => model.setSearch(e.target.value),
|
|
124
|
+
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"
|
|
125
|
+
})]
|
|
126
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
127
|
+
className: "max-h-64 overflow-y-auto rounded-md border border-gray-700",
|
|
128
|
+
children: model.filtered.length === 0 ? /* @__PURE__ */ jsx("div", {
|
|
129
|
+
className: "px-4 py-6 text-center text-sm text-gray-500",
|
|
130
|
+
children: "No repositories found"
|
|
131
|
+
}) : model.filtered.map((repo) => {
|
|
132
|
+
const parts = repo.fullName.split("/");
|
|
133
|
+
const owner = parts[0] ?? "";
|
|
134
|
+
const name = parts[1] ?? "";
|
|
135
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
136
|
+
type: "button",
|
|
137
|
+
onClick: () => onSelectRepo({
|
|
138
|
+
owner,
|
|
139
|
+
repo: name
|
|
140
|
+
}),
|
|
141
|
+
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",
|
|
142
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
143
|
+
className: "min-w-0 flex-1",
|
|
144
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
145
|
+
className: "flex items-center gap-2",
|
|
146
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
147
|
+
className: "text-sm font-medium text-blue-400 truncate",
|
|
148
|
+
title: repo.fullName,
|
|
149
|
+
children: repo.fullName
|
|
150
|
+
}), repo.isPrivate && /* @__PURE__ */ jsx(Lock, { className: "h-3 w-3 flex-shrink-0 text-gray-500" })]
|
|
151
|
+
}), repo.description && /* @__PURE__ */ jsx("p", {
|
|
152
|
+
className: "mt-0.5 text-xs text-gray-500 truncate",
|
|
153
|
+
title: repo.description,
|
|
154
|
+
children: repo.description.length > 80 ? `${repo.description.slice(0, 80)}...` : repo.description
|
|
155
|
+
})]
|
|
156
|
+
}), repo.language && /* @__PURE__ */ jsx("span", {
|
|
157
|
+
className: "flex-shrink-0 rounded-full bg-gray-800 px-2 py-0.5 text-xs text-gray-400",
|
|
158
|
+
children: repo.language
|
|
159
|
+
})]
|
|
160
|
+
}, repo.fullName);
|
|
161
|
+
})
|
|
162
|
+
})]
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
export { RepoPicker, RepoPickerModel };
|
|
168
|
+
//# sourceMappingURL=RepoPicker-CoHMiJ-3.js.map
|