Skip to content

Commit bcf9663

Browse files
committed
fix(Catalog): Select appropriate Catalog for Resource
Pick the catalog based on the resource's schema type so opening a Citrus test no longer loads a Camel catalog (and vice versa). - Add `detectSchemaType()` to infer the schema from source/path without constructing a full `KaotoResource`, avoiding a chicken-and-egg problem at provider mount time. - Add `COMPATIBLE_RUNTIMES_BY_SCHEMA_TYPE` as the single source of truth for resource→runtime compatibility, replacing the per-resource `getCompatibleRuntimes()` methods. - Persist the selected catalog per schema type via `catalog-storage.ts` (keyed map) instead of one global entry, with legacy-format detection to discard old values. - Simplify `IntegrationTypeSelector`, `RuntimeSelector`, and `NewFlow` now that runtime filtering lives in one place.
1 parent 1b30aa1 commit bcf9663

27 files changed

Lines changed: 685 additions & 374 deletions

packages/ui/src/components/Visualization/ContextToolbar/IntegrationTypeSelector/ChangeIntegrationTypeModal/ChangeIntegrationTypeModal.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { FunctionComponent } from 'react';
33

44
interface ChangeIntegrationTypeModalProps {
55
isOpen: boolean;
6-
changesCatalog?: boolean;
76
onConfirm: () => void;
87
onCancel: () => void;
98
}
@@ -14,8 +13,7 @@ export const ChangeIntegrationTypeModal: FunctionComponent<ChangeIntegrationType
1413
<ModalHeader title="Warning" titleIconVariant="warning" />
1514
<ModalBody>
1615
<p data-testid={'confirmation-modal-text'}>
17-
Changing the source type will remove any existing integration and you will lose your current work.{' '}
18-
{props.changesCatalog && 'This will also change the current selected catalog.'}
16+
Changing the source type will remove any existing integration and you will lose your current work.
1917
</p>
2018
<p>Are you sure you would like to proceed?</p>
2119
</ModalBody>

packages/ui/src/components/Visualization/ContextToolbar/IntegrationTypeSelector/IntegrationTypeSelector.test.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ describe('IntegrationTypeSelector.tsx', () => {
5555
camelResource?: KaotoResource,
5656
) => {
5757
const mockSetSelectedCatalog = jest.fn();
58+
const mockSetCodeAndNotify = jest.fn();
5859
const { Provider } = TestProvidersWrapper({ camelResource });
5960

6061
return {
@@ -67,14 +68,15 @@ describe('IntegrationTypeSelector.tsx', () => {
6768
setSelectedCatalog: mockSetSelectedCatalog,
6869
}}
6970
>
70-
<SourceCodeApiContext.Provider value={{ setCodeAndNotify: jest.fn() }}>
71+
<SourceCodeApiContext.Provider value={{ setCodeAndNotify: mockSetCodeAndNotify }}>
7172
<Provider>
7273
<IntegrationTypeSelector />
7374
</Provider>
7475
</SourceCodeApiContext.Provider>
7576
</RuntimeContext.Provider>,
7677
),
7778
mockSetSelectedCatalog,
79+
mockSetCodeAndNotify,
7880
};
7981
};
8082

@@ -159,8 +161,9 @@ describe('IntegrationTypeSelector.tsx', () => {
159161
expect(modalText.textContent).not.toContain('This will also change the current selected catalog');
160162
});
161163

