This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { GatewayKey } from '../../api';
|
||||
import {
|
||||
@@ -36,6 +37,7 @@ interface Props {
|
||||
* participate in the surrounding form's draft/dirty/Save&Apply bar.
|
||||
*/
|
||||
export function GatewayKeysSection({ showToast }: Props) {
|
||||
const { t } = useTranslation('settings');
|
||||
const qc = useQueryClient();
|
||||
const [teamFilter, setTeamFilter] = useState('');
|
||||
const [activeOnly, setActiveOnly] = useState(false);
|
||||
@@ -73,7 +75,7 @@ export function GatewayKeysSection({ showToast }: Props) {
|
||||
setRawDialog({ rawKey: created.key, team: created.team, reason: 'created' });
|
||||
}
|
||||
await qc.invalidateQueries({ queryKey: ['gateway-keys'] });
|
||||
notify('Gateway key を発行しました');
|
||||
notify(t('gateway.keys.toast.created'));
|
||||
} catch (e) {
|
||||
setCreateError(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
@@ -82,25 +84,25 @@ export function GatewayKeysSection({ showToast }: Props) {
|
||||
}
|
||||
|
||||
async function handleRotate(row: GatewayKey): Promise<void> {
|
||||
if (!confirm(`team=${row.team} のキーをローテーションしますか?\n旧キーは無効になります。`)) return;
|
||||
if (!confirm(t('gateway.keys.confirmRotate', { team: row.team }))) return;
|
||||
try {
|
||||
const created = await rotateGatewayKey(row.id);
|
||||
if (created.key) {
|
||||
setRawDialog({ rawKey: created.key, team: created.team, reason: 'rotated' });
|
||||
}
|
||||
await qc.invalidateQueries({ queryKey: ['gateway-keys'] });
|
||||
notify('Rotate しました');
|
||||
notify(t('gateway.keys.toast.rotated'));
|
||||
} catch (e) {
|
||||
notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRevoke(row: GatewayKey): Promise<void> {
|
||||
if (!confirm(`team=${row.team} のキー (${row.keyPrefix}…) を Revoke しますか?\nこの操作は取り消せません。`)) return;
|
||||
if (!confirm(t('gateway.keys.confirmRevoke', { team: row.team, prefix: row.keyPrefix }))) return;
|
||||
try {
|
||||
await revokeGatewayKey(row.id);
|
||||
await qc.invalidateQueries({ queryKey: ['gateway-keys'] });
|
||||
notify('Revoke しました');
|
||||
notify(t('gateway.keys.toast.revoked'));
|
||||
} catch (e) {
|
||||
notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
}
|
||||
@@ -110,7 +112,7 @@ export function GatewayKeysSection({ showToast }: Props) {
|
||||
try {
|
||||
await patchGatewayKey(id, patch);
|
||||
await qc.invalidateQueries({ queryKey: ['gateway-keys'] });
|
||||
notify('更新しました');
|
||||
notify(t('gateway.keys.toast.updated'));
|
||||
} catch (e) {
|
||||
notify(e instanceof Error ? e.message : String(e), 'error');
|
||||
}
|
||||
@@ -175,7 +177,7 @@ export function GatewayKeysSection({ showToast }: Props) {
|
||||
onClick={() => { setCreateError(null); setCreating(true); }}
|
||||
className="px-3 py-1.5 text-sm rounded bg-accent text-white hover:bg-accent-strong"
|
||||
>
|
||||
+ 新規発行
|
||||
{t('gateway.keys.newIssue')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -183,12 +185,12 @@ export function GatewayKeysSection({ showToast }: Props) {
|
||||
{isLoading && <div className="p-3 text-sm text-slate-500">Loading…</div>}
|
||||
{error && (
|
||||
<div className="p-3 text-sm text-red-600">
|
||||
取得エラー: {String((error as Error).message ?? error)}
|
||||
{t('gateway.keys.fetchError', { msg: String((error as Error).message ?? error) })}
|
||||
</div>
|
||||
)}
|
||||
{data && data.length === 0 && (
|
||||
<div className="p-6 text-center text-sm text-slate-400">
|
||||
キーが登録されていません。「+ 新規発行」から作成できます。
|
||||
{t('gateway.keys.empty')}
|
||||
</div>
|
||||
)}
|
||||
{data && data.length > 0 && (
|
||||
@@ -241,7 +243,7 @@ export function GatewayKeysSection({ showToast }: Props) {
|
||||
disabled={isRevoked || isConfig}
|
||||
onClick={() => setBudgetDraft({ id: row.id, value: row.tokensBudget?.toString() ?? '' })}
|
||||
className="text-left disabled:cursor-not-allowed hover:underline"
|
||||
title={isConfig ? 'config-import keys は config.yaml で管理' : isRevoked ? 'revoked' : 'click to edit'}
|
||||
title={isConfig ? t('gateway.keys.titleConfigManaged') : isRevoked ? 'revoked' : 'click to edit'}
|
||||
>
|
||||
{row.tokensBudget !== null ? row.tokensBudget.toLocaleString() : <span className="text-slate-400">∞</span>}
|
||||
</button>
|
||||
@@ -267,7 +269,7 @@ export function GatewayKeysSection({ showToast }: Props) {
|
||||
disabled={isRevoked || isConfig}
|
||||
onClick={() => setRpmDraft({ id: row.id, value: row.rateLimitRpm?.toString() ?? '' })}
|
||||
className="text-left disabled:cursor-not-allowed hover:underline"
|
||||
title={isConfig ? 'config-import keys は config.yaml で管理' : isRevoked ? 'revoked' : 'click to edit'}
|
||||
title={isConfig ? t('gateway.keys.titleConfigManaged') : isRevoked ? 'revoked' : 'click to edit'}
|
||||
>
|
||||
{row.rateLimitRpm !== null ? row.rateLimitRpm.toString() : <span className="text-slate-400">∞</span>}
|
||||
</button>
|
||||
@@ -294,7 +296,7 @@ export function GatewayKeysSection({ showToast }: Props) {
|
||||
onClick={() => setUsagePanelId(row.id)}
|
||||
className="px-2 py-0.5 text-xs rounded border border-slate-300 hover:bg-slate-50"
|
||||
>
|
||||
詳細
|
||||
{t('gateway.keys.detail')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -323,8 +325,7 @@ export function GatewayKeysSection({ showToast }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-slate-500">
|
||||
Tokens budget は月次 UTC でリセット。Rate limit (rpm) は 60 秒スライディングウィンドウ。
|
||||
config-import のキー(config.yaml から取り込まれたもの)は値の編集ができません。
|
||||
{t('gateway.keys.footer')}
|
||||
</div>
|
||||
|
||||
{creating && (
|
||||
|
||||
Reference in New Issue
Block a user