Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Topology navigation: the sidebar's Topology entry expands to Request flow, Infra layout and Service map, and opens automatically on any topology page, so the infrastructure layout graph and service map no longer require the small in-page buttons.
- User guide: `docs-site/` is published as this repository's GitHub Pages site (deployed automatically on pushes to `dev`, or manually via workflow dispatch), retargeted from the previous personal-domain deployment — site URL, navigation/landing GitHub links, robots.txt sitemap and alert runbook cross-references now point at this repository, and the previous dashboard link and personal analytics tracker are removed.

### Fixed
Expand Down Expand Up @@ -682,6 +683,7 @@ First release of the **v2 line** (versioned independently from the v1 1.x line,

### Added

- 토폴로지 내비게이션: 사이드바의 토폴로지 항목을 펼치면 요청 흐름·인프라 배치·서비스 맵으로 바로 이동할 수 있고, 토폴로지 화면에 있으면 자동으로 펼쳐집니다. 인프라 배치 그래프와 서비스 맵을 찾기 위해 화면 안의 작은 버튼을 쓰지 않아도 됩니다.
- 사용자 가이드: `docs-site/`를 이 저장소의 GitHub Pages 사이트로 게시합니다(`dev` 브랜치 push 시 자동 배포, 또는 워크플로 수동 실행). 이전 개인 도메인 배포 대상을 이 저장소 기준으로 재조정하여 사이트 URL, 내비게이션·랜딩 페이지의 GitHub 링크, robots.txt sitemap, 알림 런북 상호 참조가 이 저장소를 가리키며, 기존 대시보드 링크와 개인 애널리틱스 트래커는 제거합니다.

### 수정
Expand Down
2 changes: 2 additions & 0 deletions web/components/shell/MobileTopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const ROUTE_TKEY: Record<string, string> = {
'/cost': 'nav.cost',
'/bedrock': 'nav.bedrock',
'/topology': 'nav.topology',
'/topology/infra': 'nav.topologyInfra',
'/topology/services': 'nav.topologyServices',
'/security': 'nav.security',
'/compliance': 'nav.compliance',
'/customization': 'nav.customAgents',
Expand Down
27 changes: 27 additions & 0 deletions web/components/shell/Sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,30 @@ describe('collapsible inventory groups', () => {
expect(fixed).toContain("'/integrations'");
});
});

// Topology sub-pages were reachable only from small in-page buttons; the sidebar now
// lists them under a collapsible Topology entry.
describe('topology sub-navigation', () => {
const src = read('./Sidebar.tsx');
const fixed = src.slice(src.indexOf('const FIXED'), src.indexOf('];', src.indexOf('const FIXED')));
it('lists request flow, infra layout and service map under Topology', () => {
for (const href of ["'/topology/infra'", "'/topology/services'"]) expect(fixed).toContain(href);
for (const key of ['nav.topologyFlow', 'nav.topologyInfra', 'nav.topologyServices']) expect(fixed).toContain(key);
});
it('renders FIXED children as a collapsible, instance-scoped panel that opens on topology paths', () => {
expect(src).toContain('item.children ? renderFixedGroup(item)');
expect(src).toContain('${uid}-fixed-');
expect(src).toMatch(/item\.children && underPath\(path, item\.href\)/);
});
it('marks the header active when the panel is collapsed on a child page', () => {
// Collapsed panels unmount their children, so the header must carry aria-current.
expect(src).toContain('underPath(path, item.href) && (!open || !childActive)');
});
it('defines localized sub-nav labels', async () => {
const { translate } = await import('@/lib/i18n');
for (const key of ['nav.topologyFlow', 'nav.topologyInfra', 'nav.topologyServices']) {
expect(translate('en', key)).not.toBe(key);
expect(translate('ko', key)).not.toBe(translate('en', key));
}
});
});
80 changes: 68 additions & 12 deletions web/components/shell/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,24 @@ import ThemeToggle from '@/components/shell/ThemeToggle';
import ScopeSelector from '@/components/shell/ScopeSelector';
import { cn } from '@/lib/cn';

