fb13457f6f
CI / Supply-chain scan (push) Has been cancelled
Docker Build, Test, and Publish / Detect affected areas (push) Has been cancelled
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Has been cancelled
Nix flake check / Detect affected areas (push) Has been cancelled
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
CI / Detect affected areas (push) Has been cancelled
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
CI / Desktop E2E (push) Has been cancelled
CI / Docs Site (push) Has been cancelled
CI / Deny unrelated histories (push) Has been cancelled
CI / Check contributors (push) Has been cancelled
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
CI / Check uv.lock (push) Has been cancelled
CI / Check no committed infographics (push) Has been cancelled
CI / Profile artifact check (push) Has been cancelled
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
Docker Build, Test, and Publish / merge (push) Has been cancelled
auto-fix lint issues & formatting / Apply patch (push) Has been cancelled
CI / Check no case-colliding filenames (push) Has been cancelled
CI / package-lock.json diff (push) Has been cancelled
CI / Lint Docker scripts (push) Has been cancelled
CI / Review label gate (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
CI / CI timing report (push) Has been cancelled
CI / Python tests (push) Has been cancelled
CI / OS-specific tests (push) Has been cancelled
CI / Python lints (push) Has been cancelled
CI / JS & TS checks (push) Has been cancelled
CI / Installer tests (push) Has been cancelled
CI / Rust tests (push) Has been cancelled
Nix flake check / nix flake check (push) Has been cancelled
157 lines
5.4 KiB
TypeScript
157 lines
5.4 KiB
TypeScript
import { useStore } from '@nanostores/react'
|
|
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger
|
|
} from '@/components/ui/dropdown-menu'
|
|
import { Tip } from '@/components/ui/tooltip'
|
|
import { useI18n } from '@/i18n'
|
|
import { ChevronDown, Download, Loader2, RefreshCw } from '@/lib/icons'
|
|
import { useStoreSelector } from '@/lib/use-session-slice'
|
|
import { cn } from '@/lib/utils'
|
|
import { resolveVersionStatus } from '@/lib/version-status'
|
|
import { $connection } from '@/store/session'
|
|
import {
|
|
$backendUpdateApply,
|
|
$backendUpdateChecking,
|
|
$backendUpdateStatus,
|
|
$desktopVersion,
|
|
$updateApply,
|
|
$updateChecking,
|
|
$updateStatus,
|
|
openUpdateOverlayFor
|
|
} from '@/store/updates'
|
|
|
|
export interface SidebarVersionMenuProps {
|
|
compact?: boolean
|
|
}
|
|
|
|
/** One entry for the existing desktop and remote-backend update flows. */
|
|
export function SidebarVersionMenu({ compact = false }: SidebarVersionMenuProps) {
|
|
const { t } = useI18n()
|
|
const copy = t.shell.statusbar
|
|
const version = useStore($desktopVersion)?.appVersion
|
|
const remote = useStoreSelector($connection, connection => connection?.mode === 'remote')
|
|
const clientCheck = useStore($updateChecking)
|
|
const clientApply = useStore($updateApply)
|
|
const clientUpdate = useStore($updateStatus)
|
|
const backendCheck = useStore($backendUpdateChecking)
|
|
const backendApply = useStore($backendUpdateApply)
|
|
const backendUpdate = useStore($backendUpdateStatus)
|
|
|
|
const targets = [
|
|
{ target: 'client' as const, version, checking: clientCheck, apply: clientApply, update: clientUpdate },
|
|
...(remote
|
|
? [
|
|
{
|
|
target: 'backend' as const,
|
|
version: backendUpdate?.currentVersion,
|
|
checking: backendCheck,
|
|
apply: backendApply,
|
|
update: backendUpdate
|
|
}
|
|
]
|
|
: [])
|
|
].map(entry => {
|
|
const applying = entry.apply.applying || entry.apply.stage === 'restart'
|
|
|
|
return {
|
|
...entry,
|
|
applying,
|
|
status: resolveVersionStatus({
|
|
applying,
|
|
applyMessage: entry.apply.message,
|
|
behind: entry.update?.behind ?? 0,
|
|
branch: entry.update?.branch,
|
|
copy,
|
|
remote,
|
|
restarting: entry.apply.stage === 'restart',
|
|
sha: entry.update?.currentSha?.slice(0, 7),
|
|
target: entry.target,
|
|
updateAvailable: entry.update?.updateAvailable,
|
|
version: entry.version
|
|
})
|
|
}
|
|
})
|
|
|
|
const busy = targets.some(entry => entry.applying || entry.checking)
|
|
const available = targets.some(entry => entry.status.hasUpdate)
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<Tip label={t.settings.about.updates} side={compact ? 'right' : 'bottom'}>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
aria-label={t.settings.about.updates}
|
|
className={cn('min-w-0 max-w-full [-webkit-app-region:no-drag]', available && 'text-primary')}
|
|
data-slot="sidebar-version-menu"
|
|
size={compact ? 'icon-sm' : 'inline'}
|
|
variant={compact ? 'ghost' : 'text'}
|
|
>
|
|
{compact ? (
|
|
busy ? (
|
|
<Loader2 aria-hidden className="animate-spin" />
|
|
) : (
|
|
<Download aria-hidden />
|
|
)
|
|
) : (
|
|
<>
|
|
<span className="truncate text-[0.6875rem]">
|
|
{version ? `v${version}` : t.settings.about.versionUnavailable}
|
|
</span>
|
|
{busy ? (
|
|
<Loader2 aria-hidden className="animate-spin" />
|
|
) : available ? (
|
|
<Download aria-hidden />
|
|
) : (
|
|
<ChevronDown aria-hidden />
|
|
)}
|
|
</>
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
</Tip>
|
|
<DropdownMenuContent align="start" className="w-64 min-w-0 max-w-[calc(100vw-1rem)]" side="bottom">
|
|
{targets.map((entry, index) => (
|
|
<div key={entry.target}>
|
|
{index > 0 && <DropdownMenuSeparator />}
|
|
<DropdownMenuLabel className="truncate">
|
|
{entry.target === 'client'
|
|
? copy.desktopVersion(entry.version ?? copy.unknown)
|
|
: copy.backendVersion(entry.version ?? copy.unknown)}
|
|
</DropdownMenuLabel>
|
|
{entry.target === 'client' && entry.status.detail && (
|
|
<DropdownMenuLabel className="truncate font-normal text-(--ui-text-tertiary)">
|
|
{copy.commit(entry.status.detail)}
|
|
</DropdownMenuLabel>
|
|
)}
|
|
<DropdownMenuItem onSelect={() => openUpdateOverlayFor(entry.target)}>
|
|
{entry.applying || entry.checking ? (
|
|
<Loader2 aria-hidden className="animate-spin" />
|
|
) : entry.status.hasUpdate ? (
|
|
<Download aria-hidden />
|
|
) : (
|
|
<RefreshCw aria-hidden />
|
|
)}
|
|
<span className="truncate">
|
|
{entry.applying
|
|
? copy.updateInProgress
|
|
: entry.checking
|
|
? t.updates.checking
|
|
: entry.status.hasUpdate
|
|
? t.updates.updateNow
|
|
: t.settings.about.checkNow}
|
|
</span>
|
|
</DropdownMenuItem>
|
|
</div>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|