feat(admin-ui): catalog refresh + token migration (Stage 3.2)

This commit is contained in:
Александр Зимин
2026-05-11 09:08:38 +00:00
parent 3e19fd2cf9
commit 6db2a0c345
24 changed files with 289 additions and 273 deletions
@@ -119,7 +119,7 @@ export const CascadeConfirmDialog = ({
{plan && (
<>
{/* Header summary */}
<p className="text-sm text-carbon">
<p className="text-sm text-ink">
{hasBlockers
? t('cascade.summary.blocked', {
blockers: plan.blockers.length,
@@ -144,16 +144,16 @@ export const CascadeConfirmDialog = ({
<span className="font-mono">
{b.sourceDict}/{b.businessKey}
</span>
<span className="text-carbon/60">.{b.sourceField}</span>
<span className="text-mute">.{b.sourceField}</span>
</li>
))}
{plan.blockers.length > 5 && (
<li className="text-carbon/60">
<li className="text-mute">
{t('cascade.blockers.more', { count: plan.blockers.length - 5 })}
</li>
)}
</ul>
<p className="mt-2 text-2xs text-carbon/70">
<p className="mt-2 text-2xs text-ink-2">
{t('cascade.blockers.resolveFirst')}
</p>
</Alert>
@@ -164,7 +164,7 @@ export const CascadeConfirmDialog = ({
<section className="space-y-2">
<div className="flex items-center gap-2">
<WarningIcon weight="bold" size={14} className="text-horizon" />
<h4 className="text-sm font-primary">
<h4 className="text-sm font-sans">
{t('cascade.cascade.title', { count: plan.cascade.length })}
</h4>
</div>
@@ -176,13 +176,13 @@ export const CascadeConfirmDialog = ({
return (
<li
key={key}
className="border border-regolith rounded-sm px-3 py-2 text-2xs"
className="border border-line rounded-sm px-3 py-2 text-2xs"
>
<div className="flex items-center gap-2 mb-1">
<span className="font-mono text-ultramarain">
<span className="font-mono text-accent">
{first.sourceDisplayName ?? first.sourceDict}
</span>
<span className="font-mono text-carbon/70">
<span className="font-mono text-ink-2">
.{first.sourceField}
</span>
<Badge variant={onCloseVariant(first.onClose)}>
@@ -190,14 +190,14 @@ export const CascadeConfirmDialog = ({
</Badge>
<Badge variant="neutral">{entries.length}</Badge>
</div>
<ul className="text-2xs text-carbon/70 ml-1 space-y-0.5">
<ul className="text-2xs text-ink-2 ml-1 space-y-0.5">
{sample.map((e) => (
<li key={e.recordId} className="font-mono">
· {e.businessKey}
</li>
))}
{overflow > 0 && (
<li className="text-carbon/50">
<li className="text-mute">
{t('cascade.cascade.more', { count: overflow })}
</li>
)}
@@ -224,7 +224,7 @@ export const CascadeConfirmDialog = ({
<div>
<label
htmlFor="cascade-reason"
className="text-2xs text-carbon/70 uppercase tracking-label"
className="text-2xs text-ink-2 uppercase tracking-[0.10em]"
>
{t('cascade.reason.label')}
</label>
@@ -242,7 +242,7 @@ export const CascadeConfirmDialog = ({
<div>
<label
htmlFor="cascade-confirm"
className="text-2xs text-carbon/70 uppercase tracking-label"
className="text-2xs text-ink-2 uppercase tracking-[0.10em]"
>
{t('cascade.confirmGate.label', { word: 'CONFIRM' })}
</label>
@@ -264,7 +264,7 @@ export const CascadeConfirmDialog = ({
)}
{/* Footer actions */}
<div className="flex items-center justify-end gap-2 pt-2 border-t border-regolith">
<div className="flex items-center justify-end gap-2 pt-2 border-t border-line">
<Button
variant="secondary"
onClick={onClose}
@@ -37,10 +37,10 @@ export const DictionaryDependentsPanel = ({ dictionaryName }: Props) => {
return (
<Panel>
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-primary text-carbon">
<h3 className="text-sm font-sans text-ink">
{t('lineage.usedBy.title')}
</h3>
<span className="text-2xs text-carbon/60">
<span className="text-2xs text-mute">
{t('lineage.usedBy.count', { count: data.length })}
</span>
</div>
@@ -48,21 +48,21 @@ export const DictionaryDependentsPanel = ({ dictionaryName }: Props) => {
{data.map((d) => (
<li
key={`${d.sourceDict}.${d.sourceField}`}
className="flex items-center gap-2 px-2.5 py-1.5 rounded-sm border border-regolith bg-white text-2xs"
className="flex items-center gap-2 px-2.5 py-1.5 rounded-sm border border-line bg-white text-2xs"
>
<Link
to="/dictionaries/$name"
params={{ name: d.sourceDict }}
className="text-ultramarain hover:underline font-mono"
className="text-accent hover:underline font-mono"
aria-label={t('lineage.usedBy.openSourceDict', {
dict: d.sourceDisplayName ?? d.sourceDict,
})}
>
{d.sourceDisplayName ?? d.sourceDict}
</Link>
<span className="font-mono text-carbon/70">.{d.sourceField}</span>
<ArrowRightIcon weight="bold" size={12} className="text-carbon/40" />
<span className="font-mono text-carbon/70">{d.targetField}</span>
<span className="font-mono text-ink-2">.{d.sourceField}</span>
<ArrowRightIcon weight="bold" size={12} className="text-mute/70" />
<span className="font-mono text-ink-2">{d.targetField}</span>
{d.activeRecordsInSourceDict > 0 && (
<Badge variant="neutral">
{t('lineage.usedBy.activeCount', {
@@ -54,7 +54,7 @@ const NeighborCard = ({
to="/dictionaries/$name"
params={{ name: dict.name }}
search={{ view: 'hub' }}
className={`group block bg-white border border-regolith rounded-lg p-3 transition hover:shadow-hover hover:border-ultramarain/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ultramarain/40 ${
className={`group block bg-white border border-line rounded-lg p-3 transition hover:shadow-hover hover:border-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ${
side === 'left' ? 'text-right' : 'text-left'
}`}
>
@@ -65,11 +65,11 @@ const NeighborCard = ({
}`}
aria-hidden="true"
/>
<h4 className="font-primary text-[14px] font-semibold text-ultramarain truncate group-hover:underline">
<h4 className="font-sans text-[14px] font-semibold text-accent truncate group-hover:underline">
{dict.displayName ?? dict.name}
</h4>
</div>
<div className="mt-0.5 font-mono text-[10px] text-carbon/60 truncate">
<div className="mt-0.5 font-mono text-[10px] text-mute truncate">
{dict.name}
</div>
</Link>
@@ -85,7 +85,7 @@ const SectionLabel = ({
const { t } = useTranslation()
return (
<div
className={`text-2xs uppercase tracking-label text-carbon/60 mb-2 ${
className={`text-2xs uppercase tracking-[0.10em] text-mute mb-2 ${
side === 'left' ? 'text-right' : 'text-left'
}`}
>
@@ -141,7 +141,7 @@ export function DictionaryHubView({ detail }: { detail: DictionaryDetail }) {
<div className="md:order-1 order-2 space-y-2">
<SectionLabel side="left" count={outgoingDicts.length} />
{outgoingDicts.length === 0 ? (
<div className="text-2xs text-carbon/40 text-right italic">
<div className="text-2xs text-mute/70 text-right italic">
{t('hub.noOutgoing', { defaultValue: 'нет исходящих ссылок' })}
</div>
) : (
@@ -156,7 +156,7 @@ export function DictionaryHubView({ detail }: { detail: DictionaryDetail }) {
{/* Center: focused dict (large card) */}
<div className="md:order-2 order-1 relative">
<div
className="relative bg-white rounded-xl p-5 border-2 border-ultramarain"
className="relative bg-white rounded-xl p-5 border-2 border-accent"
style={{ boxShadow: '0 18px 36px -22px rgba(20,30,140,.5)' }}
>
<div className="flex items-start gap-2 mb-2 flex-wrap">
@@ -166,22 +166,22 @@ export function DictionaryHubView({ detail }: { detail: DictionaryDetail }) {
{t('dict.list.approval', { defaultValue: 'approval' })}
</Badge>
)}
<span className="ml-auto font-mono text-[11px] text-carbon/60">
<span className="ml-auto font-mono text-[11px] text-mute">
v{detail.schemaVersion}
</span>
</div>
<h2 className="font-primary text-[22px] font-semibold text-ultramarain leading-tight">
<h2 className="font-sans text-[22px] font-semibold text-accent leading-tight">
{detail.displayName ?? detail.name}
</h2>
<div className="font-mono text-[11px] text-carbon/60 mt-0.5">
<div className="font-mono text-[11px] text-mute mt-0.5">
{detail.name}
</div>
{detail.description && (
<p className="text-[13px] text-carbon mt-3 leading-relaxed">
<p className="text-[13px] text-ink mt-3 leading-relaxed">
{detail.description}
</p>
)}
<div className="mt-4 pt-3 border-t border-regolith flex items-center justify-between text-2xs uppercase tracking-label text-carbon/60">
<div className="mt-4 pt-3 border-t border-line flex items-center justify-between text-2xs uppercase tracking-[0.10em] text-mute">
<span>{detail.bundle}</span>
<span>{detail.supportedLocales.join(', ')}</span>
{typeof detail.recordCount === 'number' && (
@@ -197,7 +197,7 @@ export function DictionaryHubView({ detail }: { detail: DictionaryDetail }) {
<div className="md:order-3 order-3 space-y-2">
<SectionLabel side="right" count={incomingDicts.length} />
{incomingDicts.length === 0 ? (
<div className="text-2xs text-carbon/40 italic">
<div className="text-2xs text-mute/70 italic">
{t('hub.noIncoming', { defaultValue: 'никто не ссылается' })}
</div>
) : (
@@ -68,19 +68,19 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
return (
<section className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-primary text-carbon">
<h3 className="text-sm font-sans text-ink">
{t('lineage.refs.title')}
</h3>
<div className="flex items-center gap-2">
{stale !== null && stale >= 2 && (
<span
className="text-2xs text-carbon/60 px-2 py-0.5 rounded-sm border border-regolith"
className="text-2xs text-mute px-2 py-0.5 rounded-sm border border-line"
title={t('lineage.refs.staleHint')}
>
{t('lineage.refs.stale', { minutes: stale })}
</span>
)}
<span className="text-2xs text-carbon/60">
<span className="text-2xs text-mute">
{t('lineage.refs.total', { count: data.total })}
</span>
</div>
@@ -91,16 +91,16 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
{data.perSource.map((s) => (
<li
key={`${s.sourceDict}.${s.sourceField}`}
className="flex items-center gap-1.5 px-2 py-1 rounded-sm border border-regolith bg-regolith/30 text-2xs"
className="flex items-center gap-1.5 px-2 py-1 rounded-sm border border-line bg-line/30 text-2xs"
>
<Link
to="/dictionaries/$name"
params={{ name: s.sourceDict }}
className="text-ultramarain hover:underline font-mono"
className="text-accent hover:underline font-mono"
>
{s.sourceDisplayName ?? s.sourceDict}
</Link>
<span className="font-mono text-carbon/70">.{s.sourceField}</span>
<span className="font-mono text-ink-2">.{s.sourceField}</span>
<Badge variant="neutral">{s.count}</Badge>
<Badge variant={onCloseVariant(s.onClose)}>
{t(`lineage.onClose.${s.onClose}`)}
@@ -111,7 +111,7 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
{/* Items list */}
{data.items.length > 0 && (
<ul className="divide-y divide-regolith border border-regolith rounded-sm">
<ul className="divide-y divide-regolith border border-line rounded-sm">
{data.items.map((r) => (
<li
key={r.id}
@@ -119,13 +119,13 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="font-mono text-carbon/70">{r.sourceDict}</span>
<span className="text-carbon/40">/</span>
<span className="font-mono text-ink-2">{r.sourceDict}</span>
<span className="text-mute/70">/</span>
<Link
to="/dictionaries/$name"
params={{ name: r.sourceDict }}
search={{ q: r.businessKey }}
className="text-ultramarain hover:underline font-mono truncate"
className="text-accent hover:underline font-mono truncate"
aria-label={t('lineage.refs.openSourceRecord', {
key: r.businessKey,
})}
@@ -135,10 +135,10 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
<ArrowSquareOutIcon
weight="bold"
size={12}
className="text-carbon/40 shrink-0"
className="text-mute/70 shrink-0"
/>
</div>
<div className="text-carbon/60 mt-0.5">
<div className="text-mute mt-0.5">
{t('lineage.refs.field', { field: r.sourceField })} ·{' '}
{t('lineage.refs.created', {
when: new Date(r.createdAt).toLocaleString(),
@@ -162,7 +162,7 @@ export const RecordDependentsPanel = ({ dictionaryName, businessKey }: Props) =>
>
{t('pagination.prev')}
</Button>
<span className="text-carbon/70">
<span className="text-ink-2">
{t('pagination.pageOf', { page: page + 1, total: totalPages })}
</span>
<Button