162-
it('should warn the user when selected flow changes the catalog', async () => {
163-
const { findByTestId, getByTestId, mockSetSelectedCatalog } = renderWithCustomRuntime(mockCamelCatalog);
164+
it('should warn the user when selecting a flow type that previously changed the catalog', async () => {
165+
const { findByTestId, getByTestId, mockSetSelectedCatalog, mockSetCodeAndNotify } =
166+
renderWithCustomRuntime(mockCamelCatalog);
164167

165168
const trigger = await findByTestId('integration-type-list-dropdown');
166169

@@ -181,7 +184,7 @@ describe('IntegrationTypeSelector.tsx', () => {
181184

182185
const modalText = await findByTestId('confirmation-modal-text');
183186
expect(modalText).toBeInTheDocument();
184-
expect(modalText.textContent).toContain('This will also change the current selected catalog');
187+
expect(modalText.textContent).not.toContain('This will also change the current selected catalog');
185188

186189
/** Confirm **/
187190
const confirmButton = await findByTestId('confirmation-modal-confirm');
@@ -191,11 +194,8 @@ describe('IntegrationTypeSelector.tsx', () => {
191194
});
192195

193196
await waitFor(() => {
194-
expect(mockSetSelectedCatalog).toHaveBeenCalledWith(
195-
expect.objectContaining({
196-
runtime: 'Citrus',
197-
}),
198-
);
197+
expect(mockSetCodeAndNotify).toHaveBeenCalled();
198+
expect(mockSetSelectedCatalog).not.toHaveBeenCalled();
199199
});
200200
});
201201
});
Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,27 @@
1-
import { isDefined } from '@kaoto/forms';
21
import { FunctionComponent, PropsWithChildren, useCallback, useContext, useState } from 'react';
32

4-
import { useRuntimeContext } from '../../../../hooks/useRuntimeContext/useRuntimeContext';
53
import { SourceSchemaType } from '../../../../models/camel';
64
import { FlowTemplateService } from '../../../../models/visualization/flows/support/flow-templates-service';
75
import { SourceCodeApiContext } from '../../../../providers';
8-
import { findCatalog } from '../../../../utils/catalog-helper';
96
import { ChangeIntegrationTypeModal } from './ChangeIntegrationTypeModal/ChangeIntegrationTypeModal';
107
import { IntegrationTypeSelectorToggle } from './IntegrationTypeSelectorToggle/IntegrationTypeSelectorToggle';
118

129
export const IntegrationTypeSelector: FunctionComponent<PropsWithChildren> = () => {
13-
const runtimeContext = useRuntimeContext();
1410
const sourceCodeContextApi = useContext(SourceCodeApiContext);
1511
const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false);
1612
const [proposedFlowType, setProposedFlowType] = useState<SourceSchemaType>();
17-
const [changeCatalog, setChangeCatalog] = useState<boolean>();
1813

19-
const checkBeforeAddNewFlow = useCallback((flowType: SourceSchemaType, changeCatalog: boolean) => {
20-
/**
21-
* If it is not the same integration type, this operation might result in
22-
* removing the existing flows, so then we warn the user first
23-
*/
14+
const checkBeforeAddNewFlow = useCallback((flowType: SourceSchemaType) => {
2415
setProposedFlowType(flowType);
25-
setChangeCatalog(changeCatalog);
2616
setIsConfirmationModalOpen(true);
2717
}, []);
2818

2919
const onConfirm = useCallback(() => {
3020
if (proposedFlowType) {
3121
sourceCodeContextApi.setCodeAndNotify(FlowTemplateService.getFlowYamlTemplate(proposedFlowType));
32-
33-
if (changeCatalog) {
34-
const matchingCatalog = findCatalog(proposedFlowType, runtimeContext.catalogLibrary);
35-
if (isDefined(matchingCatalog)) {
36-
runtimeContext.setSelectedCatalog(matchingCatalog);
37-
}
38-
}
39-
4022
setIsConfirmationModalOpen(false);
4123
}
42-
}, [proposedFlowType, runtimeContext, sourceCodeContextApi, changeCatalog]);
24+
}, [proposedFlowType, sourceCodeContextApi]);
4325

4426
const onCancel = useCallback(() => {
4527
setIsConfirmationModalOpen(false);
@@ -48,12 +30,7 @@ export const IntegrationTypeSelector: FunctionComponent<PropsWithChildren> = ()
4830
return (
4931
<>
5032
<IntegrationTypeSelectorToggle onSelect={checkBeforeAddNewFlow} />
51-
<ChangeIntegrationTypeModal
52-
isOpen={isConfirmationModalOpen}
53-
changesCatalog={changeCatalog}
54-
onConfirm={onConfirm}
55-
onCancel={onCancel}
56-
/>
33+
<ChangeIntegrationTypeModal isOpen={isConfirmationModalOpen} onConfirm={onConfirm} onCancel={onCancel} />
5734
</>
5835
);
5936
};

packages/ui/src/components/Visualization/ContextToolbar/IntegrationTypeSelector/IntegrationTypeSelectorToggle/IntegrationTypeSelectorToggle.tsx

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,38 @@
11
import { MenuToggle, Select, SelectList, SelectOption } from '@patternfly/react-core';
22
import { FunctionComponent, MouseEvent, RefObject, useCallback, useContext, useState } from 'react';
33

4-
import { useRuntimeContext } from '../../../../../hooks/useRuntimeContext/useRuntimeContext';
54
import { ISourceSchema, sourceSchemaConfig, SourceSchemaType } from '../../../../../models/camel';
65
import { EntitiesContext } from '../../../../../providers/entities.provider';
76
import { getSupportedDsls } from '../../../../../serializers/serializer-dsl-lists';
8-
import { requiresCatalogChange } from '../../../../../utils/catalog-helper';
97

108
interface ISourceTypeSelector {
11-
onSelect?: (value: SourceSchemaType, changeCatalog: boolean) => void;
9+
onSelect?: (value: SourceSchemaType) => void;
1210
}
1311

1412
export const IntegrationTypeSelectorToggle: FunctionComponent<ISourceTypeSelector> = (props) => {
15-
const runtimeContext = useRuntimeContext();
16-
const { selectedCatalog } = runtimeContext;
1713
const { currentSchemaType, camelResource } = useContext(EntitiesContext)!;
1814
const currentFlowType: ISourceSchema = sourceSchemaConfig.config[currentSchemaType];
1915
const [isOpen, setIsOpen] = useState(false);
2016
const dslEntries = getSupportedDsls(camelResource);
2117

2218
const onSelect = useCallback(
2319
(_event: MouseEvent | undefined, flowType: string | number | undefined) => {
24-
if (!flowType) {
25-
return;
26-
}
20+
if (!flowType) return;
2721
const integrationType = sourceSchemaConfig.config[flowType as SourceSchemaType];
2822

2923
setIsOpen(false);
3024
if (integrationType !== undefined) {
31-
props.onSelect?.(
32-
flowType as SourceSchemaType,
33-
requiresCatalogChange(flowType as SourceSchemaType, selectedCatalog),
34-
);
25+
props.onSelect?.(flowType as SourceSchemaType);
3526
}
3627
},
37-
[props, selectedCatalog],
28+
[props],
3829
);
3930

4031
const toggle = (toggleRef: RefObject<HTMLButtonElement>) => (
4132
<MenuToggle
4233
data-testid="integration-type-list-dropdown"
4334
ref={toggleRef}
44-
onClick={() => {
45-
setIsOpen(!isOpen);
46-
}}
35+
onClick={() => setIsOpen(!isOpen)}
4736
isExpanded={isOpen}
4837
>
4938
{sourceSchemaConfig.config[currentSchemaType].name}
@@ -64,7 +53,6 @@ export const IntegrationTypeSelectorToggle: FunctionComponent<ISourceTypeSelecto
6453
{dslEntries.map((sourceType, index) => {
6554
const sourceSchema = sourceSchemaConfig.config[sourceType];
6655
const isCurrentType = sourceSchema.name === currentFlowType.name;
67-
const changeCatalog = requiresCatalogChange(sourceType, selectedCatalog);
6856

6957
return (
7058
<SelectOption
@@ -79,7 +67,6 @@ export const IntegrationTypeSelectorToggle: FunctionComponent<ISourceTypeSelecto
7967
isDisabled={isCurrentType}
8068
>
8169
{sourceSchema.name}
82-
{changeCatalog && !isCurrentType && ' (in different catalog)'}
8370
{isCurrentType && ' (current integration type)'}
8471
</SelectOption>
8572
);

packages/ui/src/components/Visualization/ContextToolbar/RuntimeSelector/RuntimeSelector.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { act, fireEvent, render, waitFor } from '@testing-library/react';
22

3+
import { LocalStorageKeys } from '../../../../models';
34
import { TestProvidersWrapper, TestRuntimeProviderWrapper } from '../../../../stubs';
45
import { RuntimeSelector } from './RuntimeSelector';
56

@@ -53,6 +54,14 @@ describe('RuntimeSelector', () => {
5354
await waitFor(async () => {
5455
expect(setSelectedCatalog).toHaveBeenCalled();
5556
});
57+
58+
const raw = localStorage.getItem(LocalStorageKeys.SelectedCatalog);
59+
if (raw !== null) {
60+
const parsed = JSON.parse(raw);
61+
expect(parsed).toEqual(expect.any(Object));
62+
expect((parsed as Record<string, unknown>).name).toBeUndefined();
63+
expect((parsed as Record<string, unknown>).version).toBeUndefined();
64+
}
5665
});
5766

5867
it('should toggle list of Runtimes', async () => {

packages/ui/src/components/Visualization/ContextToolbar/RuntimeSelector/RuntimeSelector.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import citrusLogo from '../../../../assets/citrus-logo.png';
99
import quarkusLogo from '../../../../assets/quarkus-logo.svg';
1010
import redhatLogo from '../../../../assets/redhat-logo.svg';
1111
import springBootLogo from '../../../../assets/springboot-logo.svg';
12-
import { useLocalStorage } from '../../../../hooks';
1312
import { useRuntimeContext } from '../../../../hooks/useRuntimeContext/useRuntimeContext';
14-
import { LocalStorageKeys } from '../../../../models';
13+
import { SourceSchemaType } from '../../../../models/camel';
14+
import { COMPATIBLE_RUNTIMES_BY_SCHEMA_TYPE } from '../../../../models/camel/compatible-runtimes';
1515
import { EntitiesContext } from '../../../../providers';
1616

1717
const SPACE_REGEX = /\s/g;
@@ -55,11 +55,8 @@ export const RuntimeSelector: FunctionComponent = () => {
5555
const toggleRef = useRef<HTMLButtonElement>(null);
5656
const runtimeContext = useRuntimeContext();
5757
const entitiesContext = useContext(EntitiesContext);
58-
const compatibleRuntimes = entitiesContext?.camelResource.getCompatibleRuntimes() ?? [];
59-
const [_, setSelectedCatalogLocalStorage] = useLocalStorage(
60-
LocalStorageKeys.SelectedCatalog,
61-
runtimeContext.selectedCatalog,
62-
);
58+
const currentSchemaType = entitiesContext?.currentSchemaType ?? SourceSchemaType.Route;
59+
const compatibleRuntimes = COMPATIBLE_RUNTIMES_BY_SCHEMA_TYPE[currentSchemaType];
6360
const groupedRuntimes =
6461
runtimeContext.catalogLibrary?.definitions
6562
.filter((catalog) => compatibleRuntimes.includes(catalog.runtime))
@@ -88,12 +85,11 @@ export const RuntimeSelector: FunctionComponent = () => {
8885

8986
if (isDefined(selectedCatalog)) {
9087
runtimeContext.setSelectedCatalog(selectedCatalog);
91-
setSelectedCatalogLocalStorage(selectedCatalog);
9288
}
9389

9490
setIsOpen(false);
9591
},
96-
[runtimeContext, setSelectedCatalogLocalStorage],
92+
[runtimeContext],
9793
);
9894

9995
const getMenuItem = useCallback(

packages/ui/src/components/Visualization/EmptyState/FlowType/NewFlow.test.tsx

Lines changed: 7 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ describe('NewFlow.tsx', () => {
5555
sourceSchemaType: SourceSchemaType = SourceSchemaType.Integration,
5656
) => {
5757
const mockSetSelectedCatalog = jest.fn();
58+
const mockSetCodeAndNotify = jest.fn();
5859
const defaultRuntimeContext: IRuntimeContext = {
5960
basePath: '',
6061
selectedCatalog: mockCamelCatalog,
@@ -69,7 +70,7 @@ describe('NewFlow.tsx', () => {
6970
<RuntimeContext.Provider value={mergedRuntimeContext}>
7071
<SourceCodeApiContext.Provider
7172
value={{
72-
setCodeAndNotify: jest.fn(),
73+
setCodeAndNotify: mockSetCodeAndNotify,
7374
}}
7475
>
7576
<EntitiesContext.Provider
@@ -89,6 +90,7 @@ describe('NewFlow.tsx', () => {
8990
</RuntimeContext.Provider>,
9091
),
9192
mockSetSelectedCatalog,
93+
mockSetCodeAndNotify,
9294
};
9395
};
9496

@@ -128,13 +130,11 @@ describe('NewFlow.tsx', () => {
128130
expect(modal).toBeInTheDocument();
129131
});
130132

131-
it('should update catalog when switching to Citrus test', async () => {
132-
const mockSetSelectedCatalog = jest.fn();
133-
const { findByTestId, getByText } = renderWithContext(
133+
it('should call setCodeAndNotify and not directly call setSelectedCatalog when switching flow types', async () => {
134+
const { findByTestId, getByText, mockSetCodeAndNotify, mockSetSelectedCatalog } = renderWithContext(
134135
{
135136
selectedCatalog: mockCamelCatalog,
136137
catalogLibrary: mockCatalogLibrary,
137-
setSelectedCatalog: mockSetSelectedCatalog,
138138
},
139139
SourceSchemaType.Route,
140140
);
@@ -158,41 +158,8 @@ describe('NewFlow.tsx', () => {
158158
fireEvent.click(confirmButton);
159159
});
160160

161-
/** Verify catalog was updated to Citrus */
162-
expect(mockSetSelectedCatalog).toHaveBeenCalledWith(mockCitrusCatalog);
163-
});
164-
165-
it('should not update catalog when switching between Camel flow types', async () => {
166-
const mockSetSelectedCatalog = jest.fn();
167-
const { findByTestId, getByText } = renderWithContext(
168-
{
169-
selectedCatalog: mockCamelCatalog,
170-
catalogLibrary: mockCatalogLibrary,
171-
setSelectedCatalog: mockSetSelectedCatalog,
172-
},
173-
SourceSchemaType.Route,
174-
);
175-
176-
const trigger = await findByTestId('viz-dsl-list-dropdown');
177-
178-
/** Open Select */
179-
act(() => {
180-
fireEvent.click(trigger);
181-
});
182-
183-
/** Select Pipe option (another Camel type) */
184-
act(() => {
185-
const element = getByText('Pipe');
186-
fireEvent.click(element);
187-
});
188-
189-
/** Confirm the modal */
190-
const confirmButton = await findByTestId('confirmation-modal-confirm');
191-
act(() => {
192-
fireEvent.click(confirmButton);
193-
});
194-
195-
/** Verify catalog was NOT updated since both are Camel types */
161+
/** Verify the reactive chain is triggered via setCodeAndNotify, not directly via setSelectedCatalog */
162+
expect(mockSetCodeAndNotify).toHaveBeenCalled();
196163
expect(mockSetSelectedCatalog).not.toHaveBeenCalled();
197164
});
198165
});

packages/ui/src/components/Visualization/EmptyState/FlowType/NewFlow.tsx

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
1-
import { isDefined } from '@kaoto/forms';
21
import { Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalVariant } from '@patternfly/react-core';
32
import { PlusIcon } from '@patternfly/react-icons';
43
import { FunctionComponent, PropsWithChildren, useCallback, useContext, useState } from 'react';
54

65
import { useEntityContext } from '../../../../hooks/useEntityContext/useEntityContext';
7-
import { useRuntimeContext } from '../../../../hooks/useRuntimeContext/useRuntimeContext';
86
import { ISourceSchema, sourceSchemaConfig, SourceSchemaType } from '../../../../models/camel';
97
import { FlowTemplateService } from '../../../../models/visualization/flows/support/flow-templates-service';
108
import { SourceCodeApiContext } from '../../../../providers';
119
import { VisibleFlowsContext } from '../../../../providers/visible-flows.provider';
12-
import { findCatalog, requiresCatalogChange } from '../../../../utils/catalog-helper';
1310
import { FlowTypeSelector } from './FlowTypeSelector';
1411

1512
export const NewFlow: FunctionComponent<PropsWithChildren> = () => {
16-
const runtimeContext = useRuntimeContext();
1713
const sourceCodeContextApi = useContext(SourceCodeApiContext);
1814
const { currentSchemaType, camelResource, updateEntitiesFromCamelResource } = useEntityContext();
1915
const currentFlowType: ISourceSchema = sourceSchemaConfig.config[currentSchemaType];
@@ -84,16 +80,6 @@ export const NewFlow: FunctionComponent<PropsWithChildren> = () => {
8480
onClick={() => {
8581
if (proposedFlowType) {
8682
sourceCodeContextApi.setCodeAndNotify(FlowTemplateService.getFlowYamlTemplate(proposedFlowType));
87-
88-
// Update catalog if needed when switching flow types
89-
const changeCatalog = requiresCatalogChange(proposedFlowType, runtimeContext.selectedCatalog);
90-
if (changeCatalog) {
91-
const matchingCatalog = findCatalog(proposedFlowType, runtimeContext.catalogLibrary);
92-
if (isDefined(matchingCatalog)) {
93-
runtimeContext.setSelectedCatalog(matchingCatalog);
94-
}
95-
}
96-
9783
setIsConfirmationModalOpen(false);
9884
}
9985
}}

0 commit comments

Comments
 (0)