Skip to content
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
17 changes: 10 additions & 7 deletions packages/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,25 @@ import { setColorScheme } from './utils/color-scheme';
function App() {
const controller = useMemo(() => ControllerService.createController(), []);
const settingsAdapter = new LocalStorageSettingsAdapter();
const settings = settingsAdapter.getSettings();
let catalogUrl = CatalogSchemaLoader.DEFAULT_CATALOG_PATH;
const settingsCatalogUrl = settingsAdapter.getSettings().catalogUrl;
const colorSchema = settingsAdapter.getSettings().colorScheme;

if (isDefined(settingsCatalogUrl) && settingsCatalogUrl !== '') {
catalogUrl = settingsCatalogUrl;
if (isDefined(settings.catalogUrl) && settings.catalogUrl !== '') {
catalogUrl = settings.catalogUrl;
}

useLayoutEffect(() => {
setColorScheme(colorSchema);
}, [colorSchema]);
setColorScheme(settings.colorScheme);
}, [settings.colorScheme]);

return (
<SettingsProvider adapter={settingsAdapter}>
<SourceCodeLocalStorageProvider>
<RuntimeProvider catalogUrl={catalogUrl}>
<RuntimeProvider
catalogUrl={catalogUrl}
camelCatalog={settings.camelCatalog}
citrusCatalog={settings.citrusCatalog}
>
<SchemasLoaderProvider>
<CatalogLoaderProvider>
<EntitiesProvider>
Expand Down
40 changes: 40 additions & 0 deletions packages/ui/src/assets/settingsSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,46 @@
"type": "string",
"format": "uri"
},
"camelCatalog": {
"title": "Camel Catalog",
"description": "Default catalog for Camel integrations (Routes, Pipes, Kamelets). Supports Main, Quarkus, and Spring Boot runtimes.",
"type": "object",
"default": {
"version": "",
"runtime": "Main"
},
"properties": {
"version": {
"type": "string",
"description": "Catalog version (e.g., '4.14.5'). Empty string means use default from catalog library."
},
"runtime": {
"type": "string",
"description": "Runtime type: 'Main', 'Quarkus', or 'Spring Boot'"
}
},
"required": ["version", "runtime"]
},
"citrusCatalog": {
"title": "Test Catalog",
"description": "Default catalog for test integrations.",
"type": "object",
"default": {
"version": "",
"runtime": "Citrus"
},
"properties": {
"version": {
"type": "string",
"description": "Catalog version (e.g., '4.10.1'). Empty string means use default from catalog library."
},
"runtime": {
"type": "string",
"description": "Runtime type: 'Citrus'"
}
},
"required": ["version", "runtime"]
},
"nodeLabel": {
"title": "Node label to display in canvas",
"description": "Node label, which will be used for nodes in the canvas. Can be either `description` or `id`. If `description` is selected, it will be displayed only if it is available, otherwise `id` will be displayed by default.",
Expand Down
12 changes: 12 additions & 0 deletions packages/ui/src/components/Icons/RuntimeIcon.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.runtime-icon {
margin-top: 2px;

> [class^='pf-v6-c-icon'] {
display: inline-flex;
align-items: center;
width: 14px;
height: 14px;
line-height: 1;
padding-top: 2px;
}
}
51 changes: 51 additions & 0 deletions packages/ui/src/components/Icons/RuntimeIcon.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { render } from '@testing-library/react';

import { getRuntimeIcon } from './RuntimeIcon';

describe('getRuntimeIcon', () => {
it.each([
['Main', 'Apache Camel logo'],
['Camel Main 4.0.0', 'Apache Camel logo'],
['Unknown Runtime', 'Apache Camel logo'],
])('returns Camel icon for %s', (input, expectedAlt) => {
const { container } = render(getRuntimeIcon(input));
const img = container.querySelector('img');
expect(img?.alt).toBe(expectedAlt);
});

it.each([
['Citrus', 'Citrus logo'],
['Citrus 1.0.0', 'Citrus logo'],
])('returns Citrus icon for %s', (input, expectedAlt) => {
const { container } = render(getRuntimeIcon(input));
const img = container.querySelector('img');
expect(img?.alt).toBe(expectedAlt);
});

it.each([
['Quarkus', 'Quarkus logo'],
['Camel Quarkus 3.0.0', 'Quarkus logo'],
])('returns Quarkus icon for %s', (input, expectedAlt) => {
const { container } = render(getRuntimeIcon(input));
const img = container.querySelector('img');
expect(img?.alt).toBe(expectedAlt);
});

it.each([
['Spring Boot', 'Spring Boot logo'],
['CamelSpringBoot3.0.0', 'Spring Boot logo'],
])('returns Spring Boot icon for %s', (input, expectedAlt) => {
const { container } = render(getRuntimeIcon(input));
const img = container.querySelector('img');
expect(img?.alt).toBe(expectedAlt);
});

it.each([
['Camel Main 4.0.0.redhat-00001', 'Red Hat logo'],
['anything-with-redhat-in-name', 'Red Hat logo'],
])('returns Red Hat icon for %s', (input, expectedAlt) => {
const { container } = render(getRuntimeIcon(input));
const img = container.querySelector('img');
expect(img?.alt).toBe(expectedAlt);
});
});
55 changes: 55 additions & 0 deletions packages/ui/src/components/Icons/RuntimeIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import './RuntimeIcon.scss';

import { Icon } from '@patternfly/react-core';

import camelLogo from '../../assets/camel-logo.svg';
import citrusLogo from '../../assets/citrus-logo.png';
import quarkusLogo from '../../assets/quarkus-logo.svg';
import redhatLogo from '../../assets/redhat-logo.svg';
import springBootLogo from '../../assets/springboot-logo.svg';

/**
* Returns the appropriate icon for a given runtime or catalog name.
*
* @param runtimeOrName - Runtime type (e.g., "Main", "Quarkus") or catalog name (e.g., "Camel Main 4.0.0.redhat-00001")
* @returns Icon component with the appropriate logo
*/
export const getRuntimeIcon = (runtimeOrName: string) => {
if (runtimeOrName.includes('redhat')) {
return (
<Icon className="runtime-icon">
<img src={redhatLogo} alt="Red Hat logo" />
</Icon>
);
}

if (runtimeOrName === 'Citrus' || runtimeOrName.includes('Citrus')) {
return (
<Icon className="runtime-icon">
<img src={citrusLogo} alt="Citrus logo" />
</Icon>
);
}

if (runtimeOrName === 'Quarkus' || runtimeOrName.includes('Quarkus')) {
return (
<Icon className="runtime-icon">
<img src={quarkusLogo} alt="Quarkus logo" />
</Icon>
);
}

if (runtimeOrName === 'Spring Boot' || runtimeOrName.replace(/\s/g, '').includes('SpringBoot')) {
return (
<Icon className="runtime-icon">
<img src={springBootLogo} alt="Spring Boot logo" />
</Icon>
);
}

return (
<Icon className="runtime-icon">
<img src={camelLogo} alt="Apache Camel logo" />
</Icon>
);
};
97 changes: 65 additions & 32 deletions packages/ui/src/components/Settings/SettingsForm.test.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,95 @@
import { act, fireEvent, render, screen } from '@testing-library/react';
import { CatalogLibrary, CatalogLibraryEntry } from '@kaoto/camel-catalog/types';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';

import { AbstractSettingsAdapter, DefaultSettingsAdapter } from '../../models/settings';
import { ReloadContext, SettingsProvider } from '../../providers';
import { ReloadContext, RuntimeProvider, SettingsProvider } from '../../providers';
import { CatalogSchemaLoader } from '../../utils/catalog-schema-loader';
import { SettingsForm } from './SettingsForm';

describe('SettingsForm', () => {
let reloadPage: jest.Mock;
let settingsAdapter: AbstractSettingsAdapter;
const mockCatalogLibrary: CatalogLibrary = {
definitions: [
{ name: 'Camel Main 4.18.0', version: '4.18.0', runtime: 'Main', catalogs: {} },
{ name: 'Citrus 4.10.1', version: '4.10.1', runtime: 'Citrus', catalogs: {} },
] as unknown as CatalogLibraryEntry[],
version: 0,
name: 'test-catalog-library',
};

const renderSettingsForm = async () => {
const reloadPage = jest.fn();
const settingsAdapter: AbstractSettingsAdapter = new DefaultSettingsAdapter();

jest.spyOn(globalThis, 'fetch').mockResolvedValue({
json: async () => mockCatalogLibrary,
url: `http://localhost/${CatalogSchemaLoader.DEFAULT_CATALOG_PATH}`,
} as unknown as Response);

Check warning on line 28 in packages/ui/src/components/Settings/SettingsForm.test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since the receiver accepts the original type of the expression.

See more on https://sonarcloud.io/project/issues?id=KaotoIO_kaoto&issues=AZ4sfhUXI5YX8Z4FQch7&open=AZ4sfhUXI5YX8Z4FQch7&pullRequest=3218

const wrapper = ({ children }: { children: React.ReactNode }) => {
return (
const wrapper = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter>
<ReloadContext.Provider value={{ reloadPage, lastRender: 0 }}>
<SettingsProvider adapter={settingsAdapter}>{children}</SettingsProvider>
<SettingsProvider adapter={settingsAdapter}>
<RuntimeProvider
catalogUrl={CatalogSchemaLoader.DEFAULT_CATALOG_PATH}
camelCatalog={{ version: '4.18.0', runtime: 'Main' }}
citrusCatalog={{ version: '4.10.1', runtime: 'Citrus' }}
>
{children}
</RuntimeProvider>
</SettingsProvider>
</ReloadContext.Provider>
</MemoryRouter>
);
};

beforeEach(() => {
reloadPage = jest.fn();
settingsAdapter = new DefaultSettingsAdapter();
render(<SettingsForm />, { wrapper });
await screen.findByTestId('settings-form');

return {
reloadPage,
settingsAdapter,
user: userEvent.setup(),
};
};

afterEach(() => {
jest.clearAllMocks();
});

it('should render', () => {
expect(screen.getByTestId('settings-form')).toMatchSnapshot();
it('should render the settings form', async () => {
await renderSettingsForm();

expect(screen.getByTestId('settings-form')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();
});

it('should update settings upon clicking save', () => {
act(() => {
const input = screen.getByLabelText('Camel Catalog URL');
fireEvent.change(input, { target: { value: 'http://localhost:8080' } });
});
it('should update settings upon clicking save', async () => {
const { settingsAdapter, user } = await renderSettingsForm();

const input = screen.getByLabelText('Camel Catalog URL');
await user.clear(input);
await user.type(input, 'http://localhost:8080');

act(() => {
const button = screen.getByTestId('settings-form-save-btn');
fireEvent.click(button);
});
await user.click(screen.getByRole('button', { name: 'Save' }));

expect(settingsAdapter.getSettings().catalogUrl).toBe('http://localhost:8080');
});

it('should not update settings if the save button was not clicked', () => {
act(() => {
const input = screen.getByLabelText('Camel Catalog URL');
fireEvent.change(input, { target: { value: 'http://localhost:8080' } });
});
it('should not update settings if the save button was not clicked', async () => {
const { settingsAdapter, user } = await renderSettingsForm();

const input = screen.getByLabelText('Camel Catalog URL');
await user.clear(input);
await user.type(input, 'http://localhost:8080');

expect(settingsAdapter.getSettings().catalogUrl).not.toBe('http://localhost:8080');
});

it('should reload the page upon clicking save', () => {
act(() => {
const button = screen.getByTestId('settings-form-save-btn');
fireEvent.click(button);
});
it('should reload the page upon clicking save', async () => {
const { reloadPage, user } = await renderSettingsForm();

await user.click(screen.getByRole('button', { name: 'Save' }));

expect(reloadPage).toHaveBeenCalledTimes(1);
});
Expand Down
Loading
Loading