Skip to content

Commit 1620d5c

Browse files
authored
Merge pull request finos#1744 from aidanm3341/panel
feat(calm-hub-ui): add icons and panels to organise ui
2 parents 238b466 + d8f4650 commit 1620d5c

File tree

12 files changed

+456
-64
lines changed

12 files changed

+456
-64
lines changed

calm-hub-ui/src/hub/Hub.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,14 @@ describe('Hub', () => {
6666
it('renders JsonRenderer when data is loaded', () => {
6767
renderWithRouter(<Hub />);
6868

69-
// Initially, JsonRenderer should be present but empty
70-
expect(screen.getByTestId('json-renderer')).toBeInTheDocument();
71-
expect(screen.getByTestId('json-renderer')).toHaveTextContent('');
69+
// Initially, no content should be rendered
70+
expect(screen.queryByTestId('json-renderer')).not.toBeInTheDocument();
7271

7372
// Click the Load Test Data button to simulate data loading
7473
fireEvent.click(screen.getByText('Load Test Data'));
7574

76-
// Now JsonRenderer should show JSON content
75+
// Now JsonRenderer should be visible and show JSON content
76+
expect(screen.getByTestId('json-renderer')).toBeInTheDocument();
7777
expect(screen.getByTestId('json-renderer')).toHaveTextContent('JSON');
7878
});
7979

calm-hub-ui/src/hub/Hub.tsx

Lines changed: 20 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { useState } from 'react';
2-
import { JsonRenderer } from './components/json-renderer/JsonRenderer.js';
32
import { TreeNavigation } from './components/tree-navigation/TreeNavigation.js';
43
import { Data, Adr } from '../model/calm.js';
54
import { Navbar } from '../components/navbar/Navbar.js';
65
import { AdrRenderer } from './components/adr-renderer/AdrRenderer.js';
6+
import { DocumentDetailSection } from './components/document-detail-section/DocumentDetailSection.js';
7+
import { ArchitectureSection } from './components/architecture-section/ArchitectureSection.js';
78
import './Hub.css';
8-
import { Drawer } from '../visualizer/components/drawer/Drawer.js';
99

1010
export default function Hub() {
1111
const [data, setData] = useState<Data | undefined>();
@@ -22,54 +22,27 @@ export default function Hub() {
2222
}
2323

2424
return (
25-
<div className="flex flex-col h-screen">
25+
<div className="flex flex-col h-screen overflow-hidden">
2626
<Navbar />
27-
<div className="flex flex-row flex-1 overflow-hidden">
28-
<div className="w-1/4">
29-
<TreeNavigation onDataLoad={handleDataLoad} onAdrLoad={handleAdrLoad} />
30-
</div>
31-
{adrData ? (
32-
<AdrRenderer adrDetails={adrData} />
33-
) : (
34-
<div className="w-full h-full overflow-auto">
35-
{adrData && <AdrRenderer adrDetails={adrData} />}
36-
{data?.calmType === 'Architectures' && <ArchitectureSection data={data} />}
37-
{data?.calmType !== 'Architectures' && !adrData && (
38-
<div className="p-5 bg-base-200 h-full">
39-
<JsonRenderer json={data} />
40-
</div>
41-
)}
42-
</div>
43-
)}
44-
</div>
45-
</div>
46-
);
47-
}
48-
49-
function ArchitectureSection({ data }: { data: Data & { calmType: 'Architectures' } }) {
50-
return (
51-
<div className="relative w-full h-full flex flex-col">
52-
<div className="tabs w-full flex-1 flex flex-col">
53-
<input
54-
type="radio"
55-
name="view-tabs"
56-
className="tab absolute top-4 right-25 z-[100] backdrop-blur-sm shadow-lg rounded-lg px-4 py-2 border border-base-300 bg-base-100/90 checked:!bg-[#007dff] checked:!text-white hover:bg-base-200/90"
57-
aria-label="JSON"
58-
/>
59-
<div className="tab-content flex-1">
60-
<div className="h-full bg-base-200">
61-
<JsonRenderer json={data} />
27+
<div className="flex flex-row flex-1 overflow-hidden bg-base-300">
28+
<div className="w-1/4 p-4 pr-2">
29+
<div className="h-full bg-base-100 rounded-2xl overflow-hidden shadow-xl">
30+
<TreeNavigation onDataLoad={handleDataLoad} onAdrLoad={handleAdrLoad} />
6231
</div>
6332
</div>
64-
<input
65-
type="radio"
66-
name="view-tabs"
67-
className="tab absolute top-4 right-4 z-[100] backdrop-blur-sm shadow-lg rounded-lg px-4 py-2 border border-base-300 bg-base-100/90 checked:!bg-[#007dff] checked:!text-white hover:bg-base-200/90"
68-
aria-label="Diagram"
69-
defaultChecked
70-
/>
71-
<div className="tab-content flex-1">
72-
<Drawer data={data} />
33+
<div className="flex-1 overflow-auto">
34+
{adrData ? (
35+
<AdrRenderer adrDetails={adrData} />
36+
) : (
37+
<>
38+
{data?.calmType === 'Architectures' && (
39+
<ArchitectureSection data={data} />
40+
)}
41+
{data?.calmType !== 'Architectures' && !adrData && (
42+
<DocumentDetailSection data={data} />
43+
)}
44+
</>
45+
)}
7346
</div>
7447
</div>
7548
</div>
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { MemoryRouter } from 'react-router-dom';
3+
import userEvent from '@testing-library/user-event';
4+
import { ArchitectureSection } from './ArchitectureSection.js';
5+
import { describe, it, expect, vi } from 'vitest';
6+
import { Data } from '../../../model/calm.js';
7+
8+
vi.mock('react-router-dom', async () => {
9+
const actual = await vi.importActual('react-router-dom');
10+
return {
11+
...actual,
12+
useNavigate: vi.fn(() => vi.fn()),
13+
};
14+
});
15+
16+
vi.mock('@monaco-editor/react', () => ({
17+
Editor: ({ value }: { value: string }) => <textarea value={value} readOnly data-testid="monaco-editor" />
18+
}));
19+
20+
vi.mock('../../../visualizer/components/drawer/Drawer.js', () => ({
21+
Drawer: ({ data }: { data: Data }) => <div data-testid="drawer">Drawer for {data.id}</div>
22+
}));
23+
24+
describe('ArchitectureSection', () => {
25+
const architectureData: Data & { calmType: 'Architectures' } = {
26+
id: 'test-arch',
27+
version: '1.0.0',
28+
name: 'arch-namespace',
29+
calmType: 'Architectures',
30+
data: undefined,
31+
};
32+
33+
it('renders architecture title with namespace, id, and version', () => {
34+
render(
35+
<MemoryRouter>
36+
<ArchitectureSection data={architectureData} />
37+
</MemoryRouter>
38+
);
39+
40+
const heading = screen.getByRole('heading');
41+
expect(heading).toHaveTextContent('arch-namespace');
42+
expect(heading).toHaveTextContent('test-arch');
43+
expect(heading).toHaveTextContent('1.0.0');
44+
});
45+
46+
it('renders tabs with icons', () => {
47+
render(
48+
<MemoryRouter>
49+
<ArchitectureSection data={architectureData} />
50+
</MemoryRouter>
51+
);
52+
53+
expect(screen.getByRole('tab', { name: /diagram/i })).toBeInTheDocument();
54+
expect(screen.getByRole('tab', { name: /json/i })).toBeInTheDocument();
55+
});
56+
57+
it('shows diagram tab by default', () => {
58+
render(
59+
<MemoryRouter>
60+
<ArchitectureSection data={architectureData} />
61+
</MemoryRouter>
62+
);
63+
64+
expect(screen.getByTestId('drawer')).toBeInTheDocument();
65+
expect(screen.getByTestId('drawer')).toHaveTextContent('Drawer for test-arch');
66+
});
67+
68+
it('switches to JSON tab when clicked', async () => {
69+
const user = userEvent.setup();
70+
render(
71+
<MemoryRouter>
72+
<ArchitectureSection data={architectureData} />
73+
</MemoryRouter>
74+
);
75+
76+
const jsonTab = screen.getByRole('tab', { name: /json/i });
77+
await user.click(jsonTab);
78+
79+
expect(screen.getByTestId('monaco-editor')).toBeInTheDocument();
80+
expect(screen.queryByTestId('drawer')).not.toBeInTheDocument();
81+
});
82+
83+
it('switches back to diagram tab when clicked', async () => {
84+
const user = userEvent.setup();
85+
render(
86+
<MemoryRouter>
87+
<ArchitectureSection data={architectureData} />
88+
</MemoryRouter>
89+
);
90+
91+
const jsonTab = screen.getByRole('tab', { name: /json/i });
92+
await user.click(jsonTab);
93+
94+
expect(screen.getByTestId('monaco-editor')).toBeInTheDocument();
95+
96+
const diagramTab = screen.getByRole('tab', { name: /diagram/i });
97+
await user.click(diagramTab);
98+
99+
expect(screen.getByTestId('drawer')).toBeInTheDocument();
100+
expect(screen.queryByTestId('monaco-editor')).not.toBeInTheDocument();
101+
});
102+
103+
it('applies active styles to the selected tab', async () => {
104+
const user = userEvent.setup();
105+
render(
106+
<MemoryRouter>
107+
<ArchitectureSection data={architectureData} />
108+
</MemoryRouter>
109+
);
110+
111+
const diagramTab = screen.getByRole('tab', { name: /diagram/i });
112+
const jsonTab = screen.getByRole('tab', { name: /json/i });
113+
114+
// Diagram tab should be active by default
115+
expect(diagramTab).toHaveClass('tab-active');
116+
expect(jsonTab).not.toHaveClass('tab-active');
117+
118+
// Click JSON tab
119+
await user.click(jsonTab);
120+
121+
expect(jsonTab).toHaveClass('tab-active');
122+
expect(diagramTab).not.toHaveClass('tab-active');
123+
});
124+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useState } from 'react';
2+
import { IoConstructOutline, IoEyeOutline, IoCodeOutline } from 'react-icons/io5';
3+
import { Data } from '../../../model/calm.js';
4+
import { JsonRenderer } from '../json-renderer/JsonRenderer.js';
5+
import { Drawer } from '../../../visualizer/components/drawer/Drawer.js';
6+
import { SectionHeader } from '../section-header/SectionHeader.js';
7+
8+
interface ArchitectureSectionProps {
9+
data: Data & { calmType: 'Architectures' };
10+
}
11+
12+
export function ArchitectureSection({ data }: ArchitectureSectionProps) {
13+
const [activeTab, setActiveTab] = useState<'diagram' | 'json'>('diagram');
14+
15+
const tabs = (
16+
<div role="tablist" className="tabs tabs-boxed tabs-sm bg-base-100">
17+
<button
18+
role="tab"
19+
className={`tab gap-1 rounded-lg ${activeTab === 'diagram' ? 'tab-active !bg-accent !text-white' : ''}`}
20+
onClick={() => setActiveTab('diagram')}
21+
>
22+
<IoEyeOutline />
23+
Diagram
24+
</button>
25+
<button
26+
role="tab"
27+
className={`tab gap-1 rounded-lg ${activeTab === 'json' ? 'tab-active !bg-accent !text-white' : ''}`}
28+
onClick={() => setActiveTab('json')}
29+
>
30+
<IoCodeOutline />
31+
JSON
32+
</button>
33+
</div>
34+
);
35+
36+
return (
37+
<div className="w-full h-full py-4 pl-2 pr-4">
38+
<div className="h-full bg-base-100 rounded-2xl overflow-hidden flex flex-col shadow-xl">
39+
<SectionHeader
40+
icon={<IoConstructOutline className="text-accent" />}
41+
namespace={data.name}
42+
id={data.id}
43+
version={data.version}
44+
rightContent={tabs}
45+
/>
46+
47+
<div className="flex-1 min-h-0 overflow-hidden">
48+
{activeTab === 'diagram' ? (
49+
<div className="w-full h-full">
50+
<Drawer data={data} />
51+
</div>
52+
) : (
53+
<div className="h-full bg-base-200 overflow-auto">
54+
<JsonRenderer json={data} />
55+
</div>
56+
)}
57+
</div>
58+
</div>
59+
</div>
60+
);
61+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { MemoryRouter } from 'react-router-dom';
3+
import { DocumentDetailSection } from './DocumentDetailSection.js';
4+
import { describe, it, expect, vi } from 'vitest';
5+
import { Data } from '../../../model/calm.js';
6+
7+
vi.mock('react-router-dom', async () => {
8+
const actual = await vi.importActual('react-router-dom');
9+
return {
10+
...actual,
11+
useNavigate: vi.fn(() => vi.fn()),
12+
};
13+
});
14+
15+
vi.mock('@monaco-editor/react', () => ({
16+
Editor: ({ value }: { value: string }) => <textarea value={value} readOnly data-testid="monaco-editor" />
17+
}));
18+
19+
describe('DocumentDetailSection', () => {
20+
it('renders null when data is undefined', () => {
21+
const { container } = render(
22+
<MemoryRouter>
23+
<DocumentDetailSection data={undefined} />
24+
</MemoryRouter>
25+
);
26+
expect(container.firstChild).toBeNull();
27+
});
28+
29+
it('renders Patterns with correct icon', () => {
30+
const data: Data = {
31+
id: 'test-pattern',
32+
version: '1.0.0',
33+
name: 'my-namespace',
34+
calmType: 'Patterns',
35+
data: undefined,
36+
};
37+
38+
render(
39+
<MemoryRouter>
40+
<DocumentDetailSection data={data} />
41+
</MemoryRouter>
42+
);
43+
44+
const heading = screen.getByRole('heading');
45+
expect(heading).toHaveTextContent('my-namespace');
46+
expect(heading).toHaveTextContent('test-pattern');
47+
expect(heading).toHaveTextContent('1.0.0');
48+
});
49+
50+
it('renders Flows with correct icon', () => {
51+
const data: Data = {
52+
id: 'test-flow',
53+
version: '2.0.0',
54+
name: 'flow-namespace',
55+
calmType: 'Flows',
56+
data: undefined,
57+
};
58+
59+
render(
60+
<MemoryRouter>
61+
<DocumentDetailSection data={data} />
62+
</MemoryRouter>
63+
);
64+
65+
const heading = screen.getByRole('heading');
66+
expect(heading).toHaveTextContent('flow-namespace');
67+
expect(heading).toHaveTextContent('test-flow');
68+
expect(heading).toHaveTextContent('2.0.0');
69+
});
70+
71+
it('renders JsonRenderer with correct data', () => {
72+
const data: Data = {
73+
id: 'test-id',
74+
version: '1.0.0',
75+
name: 'test-namespace',
76+
calmType: 'Patterns',
77+
data: undefined,
78+
};
79+
80+
render(
81+
<MemoryRouter>
82+
<DocumentDetailSection data={data} />
83+
</MemoryRouter>
84+
);
85+
86+
const textarea = screen.getByTestId('monaco-editor');
87+
expect(textarea).toHaveValue(JSON.stringify(data, null, 2));
88+
});
89+
});

0 commit comments

Comments
 (0)