Skip to content

Visualizer styling updates #1373

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions brand/Horizontal/2025_CALM_Horizontal_Navbar_Logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion calm-hub-ui/src/components/navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function Navbar() {
</div>
<a className="btn btn-ghost">
<img
src="/brand/Icon/2025_CALM_Icon.svg"
src="/brand/Horizontal/2025_CALM_Horizontal_Navbar_Logo.svg"
alt="CALM Logo"
className="h-10 logo"
/>
Expand Down
108 changes: 108 additions & 0 deletions calm-hub-ui/src/visualizer/Visualizer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import Visualizer from './Visualizer.js';
import { MemoryRouter } from 'react-router-dom';
import { CalmArchitectureSchema } from '@finos/calm-shared';
import { Data } from '../model/calm.js';
import { ReactNode } from 'react';

vi.mock('./components/drawer/Drawer.js', () => ({
Drawer: ({
calmInstance,
title,
data,
}: {
calmInstance?: CalmArchitectureSchema;
title: string;
data?: Data;
}) => (
<div data-testid="drawer">
<span>{title}</span>
<span>{JSON.stringify(calmInstance)}</span>
<span>{JSON.stringify(data)}</span>
</div>
),
}));
vi.mock('../components/navbar/Navbar.js', () => ({
Navbar: () => <nav data-testid="navbar" />,
}));

const mockFileContent = JSON.stringify({ foo: 'bar' });
const mockFile = {
name: 'test.json',
type: 'application/json',
text: () => Promise.resolve(mockFileContent),
} as File;
vi.mock('./components/menu/Menu.js', () => ({
Menu: ({ handleUpload }: { handleUpload: (instanceFile: File) => void }) => (
<button data-testid="upload" onClick={() => handleUpload(mockFile)}>
Upload Architecture
</button>
),
}));

describe('Visualizer', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('renders Navbar, Menu, and Drawer', () => {
render(
<MemoryRouter>
<Visualizer />
</MemoryRouter>
);
expect(screen.getByTestId('navbar')).toBeInTheDocument();
expect(screen.getByTestId('upload')).toBeInTheDocument();
expect(screen.getByTestId('drawer')).toBeInTheDocument();
});

it('uses location.state data for title and calmInstance', () => {
const state = {
name: 'Test Title',
data: {
id: 'test-id',
version: '1.0.0',
name: 'test-name',
calmType: 'Architectures',
data: { key: 'value' },
},
};
const Wrapper = ({ children }: { children: ReactNode }) => (
<MemoryRouter initialEntries={[{ pathname: '/', state }]}>{children}</MemoryRouter>
);
render(
<Wrapper>
<Visualizer />
</Wrapper>
);
expect(screen.getByText('Test Title')).toBeInTheDocument();
expect(screen.getByText(JSON.stringify(state.data))).toBeInTheDocument();
});

it('updates title and calmInstance when file is uploaded', async () => {
render(
<MemoryRouter>
<Visualizer />
</MemoryRouter>
);
fireEvent.click(screen.getByTestId('upload'));
await waitFor(() => {
expect(screen.getByText('test.json')).toBeInTheDocument();
});
});

it('falls back to location.state if no file uploaded', () => {
const state = { name: 'Fallback Title', data: { fallback: true } };
const Wrapper = ({ children }: { children: ReactNode }) => (
<MemoryRouter initialEntries={[{ pathname: '/', state }]}>{children}</MemoryRouter>
);
render(
<Wrapper>
<Visualizer />
</Wrapper>
);
expect(screen.getByText('Fallback Title')).toBeInTheDocument();
expect(screen.getByText(JSON.stringify({ fallback: true }))).toBeInTheDocument();
});
});
22 changes: 2 additions & 20 deletions calm-hub-ui/src/visualizer/Visualizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,17 @@ import { useEffect, useMemo, useState } from 'react';
import './Visualizer.css';
import { Drawer } from './components/drawer/Drawer.js';
import { Navbar } from '../components/navbar/Navbar.js';
import React from 'react';
import { Menu } from './components/menu/Menu.js';
import { useLocation } from 'react-router-dom';
import { CalmArchitectureSchema } from '@finos/calm-shared/src/types/core-types.js';

