Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a878fb3
feat(calm-hub-ui): Deeplinks within CalmHub
aamanrebello Dec 3, 2025
cd29f9f
Merge branch 'main' into deeplink
aamanrebello Dec 3, 2025
8dfb693
Merge branch 'main' into deeplink
aamanrebello Dec 4, 2025
76c0c94
Merge branch 'main' into deeplink
rocketstack-matt Dec 8, 2025
5a805e3
Merge branch 'main' into deeplink
rocketstack-matt Dec 19, 2025
4477e08
Merge branch 'main' into deeplink
aamanrebello Dec 19, 2025
88a7bfb
Merge branch 'main' into deeplink
aamanrebello Dec 23, 2025
c8047f6
feat(calm-hub-ui): Better state management
aamanrebello Dec 23, 2025
5d15ca5
Merge branch 'main' into deeplink
aamanrebello Dec 24, 2025
b45068e
Merge branch 'main' into deeplink
aamanrebello Jan 7, 2026
0a144d8
Merge branch 'main' into deeplink
markscott-ms Jan 12, 2026
9fb4132
Merge branch 'main' into deeplink
aamanrebello Jan 19, 2026
c2f6056
Merge branch 'main' into deeplink
YoofiTT96 Jan 21, 2026
32a3c32
Merge branch 'main' into deeplink
aamanrebello Feb 5, 2026
103f97e
Merge branch 'main' into deeplink
rocketstack-matt Feb 7, 2026
6da78df
Merge branch 'main' into deeplink
markscott-ms Feb 14, 2026
9a9e743
Merge branch 'main' of https://github.com/finos/architecture-as-code …
aamanrebello Feb 26, 2026
e6820f8
Merge branch 'deeplink' of https://github.com/aamanrebello/architectu…
aamanrebello Feb 26, 2026
0f50f24
Merge branch 'main' of https://github.com/finos/architecture-as-code …
aamanrebello Mar 8, 2026
f737b39
feat(calm-hub-ui): Rewrite deeplink logic based on reviews
aamanrebello Mar 8, 2026
aeea3cb
feat(calm-hub-ui): Final cleanup
aamanrebello Mar 9, 2026
3b0b62c
Merge branch 'main' of https://github.com/finos/architecture-as-code …
aamanrebello Mar 9, 2026
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
4 changes: 4 additions & 0 deletions calm-hub-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import Hub from './hub/Hub.js';
import Visualizer from './visualizer/Visualizer.js';

