tailscale install check!
This commit is contained in:
@@ -47,6 +47,7 @@
|
|||||||
},
|
},
|
||||||
"deb": {
|
"deb": {
|
||||||
"compression": "gz",
|
"compression": "gz",
|
||||||
|
"afterRemove": "build/linux/after-remove.sh",
|
||||||
"depends": [
|
"depends": [
|
||||||
"xdotool"
|
"xdotool"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -487,6 +487,26 @@ function tailscaleCommandEnv(loginServer: string): NodeJS.ProcessEnv | undefined
|
|||||||
return /^http:\/\//i.test(loginServer.trim()) ? { TS_DEBUG_ALLOW_HTTP: '1' } : undefined
|
return /^http:\/\//i.test(loginServer.trim()) ? { TS_DEBUG_ALLOW_HTTP: '1' } : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function installTailscale(): Promise<CommandResult> {
|
||||||
|
const installScript = [
|
||||||
|
'set -e',
|
||||||
|
'if command -v tailscale >/dev/null 2>&1; then exit 0; fi',
|
||||||
|
'curl -fsSL https://tailscale.com/install.sh | sh',
|
||||||
|
'mkdir -p /var/lib/autonomy-studio',
|
||||||
|
"printf '%s\\n' installed_by=autonomy-studio-launcher > /var/lib/autonomy-studio/managed-tailscale",
|
||||||
|
].join('; ')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await runCommand('pkexec', ['/bin/sh', '-c', installScript], 120000)
|
||||||
|
return { ok: true, message: 'Tailscale 설치 완료' }
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message: commandErrorMessage(error, 'Tailscale 설치 실패'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hasTailscaleCoordinationIssue(health?: string[]): boolean {
|
function hasTailscaleCoordinationIssue(health?: string[]): boolean {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
health?.some((message) =>
|
health?.some((message) =>
|
||||||
@@ -935,6 +955,7 @@ function createWindow(): void {
|
|||||||
ipcMain.handle('clear-launcher-cache', () => clearLauncherCache())
|
ipcMain.handle('clear-launcher-cache', () => clearLauncherCache())
|
||||||
ipcMain.handle('start-google-login', (_, url: string) => shell.openExternal(url))
|
ipcMain.handle('start-google-login', (_, url: string) => shell.openExternal(url))
|
||||||
ipcMain.handle('launcher-tailscale-status', () => readTailscaleStatus())
|
ipcMain.handle('launcher-tailscale-status', () => readTailscaleStatus())
|
||||||
|
ipcMain.handle('launcher-tailscale-install', () => installTailscale())
|
||||||
ipcMain.handle('launcher-ssh-key-ensure', () => ensureLauncherSshKey())
|
ipcMain.handle('launcher-ssh-key-ensure', () => ensureLauncherSshKey())
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
'launcher-tailscale-login',
|
'launcher-tailscale-login',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const appUrl = decodeURIComponent(
|
|||||||
const appWindowMeta = { isAppWindow, appName, url: appUrl }
|
const appWindowMeta = { isAppWindow, appName, url: appUrl }
|
||||||
const launcherTailscale = {
|
const launcherTailscale = {
|
||||||
status: () => ipcRenderer.invoke('launcher-tailscale-status'),
|
status: () => ipcRenderer.invoke('launcher-tailscale-status'),
|
||||||
|
install: () => ipcRenderer.invoke('launcher-tailscale-install'),
|
||||||
login: (authKey: string, loginServer: string, hostname?: string, forceReset?: boolean) =>
|
login: (authKey: string, loginServer: string, hostname?: string, forceReset?: boolean) =>
|
||||||
ipcRenderer.invoke('launcher-tailscale-login', authKey, loginServer, hostname, forceReset),
|
ipcRenderer.invoke('launcher-tailscale-login', authKey, loginServer, hostname, forceReset),
|
||||||
logout: () => ipcRenderer.invoke('launcher-tailscale-logout'),
|
logout: () => ipcRenderer.invoke('launcher-tailscale-logout'),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import {
|
import {
|
||||||
Code2,
|
Code2,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
@@ -92,6 +92,13 @@ type VpnPlatformPeer = {
|
|||||||
currentAddress?: string
|
currentAddress?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isTailscaleMissingStatus(status: Awaited<ReturnType<NonNullable<typeof window.launcherTailscale>['status']>> | null): boolean {
|
||||||
|
return Boolean(
|
||||||
|
status?.backendState === 'Unavailable' &&
|
||||||
|
/command not found|enoent|no such file|명령을 찾을 수 없습니다/i.test(status.error ?? '')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function Sidebar({ apps, userName, userEmail, userRole, userCreatedAt, userLastLoginAt, primaryAffiliation, currentLicense, activeApp, onAppChange, onLogout, collapsed, onToggleCollapse, launcherUpdateAvailable, launcherUpdating, launcherUpdateMessage, onLauncherUpdate, authAccessToken }: SidebarProps): React.JSX.Element {
|
function Sidebar({ apps, userName, userEmail, userRole, userCreatedAt, userLastLoginAt, primaryAffiliation, currentLicense, activeApp, onAppChange, onLogout, collapsed, onToggleCollapse, launcherUpdateAvailable, launcherUpdating, launcherUpdateMessage, onLauncherUpdate, authAccessToken }: SidebarProps): React.JSX.Element {
|
||||||
const [menuOpen, setMenuOpen] = useState(false)
|
const [menuOpen, setMenuOpen] = useState(false)
|
||||||
const [modal, setModal] = useState<UserModal>(null)
|
const [modal, setModal] = useState<UserModal>(null)
|
||||||
@@ -369,53 +376,79 @@ function Sidebar({ apps, userName, userEmail, userRole, userCreatedAt, userLastL
|
|||||||
|
|
||||||
function PlatformManagementModal({ onClose }: { onClose: () => void }): React.JSX.Element {
|
function PlatformManagementModal({ onClose }: { onClose: () => void }): React.JSX.Element {
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [installing, setInstalling] = useState(false)
|
||||||
|
const [tailscaleMissing, setTailscaleMissing] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [platforms, setPlatforms] = useState<VpnPlatformPeer[]>([])
|
const [platforms, setPlatforms] = useState<VpnPlatformPeer[]>([])
|
||||||
const [selectedId, setSelectedId] = useState('')
|
const [selectedId, setSelectedId] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
const loadPlatforms = useCallback(async (cancelledRef?: { current: boolean }): Promise<void> => {
|
||||||
let cancelled = false
|
setLoading(true)
|
||||||
|
setError('')
|
||||||
|
setTailscaleMissing(false)
|
||||||
|
|
||||||
const loadPlatforms = async (): Promise<void> => {
|
try {
|
||||||
setLoading(true)
|
if (!window.launcherTailscale) {
|
||||||
setError('')
|
throw new Error('VPN 상태 조회 기능을 사용할 수 없습니다.')
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
const status = await window.launcherTailscale.status()
|
||||||
if (!window.launcherTailscale) {
|
if (isTailscaleMissingStatus(status)) {
|
||||||
throw new Error('VPN 상태 조회 기능을 사용할 수 없습니다.')
|
if (!cancelledRef?.current) {
|
||||||
}
|
setTailscaleMissing(true)
|
||||||
|
|
||||||
const status = await window.launcherTailscale.status()
|
|
||||||
const peers = status.peers ?? []
|
|
||||||
const nextPlatforms = peers
|
|
||||||
.filter((peer) => !peer.hostName.toLowerCase().includes('launcher-client'))
|
|
||||||
.sort((a, b) => Number(b.online || b.active) - Number(a.online || a.active) || a.hostName.localeCompare(b.hostName))
|
|
||||||
|
|
||||||
if (!cancelled) {
|
|
||||||
setPlatforms(nextPlatforms)
|
|
||||||
setSelectedId((current) => {
|
|
||||||
if (current && nextPlatforms.some((platform) => platform.id === current)) return current
|
|
||||||
return nextPlatforms[0]?.id ?? ''
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (loadError) {
|
|
||||||
if (!cancelled) {
|
|
||||||
setError(loadError instanceof Error ? loadError.message : '플랫폼 목록을 불러오지 못했습니다.')
|
|
||||||
setPlatforms([])
|
setPlatforms([])
|
||||||
setSelectedId('')
|
setSelectedId('')
|
||||||
}
|
}
|
||||||
} finally {
|
return
|
||||||
if (!cancelled) setLoading(false)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void loadPlatforms()
|
const peers = status.peers ?? []
|
||||||
|
const nextPlatforms = peers
|
||||||
|
.filter((peer) => !peer.hostName.toLowerCase().includes('launcher-client'))
|
||||||
|
.sort((a, b) => Number(b.online || b.active) - Number(a.online || a.active) || a.hostName.localeCompare(b.hostName))
|
||||||
|
|
||||||
return () => {
|
if (!cancelledRef?.current) {
|
||||||
cancelled = true
|
setPlatforms(nextPlatforms)
|
||||||
|
setSelectedId((current) => {
|
||||||
|
if (current && nextPlatforms.some((platform) => platform.id === current)) return current
|
||||||
|
return nextPlatforms[0]?.id ?? ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (loadError) {
|
||||||
|
if (!cancelledRef?.current) {
|
||||||
|
setError(loadError instanceof Error ? loadError.message : '플랫폼 목록을 불러오지 못했습니다.')
|
||||||
|
setPlatforms([])
|
||||||
|
setSelectedId('')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelledRef?.current) setLoading(false)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cancelledRef = { current: false }
|
||||||
|
void loadPlatforms(cancelledRef)
|
||||||
|
return () => {
|
||||||
|
cancelledRef.current = true
|
||||||
|
}
|
||||||
|
}, [loadPlatforms])
|
||||||
|
|
||||||
|
const installTailscale = async (): Promise<void> => {
|
||||||
|
if (!window.launcherTailscale?.install) return
|
||||||
|
setInstalling(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
const result = await window.launcherTailscale.install()
|
||||||
|
if (!result.ok) {
|
||||||
|
setError(result.message || 'Tailscale 설치에 실패했습니다.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await loadPlatforms()
|
||||||
|
} finally {
|
||||||
|
setInstalling(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const selectedPlatform = platforms.find((platform) => platform.id === selectedId) ?? platforms[0]
|
const selectedPlatform = platforms.find((platform) => platform.id === selectedId) ?? platforms[0]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -441,6 +474,19 @@ function PlatformManagementModal({ onClose }: { onClose: () => void }): React.JS
|
|||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="px-2 py-8 text-center text-[12px] font-semibold leading-5 text-[#ff8c8c]">{error}</div>
|
<div className="px-2 py-8 text-center text-[12px] font-semibold leading-5 text-[#ff8c8c]">{error}</div>
|
||||||
|
) : tailscaleMissing ? (
|
||||||
|
<div className="px-2 py-8 text-center">
|
||||||
|
<p className="text-[12px] font-bold leading-5 text-white/72">원격 연결 구성 요소 설치가 필요합니다.</p>
|
||||||
|
<p className="mt-2 text-[11px] font-medium leading-5 text-white/45">플랫폼 연결을 위해 Tailscale을 설치해 주세요.</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void installTailscale()}
|
||||||
|
disabled={installing}
|
||||||
|
className="mt-4 h-8 cursor-pointer rounded-md border border-[#5588ff] bg-[#5588ff] px-3 text-[11px] font-bold text-white transition hover:bg-[#4778ea] disabled:cursor-default disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{installing ? '설치 중...' : '설치'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
) : platforms.length === 0 ? (
|
) : platforms.length === 0 ? (
|
||||||
<div className="px-2 py-8 text-center text-[12px] font-semibold leading-5 text-white/45">연결된 플랫폼이 없습니다.</div>
|
<div className="px-2 py-8 text-center text-[12px] font-semibold leading-5 text-white/45">연결된 플랫폼이 없습니다.</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -473,7 +519,22 @@ function PlatformManagementModal({ onClose }: { onClose: () => void }): React.JS
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section className="min-h-0 overflow-y-auto px-6 py-5">
|
<section className="min-h-0 overflow-y-auto px-6 py-5">
|
||||||
{!selectedPlatform ? (
|
{tailscaleMissing ? (
|
||||||
|
<div className="grid h-full place-items-center px-8 text-center">
|
||||||
|
<div>
|
||||||
|
<p className="text-[15px] font-bold text-white/86">Tailscale 설치가 필요합니다.</p>
|
||||||
|
<p className="mt-2 text-[12px] font-medium leading-5 text-white/50">Autonomy Studio는 원격 플랫폼 연결을 위해 Tailscale을 사용합니다. 설치에는 관리자 권한이 필요합니다.</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void installTailscale()}
|
||||||
|
disabled={installing}
|
||||||
|
className="mt-5 h-9 cursor-pointer rounded-md border border-[#5588ff] bg-[#5588ff] px-4 text-[12px] font-bold text-white transition hover:bg-[#4778ea] disabled:cursor-default disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{installing ? '설치 중...' : '설치'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : !selectedPlatform ? (
|
||||||
<div className="grid h-full place-items-center text-[13px] font-semibold text-white/45">플랫폼을 선택하세요.</div>
|
<div className="grid h-full place-items-center text-[13px] font-semibold text-white/45">플랫폼을 선택하세요.</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Vendored
+4
@@ -36,6 +36,10 @@ declare global {
|
|||||||
}>
|
}>
|
||||||
error?: string
|
error?: string
|
||||||
}>
|
}>
|
||||||
|
install: () => Promise<{
|
||||||
|
ok: boolean
|
||||||
|
message: string
|
||||||
|
}>
|
||||||
login: (authKey: string, loginServer: string, hostname?: string, forceReset?: boolean) => Promise<{
|
login: (authKey: string, loginServer: string, hostname?: string, forceReset?: boolean) => Promise<{
|
||||||
ok: boolean
|
ok: boolean
|
||||||
message: string
|
message: string
|
||||||
|
|||||||
@@ -598,7 +598,8 @@ function Dashboard({ authSession, onSessionUpdate, onLogout }: DashboardProps):
|
|||||||
<div className="relative" ref={noticeRef}>
|
<div className="relative" ref={noticeRef}>
|
||||||
<Tooltip label="공지사항">
|
<Tooltip label="공지사항">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={(event) => {
|
||||||
|
event.currentTarget.blur()
|
||||||
markAppNoticesRead()
|
markAppNoticesRead()
|
||||||
setNoticeOpen((v) => !v)
|
setNoticeOpen((v) => !v)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -59,8 +59,24 @@ type TailscaleProvisionResponse = {
|
|||||||
type PendingTailscaleConfirm = {
|
type PendingTailscaleConfirm = {
|
||||||
session: AuthSession
|
session: AuthSession
|
||||||
email: string
|
email: string
|
||||||
|
password: string
|
||||||
keepLogin: boolean
|
keepLogin: boolean
|
||||||
message: string
|
message: string
|
||||||
|
reason: 'install-required' | 'connection-failed'
|
||||||
|
}
|
||||||
|
|
||||||
|
class TailscaleInstallRequiredError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super('원격 연결 구성 요소 설치가 필요합니다.')
|
||||||
|
this.name = 'TailscaleInstallRequiredError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTailscaleMissingStatus(status: Awaited<ReturnType<NonNullable<typeof window.launcherTailscale>['status']>> | null): boolean {
|
||||||
|
return Boolean(
|
||||||
|
status?.backendState === 'Unavailable' &&
|
||||||
|
/command not found|enoent|no such file|명령을 찾을 수 없습니다/i.test(status.error ?? '')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractProvisionErrorMessage(payload: unknown): string {
|
function extractProvisionErrorMessage(payload: unknown): string {
|
||||||
@@ -134,11 +150,8 @@ async function ensureLauncherTailscale(account: string, password: string): Promi
|
|||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (
|
if (isTailscaleMissingStatus(status)) {
|
||||||
status?.backendState === 'Unavailable' &&
|
throw new TailscaleInstallRequiredError()
|
||||||
/command not found|enoent|no such file|명령을 찾을 수 없습니다/i.test(status.error ?? '')
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${LAUNCHER_API_BASE_URL}/api/provision`, {
|
const response = await fetch(`${LAUNCHER_API_BASE_URL}/api/provision`, {
|
||||||
@@ -185,6 +198,7 @@ function LoginForm({ onLogin, onGoSignup, onGoPasswordReset, onLoadingChange }:
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [pendingTailscaleConfirm, setPendingTailscaleConfirm] = useState<PendingTailscaleConfirm | null>(null)
|
const [pendingTailscaleConfirm, setPendingTailscaleConfirm] = useState<PendingTailscaleConfirm | null>(null)
|
||||||
|
const [tailscaleInstalling, setTailscaleInstalling] = useState(false)
|
||||||
const emailError = email.length > 0 && !EMAIL_PATTERN.test(email) ? '올바른 이메일 형식으로 입력해주세요.' : ''
|
const emailError = email.length > 0 && !EMAIL_PATTERN.test(email) ? '올바른 이메일 형식으로 입력해주세요.' : ''
|
||||||
|
|
||||||
const completeLogin = async (session: AuthSession, loginEmail: string, persistLogin: boolean, startedAt: number): Promise<void> => {
|
const completeLogin = async (session: AuthSession, loginEmail: string, persistLogin: boolean, startedAt: number): Promise<void> => {
|
||||||
@@ -282,7 +296,9 @@ function LoginForm({ onLogin, onGoSignup, onGoPasswordReset, onLoadingChange }:
|
|||||||
setPendingTailscaleConfirm({
|
setPendingTailscaleConfirm({
|
||||||
session,
|
session,
|
||||||
email,
|
email,
|
||||||
|
password,
|
||||||
keepLogin,
|
keepLogin,
|
||||||
|
reason: tailscaleError instanceof TailscaleInstallRequiredError ? 'install-required' : 'connection-failed',
|
||||||
message: tailscaleError instanceof Error ? tailscaleError.message : 'Tailscale 연결에 실패했습니다.'
|
message: tailscaleError instanceof Error ? tailscaleError.message : 'Tailscale 연결에 실패했습니다.'
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -313,6 +329,40 @@ function LoginForm({ onLogin, onGoSignup, onGoPasswordReset, onLoadingChange }:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const installTailscaleAndContinue = async (): Promise<void> => {
|
||||||
|
if (!pendingTailscaleConfirm || !window.launcherTailscale?.install) return
|
||||||
|
const pending = pendingTailscaleConfirm
|
||||||
|
setTailscaleInstalling(true)
|
||||||
|
setError('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const installResult = await window.launcherTailscale.install()
|
||||||
|
if (!installResult.ok) {
|
||||||
|
setPendingTailscaleConfirm({
|
||||||
|
...pending,
|
||||||
|
message: installResult.message || 'Tailscale 설치에 실패했습니다.',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureLauncherTailscale(pending.email, pending.password)
|
||||||
|
setPendingTailscaleConfirm(null)
|
||||||
|
setLoading(true)
|
||||||
|
onLoadingChange(true)
|
||||||
|
await completeLogin(pending.session, pending.email, pending.keepLogin, Date.now())
|
||||||
|
} catch (installError) {
|
||||||
|
setPendingTailscaleConfirm({
|
||||||
|
...pending,
|
||||||
|
reason: installError instanceof TailscaleInstallRequiredError ? 'install-required' : 'connection-failed',
|
||||||
|
message: installError instanceof Error ? installError.message : 'Tailscale 설치 또는 연결에 실패했습니다.',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setTailscaleInstalling(false)
|
||||||
|
setLoading(false)
|
||||||
|
onLoadingChange(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form onSubmit={handleSubmit} className="w-[300px] relative z-[1] animate-reveal-late">
|
<form onSubmit={handleSubmit} className="w-[300px] relative z-[1] animate-reveal-late">
|
||||||
@@ -394,33 +444,49 @@ function LoginForm({ onLogin, onGoSignup, onGoPasswordReset, onLoadingChange }:
|
|||||||
</form>
|
</form>
|
||||||
{pendingTailscaleConfirm && createPortal(
|
{pendingTailscaleConfirm && createPortal(
|
||||||
<div className="fixed inset-0 z-[200] grid place-items-center bg-black/45 px-6 backdrop-blur-[2px]">
|
<div className="fixed inset-0 z-[200] grid place-items-center bg-black/45 px-6 backdrop-blur-[2px]">
|
||||||
<section className="w-full max-w-[380px] overflow-hidden rounded-xl border border-white/14 bg-[#121722] shadow-[0_28px_80px_rgba(0,0,0,0.45)]">
|
<section className="w-full max-w-[400px] overflow-hidden rounded-xl border border-white/14 bg-[#121722] shadow-[0_28px_80px_rgba(0,0,0,0.45)]">
|
||||||
<div className="border-b border-white/10 px-5 py-4">
|
<div className="border-b border-white/10 px-5 py-4">
|
||||||
<h3 className="text-[15px] font-bold text-white">Tailscale 연결 실패</h3>
|
<h3 className="text-[15px] font-bold text-white">
|
||||||
|
{pendingTailscaleConfirm.reason === 'install-required' ? '원격 연결 구성 요소 설치' : 'Tailscale 연결 실패'}
|
||||||
|
</h3>
|
||||||
<p className="mt-2 text-[12px] font-medium leading-relaxed text-white/62">
|
<p className="mt-2 text-[12px] font-medium leading-relaxed text-white/62">
|
||||||
플랫폼 원격 제어 연결을 설정하지 못했습니다. 로그인은 계속 진행할 수 있지만, 원격 플랫폼 연결 기능은 제한될 수 있습니다.
|
{pendingTailscaleConfirm.reason === 'install-required'
|
||||||
|
? 'Autonomy Studio는 플랫폼 연결을 위해 Tailscale을 사용합니다. 설치를 진행하려면 관리자 권한이 필요합니다.'
|
||||||
|
: '플랫폼 원격 제어 연결을 설정하지 못했습니다. 로그인은 계속 진행할 수 있지만, 원격 플랫폼 연결 기능은 제한될 수 있습니다.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
<div className="max-h-[96px] overflow-y-auto rounded-lg border border-white/10 bg-black/20 px-3 py-2 text-[11px] font-medium leading-relaxed text-white/56">
|
<div className="max-h-[96px] overflow-y-auto rounded-lg border border-white/10 bg-black/20 px-3 py-2 text-[11px] font-medium leading-relaxed text-white/56">
|
||||||
{pendingTailscaleConfirm.message}
|
{pendingTailscaleConfirm.reason === 'install-required' ? 'Tailscale이 설치되어 있지 않습니다.' : pendingTailscaleConfirm.message}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2 border-t border-white/10 px-5 py-4">
|
<div className="flex justify-end gap-2 border-t border-white/10 px-5 py-4">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setPendingTailscaleConfirm(null)}
|
|
||||||
className="h-9 cursor-pointer rounded-md border border-white/16 bg-white/6 px-4 text-[12px] font-bold text-white/72 transition hover:bg-white/10 hover:text-white"
|
|
||||||
>
|
|
||||||
취소
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void continueWithoutTailscale()}
|
onClick={() => void continueWithoutTailscale()}
|
||||||
className="h-9 cursor-pointer rounded-md border border-[#5588ff] bg-[#5588ff] px-4 text-[12px] font-bold text-white transition hover:bg-[#4778ea]"
|
className="h-9 cursor-pointer rounded-md border border-white/16 bg-white/6 px-4 text-[12px] font-bold text-white/72 transition hover:bg-white/10 hover:text-white"
|
||||||
|
disabled={tailscaleInstalling}
|
||||||
>
|
>
|
||||||
계속 진행
|
나중에
|
||||||
</button>
|
</button>
|
||||||
|
{pendingTailscaleConfirm.reason === 'install-required' ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void installTailscaleAndContinue()}
|
||||||
|
className="h-9 cursor-pointer rounded-md border border-[#5588ff] bg-[#5588ff] px-4 text-[12px] font-bold text-white transition hover:bg-[#4778ea] disabled:cursor-default disabled:opacity-60"
|
||||||
|
disabled={tailscaleInstalling}
|
||||||
|
>
|
||||||
|
{tailscaleInstalling ? '설치 중...' : '설치'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void continueWithoutTailscale()}
|
||||||
|
className="h-9 cursor-pointer rounded-md border border-[#5588ff] bg-[#5588ff] px-4 text-[12px] font-bold text-white transition hover:bg-[#4778ea]"
|
||||||
|
>
|
||||||
|
계속 진행
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>,
|
</div>,
|
||||||
|
|||||||
Reference in New Issue
Block a user