sync: update from private repo (e62f5c7)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-11 01:52:48 +00:00
parent 000a2474aa
commit d061ad08d8
237 changed files with 8441 additions and 5549 deletions
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useTranslation, Trans } from 'react-i18next';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import type { SshConnection, TestResponse } from '../../lib/ssh-types';
import { SshConnectionForm } from './SshConnectionForm';
@@ -124,6 +125,7 @@ interface SshConnectionsPanelProps {
}
export function SshConnectionsPanel({ showToast }: SshConnectionsPanelProps = {}) {
const { t } = useTranslation('userfolder');
const qc = useQueryClient();
const { data, isLoading, error } = useQuery({
queryKey: ['ssh', 'connections'],
@@ -145,7 +147,7 @@ export function SshConnectionsPanel({ showToast }: SshConnectionsPanelProps = {}
onSuccess: (resp) => {
qc.invalidateQueries({ queryKey: ['ssh', 'connections'] });
setCreating(false);
showToast?.('SSH 接続を作成しました', 'success');
showToast?.(t('ssh.toast.created'), 'success');
// If the server returned a public key (always for keypairSource=generate;
// also surfaced for provided keys), open the dialog so the user can
// copy it into authorized_keys.
@@ -167,11 +169,11 @@ export function SshConnectionsPanel({ showToast }: SshConnectionsPanelProps = {}
if (publicKey) {
setPubKeyDialog({ publicKey, label, freshlyGenerated: false });
} else {
showToast?.('公開鍵の取得に失敗しました', 'error');
showToast?.(t('ssh.toast.publicKeyFetchFailed'), 'error');
}
},
onError: (e) => {
showToast?.(e instanceof Error ? e.message : '公開鍵取得失敗', 'error');
showToast?.(e instanceof Error ? e.message : t('ssh.toast.publicKeyFetchFailedShort'), 'error');
},
});
const patchMutation = useMutation({
@@ -179,17 +181,17 @@ export function SshConnectionsPanel({ showToast }: SshConnectionsPanelProps = {}
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['ssh', 'connections'] });
setEditingId(null);
showToast?.('SSH 接続を更新しました', 'success');
showToast?.(t('ssh.toast.updated'), 'success');
},
});
const deleteMutation = useMutation({
mutationFn: apiDelete,
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['ssh', 'connections'] });
showToast?.('SSH 接続を削除しました', 'success');
showToast?.(t('ssh.toast.deleted'), 'success');
},
onError: (e) => {
showToast?.(e instanceof Error ? e.message : '削除失敗', 'error');
showToast?.(e instanceof Error ? e.message : t('ssh.toast.deleteFailed'), 'error');
},
});
const testMutation = useMutation({
@@ -198,33 +200,31 @@ export function SshConnectionsPanel({ showToast }: SshConnectionsPanelProps = {}
qc.invalidateQueries({ queryKey: ['ssh', 'connections'] });
// Surface result. pass = already verified; first_observe/mismatch = needs confirm.
if (response.verdict === 'pass') {
showToast?.(`ホストキーは一致しています (${response.fingerprint.slice(0, 20)}…)`, 'success');
showToast?.(t('ssh.toast.hostKeyMatch', { fingerprint: response.fingerprint.slice(0, 20) }), 'success');
} else if (response.verdict === 'first_observe' || response.verdict === 'mismatch') {
setTestResult({ id, test: response, replaceMode: response.verdict === 'mismatch' });
} else if (response.verdict === 'alg_not_allowed') {
showToast?.('ホストキーのアルゴリズムが許可リストにありません', 'error');
showToast?.(t('ssh.toast.algNotAllowed'), 'error');
}
},
onError: (e) => {
showToast?.(e instanceof Error ? e.message : 'テスト失敗', 'error');
showToast?.(e instanceof Error ? e.message : t('ssh.toast.testFailed'), 'error');
},
});
async function handleVerifyHostKey(connId: string, args: { fingerprint: string; token: string; reason?: string }) {
await apiVerifyHostKey(connId, args);
qc.invalidateQueries({ queryKey: ['ssh', 'connections'] });
showToast?.('ホストキーを検証しました', 'success');
showToast?.(t('ssh.toast.hostKeyVerified'), 'success');
}
if (data?.sshDisabled) {
return (
<div className="h-full overflow-y-auto">
<div className="max-w-2xl mx-auto px-6 py-8">
<h2 className="text-base font-semibold text-slate-900 mb-2">SSH </h2>
<h2 className="text-base font-semibold text-slate-900 mb-2">{t('ssh.title')}</h2>
<div className="text-xs text-slate-600 bg-surface border border-hairline rounded-md p-3 leading-relaxed">
SSH <code className="font-mono">config.yaml</code> {' '}
<code className="font-mono">ssh.enabled: true</code> {' '}
<code className="font-mono">MCP_ENCRYPTION_KEY</code>
<Trans t={t} i18nKey="ssh.disabled" components={{ code: <code className="font-mono" /> }} />
</div>
</div>
</div>
@@ -239,9 +239,9 @@ export function SshConnectionsPanel({ showToast }: SshConnectionsPanelProps = {}
<div className="max-w-3xl mx-auto px-6 py-6">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-base font-semibold text-slate-900">SSH </h2>
<h2 className="text-base font-semibold text-slate-900">{t('ssh.title')}</h2>
<p className="text-2xs text-slate-500 mt-0.5">
SshExec / SshUpload / SshDownload SSH
{t('ssh.intro')}
</p>
</div>
<button
@@ -250,16 +250,16 @@ export function SshConnectionsPanel({ showToast }: SshConnectionsPanelProps = {}
className="px-3 h-7 text-xs font-semibold bg-accent text-accent-fg rounded-md hover:bg-accent-deep"
disabled={creating}
>
+
{t('ssh.create')}
</button>
</div>
{isLoading && <div className="text-xs text-slate-400">Loading</div>}
{error && <div className="text-xs text-red-500">: {String(error)}</div>}
{isLoading && <div className="text-xs text-slate-400">{t('common.loading')}</div>}
{error && <div className="text-xs text-red-500">{t('ssh.loadFailed', { error: String(error) })}</div>}
{creating && (
<section className="mb-5 border border-accent/40 rounded-md bg-canvas p-4">
<h3 className="text-xs font-semibold text-slate-700 mb-2"> SSH </h3>
<h3 className="text-xs font-semibold text-slate-700 mb-2">{t('ssh.newHeading')}</h3>
<SshConnectionForm
existing={null}
adminContext={false}
@@ -269,10 +269,10 @@ export function SshConnectionsPanel({ showToast }: SshConnectionsPanelProps = {}
</section>
)}
<SectionHeader title="自分の接続" count={owned.length} />
<SectionHeader title={t('ssh.ownSection')} count={owned.length} />
{owned.length === 0 && !creating && (
<div className="text-xs text-slate-400 px-3 py-4">
+
{t('ssh.ownEmpty')}
</div>
)}
<ul className="divide-y divide-hairline mb-6">
@@ -286,7 +286,7 @@ export function SshConnectionsPanel({ showToast }: SshConnectionsPanelProps = {}
onCancelEdit={() => setEditingId(null)}
onPatch={async (body) => { await patchMutation.mutateAsync({ id: c.id, body }); }}
onDelete={() => {
if (window.confirm(`接続 "${c.label}" を削除しますか?`)) {
if (window.confirm(t('ssh.confirmDelete', { label: c.label }))) {
deleteMutation.mutate(c.id);
}
}}
@@ -300,9 +300,9 @@ export function SshConnectionsPanel({ showToast }: SshConnectionsPanelProps = {}
{globals.length > 0 && (
<>
<SectionHeader title="グローバル接続 (info-only)" count={globals.length} />
<SectionHeader title={t('ssh.globalSection')} count={globals.length} />
<p className="text-2xs text-slate-500 px-3 mb-2">
grant
{t('ssh.globalNote')}
</p>
<ul className="divide-y divide-hairline">
{globals.map(c => (
@@ -374,6 +374,7 @@ interface ConnectionRowProps {
}
function ConnectionRow(props: ConnectionRowProps) {
const { t } = useTranslation('userfolder');
const {
connection: c, isOwner, editing,
onEdit, onCancelEdit, onPatch, onDelete, onTest, testing,
@@ -408,7 +409,7 @@ function ConnectionRow(props: ConnectionRowProps) {
)}
</div>
{c.disabledByAdminReason && (
<div className="text-2xs text-red-700 dark:text-red-300 mt-0.5">: {c.disabledByAdminReason}</div>
<div className="text-2xs text-red-700 dark:text-red-300 mt-0.5">{t('ssh.row.reasonLabel', { reason: c.disabledByAdminReason })}</div>
)}
</div>
<div className="flex items-center gap-1 flex-shrink-0 flex-wrap justify-end">
@@ -418,16 +419,16 @@ function ConnectionRow(props: ConnectionRowProps) {
disabled={testing}
className="px-2 h-7 text-2xs text-slate-700 border border-hairline rounded hover:bg-surface disabled:opacity-50"
>
{testing ? 'テスト中…' : 'Test'}
{testing ? t('ssh.row.testing') : t('ssh.row.test')}
</button>
<button
type="button"
onClick={onShowPublicKey}
disabled={showingPublicKey}
title="authorized_keys に貼る公開鍵を表示"
title={t('ssh.row.publicKeyTitle')}
className="px-2 h-7 text-2xs text-slate-700 border border-hairline rounded hover:bg-surface disabled:opacity-50"
>
{showingPublicKey ? '取得中…' : '公開鍵'}
{showingPublicKey ? t('ssh.row.fetching') : t('ssh.row.publicKey')}
</button>
{isOwner && !editing && (
<button
@@ -435,7 +436,7 @@ function ConnectionRow(props: ConnectionRowProps) {
onClick={onEdit}
className="px-2 h-7 text-2xs text-slate-700 border border-hairline rounded hover:bg-surface"
>
{t('ssh.row.edit')}
</button>
)}
{isOwner && (
@@ -444,7 +445,7 @@ function ConnectionRow(props: ConnectionRowProps) {
onClick={onDelete}
className="px-2 h-7 text-2xs text-red-600 border border-hairline rounded hover:bg-red-50 dark:hover:bg-red-500/15"
>
{t('ssh.row.delete')}
</button>
)}
</div>
@@ -469,6 +470,7 @@ function ConnectionRow(props: ConnectionRowProps) {
* answered by selecting/copying without opening any sub-view.
*/
function CopyableUuid({ value }: { value: string }) {
const { t } = useTranslation('userfolder');
const [copied, setCopied] = useState(false);
async function copy() {
try {
@@ -483,10 +485,10 @@ function CopyableUuid({ value }: { value: string }) {
<button
type="button"
onClick={copy}
title={`クリックで UUID をコピー: ${value}`}
title={t('ssh.copyUuid.title', { value })}
className="font-mono hover:underline cursor-pointer text-slate-600 hover:text-accent-deep"
>
{copied ? '✓ コピーしました' : value}
{copied ? t('ssh.copyUuid.copied') : value}
</button>
);
}