// Fixed top-level pages. `tkey` resolves the label via i18n.
const FIXED: { href: string; tkey: string; icon: LucideIcon }[] = [
type FixedLink = { href: string; tkey: string; icon: LucideIcon };
type FixedItem = FixedLink & { children?: FixedLink[] }; // one level only — the renderer does not nest

// Fixed top-level pages. `tkey` resolves the label via i18n. `children` renders the
// entry as a collapsible group so sub-pages are reachable from the sidebar.
const FIXED: FixedItem[] = [
{ href: '/', tkey: 'nav.overview', icon: LayoutDashboard },
{ href: '/ai-diagnosis', tkey: 'nav.aiDiagnosis', icon: Stethoscope },
{ href: '/assistant', tkey: 'nav.assistant', icon: MessagesSquare },
{ href: '/jobs', tkey: 'nav.jobs', icon: Activity },
{ href: '/cost', tkey: 'nav.cost', icon: DollarSign },
{ href: '/bedrock', tkey: 'nav.bedrock', icon: Gauge },
{ href: '/agentcore', tkey: 'nav.agentcore', icon: Cpu },
{ href: '/topology', tkey: 'nav.topology', icon: Network },
{ href: '/topology', tkey: 'nav.topology', icon: Network, children: [
{ href: '/topology', tkey: 'nav.topologyFlow', icon: Route },
{ href: '/topology/infra', tkey: 'nav.topologyInfra', icon: Layers },
{ href: '/topology/services', tkey: 'nav.topologyServices', icon: Boxes },
] },
{ href: '/security', tkey: 'nav.security', icon: Shield },
{ href: '/compliance', tkey: 'nav.compliance', icon: FileSearch },
{ href: '/integrations', tkey: 'nav.integrations', icon: Cable },
Expand Down Expand Up @@ -94,6 +102,8 @@ const FEATURE_ICON: Record<string, LucideIcon> = {
const STORAGE_KEY = 'awsops:nav:expanded';
const gId = (slug: string) => `g:${slug}`;
const sId = (key: string) => `s:${key}`;
const fId = (href: string) => `f:${href}`;
const underPath = (path: string, href: string) => path === href || path.startsWith(`${href}/`);

// Seed expand state from the active path (pure, identical on server + client → no
// hydration mismatch). localStorage is merged in only after mount.
Expand All @@ -104,6 +114,7 @@ function seedFromPath(path: string): Set<string> {
s.add(gId(active.slug));
if (active.subgroupKey) s.add(sId(active.subgroupKey));
}
for (const item of FIXED) if (item.children && underPath(path, item.href)) s.add(fId(item.href));
return s;
}

Expand Down Expand Up @@ -168,14 +179,9 @@ export default function Sidebar({ onNavigate, className, persist = true }: { onN
// Navigating into a group (or its subgroup) re-seeds it open — manual collapse
// persists until the next navigation into that group.
useEffect(() => {
const active = groupForPath(path);
if (!active) return;
setExpanded((prev) => {
const next = new Set(prev);
next.add(gId(active.slug));
if (active.subgroupKey) next.add(sId(active.subgroupKey));
return next;
});
const seed = seedFromPath(path);
if (!seed.size) return;
setExpanded((prev) => new Set([...prev, ...seed]));
}, [path]);

const toggle = (id: string) =>
Expand Down Expand Up @@ -203,6 +209,56 @@ export default function Sidebar({ onNavigate, className, persist = true }: { onN
/>
);

// A FIXED entry with children: header link + chevron toggle + child panel. A child is
// active on an exact match; the header is active on other sub-pages (e.g. a
// resource's relationship graph) and whenever the panel is collapsed, so the
// location is never unmarked.
function renderFixedGroup(item: FixedItem) {
const children = item.children!;
const label = t(item.tkey);
const open = expanded.has(fId(item.href));
const panelId = `${uid}-fixed-${item.href.replace(/\W+/g, '-')}`;
const childActive = children.some((c) => path === c.href);
const headerActive = underPath(path, item.href) && (!open || !childActive);
const Icon = item.icon;
return (
<div key={item.href} className="space-y-0.5">
<div className="flex items-center gap-0.5">
<Link
href={item.href}
onClick={() => { setExpanded((p) => new Set(p).add(fId(item.href))); onNavigate?.(); }}
aria-current={headerActive ? 'page' : undefined}
className={cn(
'flex min-w-0 flex-1 items-center gap-2.5 rounded-md px-2.5 py-[7px] text-[13px] font-medium no-underline transition-colors duration-[120ms]',
headerActive ? 'bg-chrome-active text-chrome-active-fg shadow-sm' : 'text-chrome-fg-muted hover:bg-chrome-active/40 hover:text-chrome-fg',
)}
>
<Icon size={16} strokeWidth={1.7} className={cn('shrink-0', headerActive ? 'text-chrome-active-fg' : 'text-chrome-fg-muted')} />
<span className="truncate">{label}</span>
</Link>
<button
type="button"
onClick={() => toggle(fId(item.href))}
aria-expanded={open}
aria-controls={open ? panelId : undefined}
aria-label={`${open ? t('sidebar.collapse') : t('sidebar.expand')} ${label}`}
className="shrink-0 rounded-md p-1.5 text-chrome-fg-muted transition-colors hover:bg-chrome-active/40 hover:text-chrome-fg"
>
<ChevronRight size={15} strokeWidth={2} className={cn('transition-transform duration-150', open && 'rotate-90')} />
</button>
</div>
{open && (
<div id={panelId} className="space-y-0.5 pl-2">
{children.map((c) => (
<NavItem key={c.href} href={c.href} label={t(c.tkey)} icon={c.icon}
active={path === c.href} onNavigate={onNavigate} />
))}
</div>
)}
</div>
);
}

function renderGroup(g: NavGroupNode) {
const label = t(g.labelKey);

Expand Down Expand Up @@ -309,7 +365,7 @@ export default function Sidebar({ onNavigate, className, persist = true }: { onN
{/* Nav */}
<nav className="flex-1 space-y-4">
<div className="space-y-0.5">
{FIXED.map((item) => (
{FIXED.map((item) => item.children ? renderFixedGroup(item) : (
<NavItem
key={item.href}
href={item.href}
Expand Down
12 changes: 12 additions & 0 deletions web/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ const MESSAGES: Record<Lang, Dict> = {
'nav.bedrock': 'Bedrock',
'nav.agentcore': 'AgentCore',
'nav.topology': '토폴로지',
'nav.topologyFlow': '요청 흐름',
'nav.topologyInfra': '인프라 배치',
'nav.topologyServices': '서비스 맵',
'nav.security': '보안',
'nav.compliance': '컴플라이언스',
'nav.customAgents': '커스텀 에이전트',
Expand Down Expand Up @@ -167,6 +170,9 @@ const MESSAGES: Record<Lang, Dict> = {
'nav.bedrock': 'Bedrock',
'nav.agentcore': 'AgentCore',
'nav.topology': 'Topology',
'nav.topologyFlow': 'Request flow',
'nav.topologyInfra': 'Infra layout',
'nav.topologyServices': 'Service map',
'nav.security': 'Security',
'nav.compliance': 'Compliance',
'nav.customAgents': 'Custom Agents',
Expand Down Expand Up @@ -281,6 +287,9 @@ const MESSAGES: Record<Lang, Dict> = {
'nav.bedrock': 'Bedrock',
'nav.agentcore': 'AgentCore',
'nav.topology': '拓扑',
'nav.topologyFlow': '请求流程',
'nav.topologyInfra': '基础设施布局',
'nav.topologyServices': '服务地图',
'nav.security': '安全',
'nav.compliance': '合规',
'nav.customAgents': '自定义代理',
Expand Down Expand Up @@ -393,6 +402,9 @@ const MESSAGES: Record<Lang, Dict> = {
'nav.bedrock': 'Bedrock',
'nav.agentcore': 'AgentCore',
'nav.topology': 'トポロジー',
'nav.topologyFlow': 'リクエストフロー',
'nav.topologyInfra': 'インフラ配置',
'nav.topologyServices': 'サービスマップ',
'nav.security': 'セキュリティ',
'nav.compliance': 'コンプライアンス',
'nav.customAgents': 'カスタムエージェント',
Expand Down
Loading