function App() {
//TODO: The artifacts route will eventually need to be changed/replaced once we create a unique identifier for resources that can be used across CalmHubs.
//When this happens the logic to handle params in TreeNavigation will also have to be updated.
//Currently the format of the route allows deeplinks to only be used within a single CalmHub.
return (
<Router>
<Routes>
<Route path="/" element={<Hub />} />
<Route path="/:namespace/:type/:id/:version" element={<Hub />} />
<Route path="/visualizer" element={<Visualizer />} />
</Routes>
</Router>
Expand Down
7 changes: 5 additions & 2 deletions calm-hub-ui/src/hub/Hub.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { IoChevronForwardOutline } from 'react-icons/io5';
import { TreeNavigation } from './components/tree-navigation/TreeNavigation.js';
import { Data, Adr } from '../model/calm.js';
Expand All @@ -23,6 +23,9 @@ export default function Hub() {
setData(undefined);
}

const memoizedDataLoad = useMemo(() => handleDataLoad, []);
const memoizedAdrLoad = useMemo(() => handleAdrLoad, []);

return (
<div className="flex flex-col h-screen overflow-hidden">
<Navbar />
Expand All @@ -31,7 +34,7 @@ export default function Hub() {
<div className="h-full bg-base-100 rounded-2xl overflow-hidden shadow-xl flex flex-col">
{isSidebarOpen ? (
<div className="flex-1 min-h-0 overflow-hidden">
<TreeNavigation onDataLoad={handleDataLoad} onAdrLoad={handleAdrLoad} onCollapse={() => setIsSidebarOpen(false)} />
<TreeNavigation onDataLoad={memoizedDataLoad} onAdrLoad={memoizedAdrLoad} onCollapse={() => setIsSidebarOpen(false)} />
</div>
) : (
<div className="flex items-center justify-center pt-3">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import { TreeNavigation } from './TreeNavigation.js';
import { MemoryRouter, useParams } from 'react-router-dom';
import { fetchArchitecture, fetchArchitectureIDs, fetchArchitectureVersions, fetchFlow, fetchFlowIDs, fetchFlowVersions, fetchNamespaces, fetchPattern, fetchPatternIDs, fetchPatternVersions } from '../../../service/calm-service.js';
import { beforeEach, describe, expect, it, vi, Mock } from 'vitest';

// Mock react-router-dom
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
return {
...actual,
useParams: vi.fn().mockReturnValue({}),
useNavigate: vi.fn(),
};
});

// Mock the service functions
vi.mock('../../../service/calm-service.js', () => ({
Expand All @@ -15,12 +28,20 @@ vi.mock('../../../service/calm-service.js', () => ({
fetchArchitecture: vi.fn()
}));

let adrServiceInstance: {
fetchAdrIDs: Mock;
fetchAdrRevisions: Mock;
fetchAdr: Mock;
} | undefined;
vi.mock('../../../service/adr-service/adr-service.js', () => ({
AdrService: vi.fn().mockImplementation(() => ({
fetchAdrIDs: vi.fn().mockResolvedValue([]),
fetchAdrRevisions: vi.fn().mockResolvedValue([]),
fetchAdr: vi.fn().mockResolvedValue({})
}))
AdrService: vi.fn().mockImplementation(() => {
adrServiceInstance = {
fetchAdrIDs: vi.fn().mockResolvedValue(['201', '202']),
fetchAdrRevisions: vi.fn().mockResolvedValue(['v1.0', 'v2.0']),
fetchAdr: vi.fn().mockResolvedValue({})
};
return adrServiceInstance;
})
}));

const mockProps = {
Expand All @@ -34,15 +55,19 @@ describe('TreeNavigation', () => {
});

it('renders the tree navigation component', () => {
render(<TreeNavigation {...mockProps} />);
render(<MemoryRouter initialEntries={["/"]}>
<TreeNavigation {...mockProps} />
</MemoryRouter>);

expect(screen.getByText('Namespaces')).toBeInTheDocument();
expect(screen.getByText('test-namespace')).toBeInTheDocument();
expect(screen.getByText('another-namespace')).toBeInTheDocument();
});

it('shows resource types only when namespace is selected', () => {
render(<TreeNavigation {...mockProps} />);
render(<MemoryRouter initialEntries={["/"]}>
<TreeNavigation {...mockProps} />
</MemoryRouter>);

// Initially, resource types should not be visible since no namespace is selected
expect(screen.queryByText('Architectures')).not.toBeInTheDocument();
Expand All @@ -52,10 +77,132 @@ describe('TreeNavigation', () => {
});

it('handles initial state correctly', () => {
render(<TreeNavigation {...mockProps} />);
render(<MemoryRouter initialEntries={["/"]}>
<TreeNavigation {...mockProps} />
</MemoryRouter>);

expect(screen.getByText('Namespaces')).toBeInTheDocument();
expect(screen.getByText('test-namespace')).toBeInTheDocument();
expect(screen.getByText('another-namespace')).toBeInTheDocument();
});

it('loads data based on deeplink route - pattern', () => {
vi.mocked(useParams).mockReturnValue({
namespace: 'test-namespace',
type: 'patterns',
id: 'pattern2',
version: 'v2.0'
});

// Mock fetchPatternIDs and fetchPatternVersions to return data
vi.mocked(fetchPatternIDs).mockImplementation((ns, callback) => Promise.resolve(callback(['pattern1', 'pattern2'])));
vi.mocked(fetchPatternVersions).mockImplementation((ns, id, callback) => Promise.resolve(callback(['v1.0', 'v2.0'])));

render(<MemoryRouter initialEntries={["/"]}>
<TreeNavigation {...mockProps} />
</MemoryRouter>);

expect(fetchNamespaces).toHaveBeenCalledWith(expect.any(Function));
expect(fetchPatternIDs).toHaveBeenCalledWith('test-namespace', expect.any(Function));
expect(fetchPatternVersions).toHaveBeenCalledWith('test-namespace', 'pattern2', expect.any(Function));
expect(fetchPattern).toHaveBeenCalledWith('test-namespace', 'pattern2', 'v2.0', expect.any(Function));

// Architecture IDs should be visible
expect(screen.getByText('pattern1')).toBeInTheDocument();
expect(screen.getByText('pattern2')).toBeInTheDocument();

// Versions should be visible
expect(screen.getByText('v1.0')).toBeInTheDocument();
expect(screen.getByText('v2.0')).toBeInTheDocument();
});

it('loads data based on deeplink route - architecture', () => {
vi.mocked(useParams).mockReturnValue({
namespace: 'test-namespace',
type: 'architectures',
id: '201',
version: 'v2.0'
});

// Mock fetchArchitectureIDs and fetchArchitectureVersions to return data
vi.mocked(fetchArchitectureIDs).mockImplementation((ns, callback) => Promise.resolve(callback(['201', '202'])));
vi.mocked(fetchArchitectureVersions).mockImplementation((ns, id, callback) => Promise.resolve(callback(['v1.0', 'v2.0'])));

render(<MemoryRouter initialEntries={["/"]}>
<TreeNavigation {...mockProps} />
</MemoryRouter>);

expect(fetchNamespaces).toHaveBeenCalledWith(expect.any(Function));
expect(fetchArchitectureIDs).toHaveBeenCalledWith('test-namespace', expect.any(Function));
expect(fetchArchitectureVersions).toHaveBeenCalledWith('test-namespace', '201', expect.any(Function));
expect(fetchArchitecture).toHaveBeenCalledWith('test-namespace', '201', 'v2.0', expect.any(Function));

// Architecture IDs should be visible
expect(screen.getByText('201')).toBeInTheDocument();
expect(screen.getByText('202')).toBeInTheDocument();

// Versions should be visible
expect(screen.getByText('v1.0')).toBeInTheDocument();
expect(screen.getByText('v2.0')).toBeInTheDocument();
});

it('loads data based on deeplink route - flow', () => {
vi.mocked(useParams).mockReturnValue({
namespace: 'test-namespace',
type: 'flows',
id: '201',
version: 'v2.0'
});

// Mock fetchFlowIDs, fetchFlowVersions, and fetchFlow to return data
vi.mocked(fetchFlowIDs).mockImplementation((ns, callback) => Promise.resolve(callback(['201', '202'])));
vi.mocked(fetchFlowVersions).mockImplementation((ns, id, callback) => Promise.resolve(callback(['v1.0', 'v2.0'])));

render(<MemoryRouter initialEntries={["/"]}>
<TreeNavigation {...mockProps} />
</MemoryRouter>);

expect(fetchNamespaces).toHaveBeenCalledWith(expect.any(Function));
expect(fetchFlowIDs).toHaveBeenCalledWith('test-namespace', expect.any(Function));
expect(fetchFlowVersions).toHaveBeenCalledWith('test-namespace', '201', expect.any(Function));
expect(fetchFlow).toHaveBeenCalledWith('test-namespace', '201', 'v2.0', expect.any(Function));

// Flow IDs should be visible
expect(screen.getByText('201')).toBeInTheDocument();
expect(screen.getByText('202')).toBeInTheDocument();

// Versions should be visible
expect(screen.getByText('v1.0')).toBeInTheDocument();
expect(screen.getByText('v2.0')).toBeInTheDocument();
});

it('loads data based on deeplink route - ADR', async () => {
vi.mocked(useParams).mockReturnValue({
namespace: 'test-namespace',
type: 'adrs',
id: '201',
version: 'v2.0'
});

render(<MemoryRouter initialEntries={["/"]}>
<TreeNavigation {...mockProps} />
</MemoryRouter>);

expect(fetchNamespaces).toHaveBeenCalledWith(expect.any(Function));

// Use waitFor since AdrService methods return promises
await waitFor(() => {
expect(adrServiceInstance?.fetchAdrIDs).toHaveBeenCalledWith('test-namespace');
expect(adrServiceInstance?.fetchAdrRevisions).toHaveBeenCalledWith('test-namespace', '201');
expect(adrServiceInstance?.fetchAdr).toHaveBeenCalledWith('test-namespace', '201', 'v2.0');
});

// ADR IDs should be visible
expect(screen.getByText('201')).toBeInTheDocument();
expect(screen.getByText('202')).toBeInTheDocument();

// Versions should be visible
expect(screen.getByText('v1.0')).toBeInTheDocument();
expect(screen.getByText('v2.0')).toBeInTheDocument();
});
});
Loading
Loading