Skip to content

Commit eefab9d

Browse files
committed
feat(Settings): Add hidden runtime and testing versions to the Settings form
As part of migrating the catalog version settings from LocalStorage to the Settings infrastructure, we need to add 2 additional fields to the settings schema. Based on #3218 from @PVinaches, we're extracting the Settings components and related logic to this pull request. Considering this is a multi pull request process, the fist step is to add the fields hidden and leverage them in an upcoming PR. relates: #3230
1 parent 76feac2 commit eefab9d

15 files changed

Lines changed: 1134 additions & 26 deletions

packages/ui/src/assets/settingsSchema.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@
1111
"type": "string",
1212
"format": "uri"
1313
},
14+
"runtimeCatalogName": {
15+
"title": "Integrations runtime version",
16+
"description": "Integration runtime version. f.i. 'Camel Main 4.14.5', 'Camel Quarkus 3.15.0.redhat-00010' or 'Camel Spring Boot 4.14.5'",
17+
"default": "<empty string>",
18+
"type": "string"
19+
},
20+
"testingCatalogName": {
21+
"title": "Testing runtime version",
22+
"description": "Testing runtime version. f.i. 'Citrus 4.10.0'",
23+
"default": "<empty string>",
24+
"type": "string"
25+
},
1426
"nodeLabel": {
1527
"title": "Node label to display in canvas",
1628
"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.",
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.runtime-icon {
2+
margin-top: 2px;
3+
4+
> [class^='pf-v6-c-icon'] {
5+
display: inline-flex;
6+
align-items: center;
7+
width: 14px;
8+
height: 14px;
9+
line-height: 1;
10+
padding-top: 2px;
11+
}
12+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { render, screen } from '@testing-library/react';
2+
3+
import { getRuntimeIcon } from './RuntimeIcon';
4+
5+
describe('getRuntimeIcon', () => {
6+
it.each([
7+
['Main', 'Apache Camel logo'],
8+
['Camel Main 4.0.0', 'Apache Camel logo'],
9+
['Unknown Runtime', 'Apache Camel logo'],
10+
['Citrus', 'Citrus logo'],
11+
['Citrus 1.0.0', 'Citrus logo'],
12+
['Quarkus', 'Quarkus logo'],
13+
['Camel Quarkus 3.0.0', 'Quarkus logo'],
14+
['Spring Boot', 'Spring Boot logo'],
15+
['CamelSpringBoot3.0.0', 'Spring Boot logo'],
16+
['Camel Main 4.0.0.redhat-00001', 'Red Hat logo'],
17+
['anything-with-redhat-in-name', 'Red Hat logo'],
18+
])('returns correct icon for "%s"', (input, expectedAlt) => {
19+
render(getRuntimeIcon(input));
20+
expect(screen.getByAltText(expectedAlt)).toBeInTheDocument();
21+
});
22+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import './RuntimeIcon.scss';
2+
3+
import { Icon } from '@patternfly/react-core';
4+
5+
import camelLogo from '../../assets/camel-logo.svg';
6+
import citrusLogo from '../../assets/citrus-logo.png';
7+
import quarkusLogo from '../../assets/quarkus-logo.svg';
8+
import redhatLogo from '../../assets/redhat-logo.svg';
9+
import springBootLogo from '../../assets/springboot-logo.svg';
10+
11+
/**
12+
* Returns the appropriate icon for a given runtime or catalog name.
13+
*
14+
* Matches are case-insensitive and whitespace-agnostic for flexibility.
15+
* @param runtimeOrName - Runtime type (e.g., "Main", "Quarkus") or catalog name (e.g., "Camel Main 4.0.0.redhat-00001")
16+
* @returns Icon component with the appropriate logo
17+
*/
18+
export const getRuntimeIcon = (runtimeOrName: string = '') => {
19+
const normalized = runtimeOrName.toLowerCase().replace(/\s/g, '');
20+
21+
if (normalized.includes('redhat')) {
22+
return (
23+
<Icon className="runtime-icon">
24+
<img src={redhatLogo} alt="Red Hat logo" />
25+
</Icon>
26+
);
27+
}
28+
29+
if (normalized.includes('citrus')) {
30+
return (
31+
<Icon className="runtime-icon">
32+
<img src={citrusLogo} alt="Citrus logo" />
33+
</Icon>
34+
);
35+
}
36+
37+
if (normalized.includes('quarkus')) {
38+
return (
39+
<Icon className="runtime-icon">
40+
<img src={quarkusLogo} alt="Quarkus logo" />
41+
</Icon>
42+
);
43+
}
44+
45+
if (normalized.includes('springboot')) {
46+
return (
47+
<Icon className="runtime-icon">
48+
<img src={springBootLogo} alt="Spring Boot logo" />
49+
</Icon>
50+
);
51+
}
52+
53+
return (
54+
<Icon className="runtime-icon">
55+
<img src={camelLogo} alt="Apache Camel logo" />
56+
</Icon>
57+
);
58+
};

packages/ui/src/components/Settings/SettingsForm.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,22 @@
55
overflow: hidden auto;
66
}
77

8+
.alert-catalog-url {
9+
margin-bottom: 24px;
10+
11+
/* stylelint-disable-next-line selector-class-pattern */
12+
.pf-v6-c-alert__icon {
13+
align-content: center;
14+
}
15+
}
16+
817
/* stylelint-disable-next-line selector-class-pattern */
918
.pf-v6-c-card__footer {
1019
padding-block-start: var(--pf-v6-c-card--first-child--PaddingBlockStart);
1120
}
21+
22+
[data-testid='#.runtimeCatalogName__field-wrapper'],
23+
[data-testid='#.testingCatalogName__field-wrapper'] {
24+
display: none;
25+
}
1226
}

packages/ui/src/components/Settings/SettingsForm.test.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
1+
import { SuggestionRegistryProvider } from '@kaoto/forms';
12
import { act, fireEvent, render, screen } from '@testing-library/react';
23
import { MemoryRouter } from 'react-router-dom';
34

45
import { AbstractSettingsAdapter, DefaultSettingsAdapter } from '../../models/settings';
56
import { ReloadContext, SettingsProvider } from '../../providers';
7+
import { TestRuntimeProviderWrapper } from '../../stubs/TestRuntimeProviderWrapper';
68
import { SettingsForm } from './SettingsForm';
79

810
describe('SettingsForm', () => {
911
let reloadPage: jest.Mock;
1012
let settingsAdapter: AbstractSettingsAdapter;
13+
const { Provider: RuntimeProvider } = TestRuntimeProviderWrapper();
1114

1215
const wrapper = ({ children }: { children: React.ReactNode }) => {
1316
return (
1417
<MemoryRouter>
1518
<ReloadContext.Provider value={{ reloadPage, lastRender: 0 }}>
16-
<SettingsProvider adapter={settingsAdapter}>{children}</SettingsProvider>
19+
<RuntimeProvider>
20+
<SettingsProvider adapter={settingsAdapter}>
21+
<SuggestionRegistryProvider>{children}</SuggestionRegistryProvider>
22+
</SettingsProvider>
23+
</RuntimeProvider>
1724
</ReloadContext.Provider>
1825
</MemoryRouter>
1926
);
@@ -52,12 +59,29 @@ describe('SettingsForm', () => {
5259
expect(settingsAdapter.getSettings().catalogUrl).not.toBe('http://localhost:8080');
5360
});
5461

55-
it('should reload the page upon clicking save', () => {
56-
act(() => {
62+
it('should reload the page upon clicking save', async () => {
63+
await act(async () => {
5764
const button = screen.getByTestId('settings-form-save-btn');
5865
fireEvent.click(button);
5966
});
6067

6168
expect(reloadPage).toHaveBeenCalledTimes(1);
6269
});
70+
71+
it('should display error alert when save fails', async () => {
72+
// Mock saveSettings to throw an error
73+
const errorMessage = 'Failed to save settings to storage';
74+
settingsAdapter.saveSettings = jest.fn().mockRejectedValue(new Error(errorMessage));
75+
76+
await act(async () => {
77+
const button = screen.getByTestId('settings-form-save-btn');
78+
fireEvent.click(button);
79+
});
80+
81+
// Wait for error alert to appear
82+
const errorAlert = await screen.findByText('Failed to save settings.');
83+
expect(errorAlert).toBeInTheDocument();
84+
expect(screen.getByText(errorMessage)).toBeInTheDocument();
85+
expect(reloadPage).not.toHaveBeenCalled();
86+
});
6387
});

packages/ui/src/components/Settings/SettingsForm.tsx

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import './SettingsForm.scss';
22

33
import { CanvasFormTabsContext, CanvasFormTabsContextResult, KaotoForm } from '@kaoto/forms';
4-
import { Button, Card, CardBody, CardFooter, CardTitle } from '@patternfly/react-core';
4+
import { Alert, Button, Card, CardBody, CardFooter, CardTitle } from '@patternfly/react-core';
55
import { FunctionComponent, useContext, useMemo, useState } from 'react';
66
import { useNavigate } from 'react-router-dom';
77

@@ -22,22 +22,50 @@ export const SettingsForm: FunctionComponent = () => {
2222
const navigate = useNavigate();
2323
const { lastRender, reloadPage } = useReloadContext();
2424
const [settings, setSettings] = useState(settingsAdapter.getSettings());
25+
const [saveError, setSaveError] = useState<string | undefined>(undefined);
26+
const initialCatalogUrl = useMemo(() => settingsAdapter.getSettings().catalogUrl, [settingsAdapter]);
2527

2628
const onChangeModel = (value: unknown) => {
2729
setSettings(value as SettingsModel);
30+
setSaveError(undefined);
2831
};
2932

30-
const onSave = () => {
31-
settingsAdapter.saveSettings(settings);
32-
reloadPage();
33-
navigate(Links.Home);
33+
const hasPendingCatalogUrlChange = settings.catalogUrl !== initialCatalogUrl;
34+
35+
const onSave = async () => {
36+
try {
37+
await settingsAdapter.saveSettings(settings);
38+
reloadPage();
39+
40+
if (!hasPendingCatalogUrlChange) {
41+
navigate(Links.Home);
42+
}
43+
} catch (error) {
44+
setSaveError(error instanceof Error ? error.message : 'Unable to save settings');
45+
}
3446
};
3547

3648
return (
3749
<Card className="settings-form-card" data-last-render={lastRender}>
3850
<CardTitle>Settings</CardTitle>
3951

4052
<CardBody>
53+
{hasPendingCatalogUrlChange && (
54+
<Alert
55+
className="alert-catalog-url"
56+
isInline
57+
variant="info"
58+
title="Catalog versions will be recomputed after saving a custom catalog."
59+
>
60+
Runtime selector versions still reflect the currently saved catalog URL. Save the settings to recompute the
61+
available Camel and testing catalogs options from the new catalog URL.
62+
</Alert>
63+
)}
64+
{saveError && (
65+
<Alert className="alert-catalog-url" isInline variant="danger" title="Failed to save settings.">
66+
{saveError}
67+
</Alert>
68+
)}
4169
<CanvasFormTabsContext.Provider value={formTabsValue}>
4270
<KaotoForm
4371
data-testid="settings-form"

0 commit comments

Comments
 (0)