function Visualizer() {
const [title, setTitle] = useState<string>('');
const [instance, setCALMInstance] = useState<CalmArchitectureSchema | undefined>(undefined);
const [isConDescActive, setConDescActive] = React.useState(true);
const [isNodeDescActive, setNodeDescActive] = React.useState(true);
const location = useLocation();
const data = useMemo(() => location.state || {}, [location.state]);
const [fileInstance, setFileInstance] = useState<string | undefined>(undefined);
const [fileTitle, setFileTitle] = useState<string | undefined>(undefined);
const toggleState = (setter: React.Dispatch<React.SetStateAction<boolean>>) => () =>
setter((prev) => !prev);

async function handleFile(instanceFile: File) {
setFileTitle(instanceFile.name);
Expand All @@ -33,21 +28,8 @@ function Visualizer() {
return (
<div className="h-screen flex flex-col">
<Navbar />
<Menu
handleUpload={handleFile}
isGraphRendered={!!instance}
toggleNodeDesc={toggleState(setNodeDescActive)}
toggleConnectionDesc={toggleState(setConDescActive)}
isNodeDescActive={isNodeDescActive}
isConDescActive={isConDescActive}
/>
<Drawer
isNodeDescActive={isNodeDescActive}
isConDescActive={isConDescActive}
calmInstance={instance}
title={title}
data={data}
/>
<Menu handleUpload={handleFile} />
<Drawer calmInstance={instance} title={title} data={data} />
</div>
);
}
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lovely refactor

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs tests

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export interface CytoscapeControlPanelProps {
title: string;
isNodeDescActive: boolean;
isRelationshipDescActive: boolean;
toggleConnectionDesc: () => void;
toggleNodeDesc: () => void;
}
export function CytoscapeControlPanel({
title,
isNodeDescActive,
isRelationshipDescActive,
toggleConnectionDesc,
toggleNodeDesc,
}: CytoscapeControlPanelProps) {
return (
<div className="graph-title absolute m-5 btn-outline btn-primary shadow-md p-4">
<div className="mb-4">
<span className="text-lg font-thin text-primary">Architecture: </span>
<span className="text-lg font-semibold text-primary">{title}</span>
</div>
<hr className="my-4 border-base-300" />
<div>
<div className="text-sm font-semibold mb-2 text-primary">Display Settings</div>
<div className="flex flex-col gap-2 text-sm">
<label className="label cursor-pointer">
<input
type="checkbox"
className="checkbox"
name="connection-description"
aria-label="connection-description"
checked={isRelationshipDescActive}
onChange={toggleConnectionDesc}
/>
<span className="label-text text-base-content ml-2">
Relationship Descriptions
</span>
</label>
<label className="label cursor-pointer">
<input
className="checkbox"
type="checkbox"
aria-label="node-description"
checked={isNodeDescActive}
onChange={toggleNodeDesc}
/>
<span className="label-text text-base-content ml-2">Node Descriptions</span>
</label>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render } from '@testing-library/react';
import { CytoscapeRenderer, CytoscapeRendererProps } from './CytoscapeRenderer.js';
import cytoscape from 'cytoscape';
import * as nodePositionService from '../../services/node-position-service.js';
import * as nodePositionService from '../../../services/node-position-service.js';

const mocks = vi.hoisted(() => ({
nodes: vi.fn(),
Expand Down Expand Up @@ -31,11 +31,11 @@ vi.mock('cytoscape', () => {
});

// Mock services
vi.mock('../../services/node-position-service.js', () => ({
vi.mock('../../../services/node-position-service.js', () => ({
loadStoredNodePositions: vi.fn(() => undefined),
saveNodePositions: vi.fn(),
}));
vi.mock('../../services/layout-correction-service.js', () => ({
vi.mock('../../../services/layout-correction-service.js', () => ({
LayoutCorrectionService: vi.fn().mockImplementation(() => ({
calculateAndUpdateNodePositions: vi.fn(),
})),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import './cytoscape.css';
import { useEffect, useRef } from 'react';
import cytoscape, { EdgeSingular, NodeSingular } from 'cytoscape';
import { CytoscapeNode, Edge } from '../../contracts/contracts.js';
import { LayoutCorrectionService } from '../../services/layout-correction-service.js';
import { CytoscapeNode, Edge } from '../../../contracts/contracts.js';
import { LayoutCorrectionService } from '../../../services/layout-correction-service.js';
import {
saveNodePositions,
loadStoredNodePositions,
} from '../../services/node-position-service.js';
} from '../../../services/node-position-service.js';

// Layout configuration
const breadthFirstLayout = {
Expand Down Expand Up @@ -69,9 +69,15 @@ function getNodeStyle(showDescription: boolean): cytoscape.Css.Node {

const layoutCorrectionService = new LayoutCorrectionService();

const accentLightColor = getComputedStyle(document.documentElement).getPropertyValue('--color-accent-light').trim();
const accentColor = getComputedStyle(document.documentElement).getPropertyValue('--color-accent').trim();
const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--color-primary').trim();
const accentLightColor = getComputedStyle(document.documentElement)
.getPropertyValue('--color-accent-light')
.trim();
const accentColor = getComputedStyle(document.documentElement)
.getPropertyValue('--color-accent')
.trim();
const primaryColor = getComputedStyle(document.documentElement)
.getPropertyValue('--color-primary')
.trim();

export function CytoscapeRenderer({
nodes = [],
Expand Down Expand Up @@ -106,14 +112,14 @@ export function CytoscapeRenderer({
selector: 'node:selected',
style: {
backgroundColor: accentLightColor,
}
},
},
{
selector: 'edge:selected',
style: {
'line-color': primaryColor,
'target-arrow-color': primaryColor,
}
},
},
{
selector: ':parent',
Expand Down
18 changes: 2 additions & 16 deletions calm-hub-ui/src/visualizer/components/drawer/Drawer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,12 @@ import { Drawer } from './Drawer.js';

describe('Drawer', () => {
it('should render Drawer', () => {
render(
<Drawer
calmInstance={undefined}
title={undefined}
isConDescActive={true}
isNodeDescActive={true}
/>
);
render(<Drawer calmInstance={undefined} title={'No file selected'} />);
expect(screen.getByText('No file selected')).toBeInTheDocument();
});

it('should render Drawer', () => {
render(
<Drawer
calmInstance={undefined}
title={undefined}
isConDescActive={false}
isNodeDescActive={false}
/>
);
render(<Drawer calmInstance={undefined} title={'No file selected'} />);
expect(screen.getByText('No file selected')).toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: 'drawer-toggle' })).not.toBeChecked();
});
Expand Down
12 changes: 1 addition & 11 deletions calm-hub-ui/src/visualizer/components/drawer/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import { Data } from '../../../model/calm.js';
interface DrawerProps {
calmInstance?: CalmArchitectureSchema;
title: string;
isNodeDescActive: boolean;
isConDescActive: boolean;
data?: Data;
}

Expand Down Expand Up @@ -94,13 +92,7 @@ function getDeployedInRelationships(calmInstance: CalmArchitectureSchema) {
return deployedInRelationships;
}

export function Drawer({
calmInstance,
title,
isConDescActive,
isNodeDescActive,
data,
}: DrawerProps) {
export function Drawer({ calmInstance, title, data }: DrawerProps) {
const [selectedNode, setSelectedNode] = useState<CytoscapeNode | null>(null);

function closeSidebar() {
Expand Down Expand Up @@ -216,8 +208,6 @@ export function Drawer({
<div className="drawer-content">
{calmInstance ? (
<VisualizerContainer
isRelationshipDescActive={isConDescActive}
isNodeDescActive={isNodeDescActive}
title={title}
nodes={nodes}
edges={edges}
Expand Down
Loading
Loading