Skip to content
Open
Show file tree
Hide file tree
Changes from 19 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
8 changes: 8 additions & 0 deletions .changeset/whole-towns-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@sap-ux/adp-flp-config-sub-generator': patch
'@sap-ux/generator-adp': patch
'@sap-ux/adp-tooling': patch
'@sap-ux/create': patch
---

feat: Adjust FLP configuration wizard for CF scenario
62 changes: 57 additions & 5 deletions packages/adp-flp-config-sub-generator/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
flpConfigurationExists,
SystemLookup,
getBaseAppInbounds,
getCfBaseAppInbounds,
loadCfConfig,
getAppParamsFromUI5Yaml,
type InternalInboundNavigation,
type AdpPreviewConfigWithTarget,
type DescriptorVariant,
Expand Down Expand Up @@ -82,6 +85,7 @@ export default class AdpFlpConfigGenerator extends Generator {
private variant: DescriptorVariant;
private tileSettingsAnswers?: TileSettingsAnswers;
private provider: AbapServiceProvider;
private isCfProject: boolean = false;

/**
* Creates an instance of the generator.
Expand All @@ -100,6 +104,7 @@ export default class AdpFlpConfigGenerator extends Generator {
this.vscode = opts.vscode;
this.inbounds = opts.inbounds;
this.layer = opts.layer;
this.isCfProject = !!opts.isCfProject;

initAppWizardCache(this.logger, this.appWizard);
this._setupFLPConfigPrompts();
Expand Down Expand Up @@ -141,7 +146,7 @@ export default class AdpFlpConfigGenerator extends Generator {
if (this.abort) {
return;
}
if (!this.launchAsSubGen) {
if (!this.launchAsSubGen && !this.isCfProject) {
await this._validateCloudProject();
if (this.abort) {
return;
Expand Down Expand Up @@ -450,9 +455,11 @@ export default class AdpFlpConfigGenerator extends Generator {
*/
private async _validateProjectType(): Promise<void> {
const isFioriAdaptation = (await getAppType(this.projectRootPath)) === 'Fiori Adaptation';
if (!isFioriAdaptation || (await isCFEnvironment(this.projectRootPath))) {
if (!isFioriAdaptation) {
this._abortExecution(t('error.projectNotSupported'));
return;
}
this.isCfProject = await isCFEnvironment(this.projectRootPath);
}

/**
Expand All @@ -474,14 +481,31 @@ export default class AdpFlpConfigGenerator extends Generator {
*/
private async _initializeStandAloneGenerator(): Promise<void> {
await this._validateProjectType();
if (this.abort) {
return;
}

this.variant = await getVariant(this.projectRootPath, this.fs);
this.appId = this.variant.reference;
this.layer = this.variant.layer;

if (this.isCfProject) {
await this._initializeCfGenerator();
} else {
await this._initializeAbapGenerator();
}
}

/**
* Initializes the ABAP-specific parts of the standalone generator.
*
* @returns {Promise<void>} A promise that resolves when initialization is complete.
*/
private async _initializeAbapGenerator(): Promise<void> {
this.ui5Yaml = await getAdpConfig<AdpPreviewConfigWithTarget>(
this.projectRootPath,
join(this.projectRootPath, FileName.Ui5Yaml)
);
this.variant = await getVariant(this.projectRootPath, this.fs);
this.appId = this.variant.reference;
this.layer = this.variant.layer;

await this._initAbapServiceProvider();

Expand All @@ -496,6 +520,34 @@ export default class AdpFlpConfigGenerator extends Generator {
}
}

/**
* Initializes the CF-specific parts of the standalone generator.
*
* @returns {Promise<void>} A promise that resolves when initialization is complete.
*/
private async _initializeCfGenerator(): Promise<void> {
const cfConfig = loadCfConfig(this.toolsLogger);
if (!cfConfig?.token) {
this._abortExecution(t('error.cfLoginRequired'));
return;
}

const appParams = getAppParamsFromUI5Yaml(this.projectRootPath);
if (!appParams.appHostId) {
this._abortExecution(t('error.cfAppHostIdMissing'));
return;
}

try {
this.inbounds =
this.inbounds ??
(await getCfBaseAppInbounds(this.appId, appParams.appHostId, cfConfig, this.toolsLogger));
} catch (e) {
this.toolsLogger.error(`CF inbounds fetching failed: ${e}`);
this._abortExecution(t('error.cfInboundsFetchFailed', { error: (e as Error).message }));
}
}

/**
* Initializes the AbapServiceProvider for the generator. If the generator is launched as a sub-generator, the provider is taken from the options.
* If the provider is cached in the app wizard, it is retrieved from the cache, otherwise, a new AbapServiceProvider is created using the ui5.yaml configuration.
Expand Down
5 changes: 4 additions & 1 deletion packages/adp-flp-config-sub-generator/src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type Generator from 'yeoman-generator';
import type { TelemetryData } from '@sap-ux/fiori-generator-shared';
import type { AbapServiceProvider } from '@sap-ux/axios-extension';
import type { ManifestNamespace, UI5FlexLayer } from '@sap-ux/project-access';

export interface FlpConfigOptions extends Generator.GeneratorOptions {
/**
* VSCode instance
Expand Down Expand Up @@ -40,6 +39,10 @@ export interface FlpConfigOptions extends Generator.GeneratorOptions {
data?: {
projectRootPath: string;
};
/**
* Flag indicating whether this is a CF environment project
*/
isCfProject?: boolean;
}

export interface State {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"authenticationFailed": "Authentication failed.",
"projectNotCloudReady": "FLP Configuration is supported for Cloud Ready adaptation projects only",
"baseAppInboundsFetching": "Error while fetching base application inbounds",
"cfConfigRequired": "Cloud Foundry configuration is required for Cloud Foundry adaptation projects.",
"cfLoginRequired": "Cloud Foundry login required. Please run `cf login` and try again.",
"cfAppHostIdMissing": "Could not determine the `appHostId` from the project's `ui5.yaml` configuration.",
"cfInboundsFetchFailed": "Failed to fetch inbounds for the Cloud Foundry application: {{error}}",
"warningCachingNotSupported": "Warning: caching is not supported"
}
}
}
203 changes: 199 additions & 4 deletions packages/adp-flp-config-sub-generator/test/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ jest.mock('@sap-ux/adp-tooling', () => ({
getAdpConfig: jest.fn(),
generateInboundConfig: jest.fn(),
getBaseAppInbounds: jest.fn(),
getCfBaseAppInbounds: jest.fn(),
loadCfConfig: jest.fn().mockReturnValue({}),
getAppParamsFromUI5Yaml: jest.fn().mockReturnValue({ appHostId: '', appName: '', appVersion: '', spaceGuid: '' }),
SystemLookup: jest.fn().mockImplementation(() => ({
getSystemByName: jest.fn().mockResolvedValue({
name: 'testDestination'
Expand Down Expand Up @@ -623,7 +626,7 @@ describe('FLPConfigGenerator Integration Tests', () => {
await expect(runContext.run()).rejects.toThrow(t('error.updatingApp'));
});

it('Should result in an error message if the project is a CF project', async () => {
it('Should result in an error message if the project is a CF project without CF login', async () => {
jest.spyOn(adpTooling, 'isCFEnvironment').mockResolvedValueOnce(true);
jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({
target: {
Expand Down Expand Up @@ -652,10 +655,10 @@ describe('FLPConfigGenerator Integration Tests', () => {

await initI18n();
await runContext.run();
expect(vsCodeMessageSpy).toHaveBeenCalledWith(t('error.projectNotSupported'));
expect(vsCodeMessageSpy).toHaveBeenCalledWith(t('error.cfLoginRequired'));
});

it('Should result in an error message if the project is a CF project and use the logger in case of CLI', async () => {
it('Should result in an error message if the project is a CF project without CF login and use the logger in case of CLI', async () => {
jest.spyOn(adpTooling, 'isCFEnvironment').mockResolvedValueOnce(true);
jest.spyOn(adpTooling, 'getAdpConfig').mockResolvedValue({
target: {
Expand Down Expand Up @@ -685,7 +688,7 @@ describe('FLPConfigGenerator Integration Tests', () => {

await initI18n();
await runContext.run();
expect(toolsLoggerErrorSpy).toHaveBeenCalledWith(t('error.projectNotSupported'));
expect(toolsLoggerErrorSpy).toHaveBeenCalledWith(t('error.cfLoginRequired'));
});

it('Should result in an error message if the project is not a CloudReady project', async () => {
Expand Down Expand Up @@ -1202,4 +1205,196 @@ describe('FLPConfigGenerator Integration Tests', () => {
await runContext.run();
expect(vsCodeMessageSpy).toHaveBeenCalledWith('Network Error');
});

it('Should work in CF sub-gen mode with pre-fetched inbounds', async () => {
const mockPrompts = {
items: [
{
name: 'Tile settings',
description: 'Configure the tile settings for the application'
},
{
name: 'SAP Fiori Launchpad Configuration',
description: ''
}
],
splice: jest.fn()
};
const testPath = join(testOutputDir, 'test_project_cf_subgen');
fs.mkdirSync(testPath, { recursive: true });
fsextra.copySync(join(__dirname, 'fixtures/app.variant1'), join(testPath, 'app.variant1'));
const testProjectPath = join(testPath, 'app.variant1');

const sendTelemetrySpy = jest.spyOn(fioriGenShared, 'sendTelemetry');

const runContext = yeomanTest
.create(
adpFlpConfigGenerator,
{
resolved: generatorPath
},
{
cwd: testProjectPath
}
)
.withOptions({
vscode,
appWizard: mockAppWizard,
loggerMock,
launchAsSubGen: true,
inbounds: inbounds,
layer: adpTooling.FlexLayer.CUSTOMER_BASE,
prompts: mockPrompts,
isCfProject: true
})
.withPrompts(answers);

await expect(runContext.run()).resolves.not.toThrow();
expect(sendTelemetrySpy).toHaveBeenCalledWith(
EventName.ADP_FLP_CONFIG_ADDED,
expect.objectContaining({
OperatingSystem: 'testOS',
Platform: 'testPlatform'
}),
testProjectPath
);
expect(generateInboundConfigSpy).toHaveBeenCalled();
});

it('Should fetch CF inbounds when CF config is available', async () => {
jest.spyOn(adpTooling, 'isCFEnvironment').mockResolvedValueOnce(true);
const mockCfConfig = {
org: { GUID: 'test-org-guid', Name: 'test-org' },
space: { GUID: 'test-space-guid', Name: 'test-space' },
url: '/test.cf',
token: 'test-token'
};
const mockInbounds = { 'inbound-1': { semanticObject: 'SO', action: 'display' } };
jest.spyOn(adpTooling, 'loadCfConfig').mockReturnValueOnce(mockCfConfig);
jest.spyOn(adpTooling, 'getAppParamsFromUI5Yaml').mockReturnValueOnce({
appHostId: 'test-host',
appName: 'test-app',
appVersion: '1.0.0',
spaceGuid: 'test-space-guid'
});
const getCfBaseAppInboundsSpy = jest
.spyOn(adpTooling, 'getCfBaseAppInbounds')
.mockResolvedValueOnce(mockInbounds as unknown as projectAccess.ManifestNamespace.Inbound);

const testProjectPath = join(__dirname, 'fixtures/app.variant1');

const runContext = yeomanTest
.create(
adpFlpConfigGenerator,
{
resolved: generatorPath
},
{
cwd: testProjectPath
}
)
.withOptions({
vscode,
appWizard: mockAppWizard,
launchFlpConfigAsSubGenerator: false
})
.withPrompts(answers);

await runContext.run();
expect(getCfBaseAppInboundsSpy).toHaveBeenCalledWith(
'mockReference',
'test-host',
mockCfConfig,
expect.anything()
);
});

it('Should abort when CF inbound fetch fails', async () => {
jest.spyOn(adpTooling, 'isCFEnvironment').mockResolvedValueOnce(true);
const mockCfConfig = {
org: { GUID: 'test-org-guid', Name: 'test-org' },
space: { GUID: 'test-space-guid', Name: 'test-space' },
url: '/test.cf',
token: 'test-token'
};
jest.spyOn(adpTooling, 'loadCfConfig').mockReturnValueOnce(mockCfConfig);
jest.spyOn(adpTooling, 'getAppParamsFromUI5Yaml').mockReturnValueOnce({
appHostId: 'test-host',
appName: 'test-app',
appVersion: '1.0.0',
spaceGuid: 'test-space-guid'
});
jest.spyOn(adpTooling, 'getCfBaseAppInbounds').mockRejectedValueOnce(new Error('Connection failed'));

const testProjectPath = join(__dirname, 'fixtures/app.variant1');

const runContext = yeomanTest
.create(
adpFlpConfigGenerator,
{
resolved: generatorPath
},
{
cwd: testProjectPath
}
)
.withOptions({
vscode,
appWizard: mockAppWizard,
launchFlpConfigAsSubGenerator: false
})
.withPrompts(answers);

await initI18n();
await runContext.run();
expect(vsCodeMessageSpy).toHaveBeenCalledWith(t('error.cfInboundsFetchFailed', { error: 'Connection failed' }));
});

it('Should load CF config from local environment in standalone mode', async () => {
jest.spyOn(adpTooling, 'isCFEnvironment').mockResolvedValueOnce(true);
const mockCfConfig = {
org: { GUID: 'test-org-guid', Name: 'test-org' },
space: { GUID: 'test-space-guid', Name: 'test-space' },
url: '/test.cf',
token: 'test-token'
};
jest.spyOn(adpTooling, 'loadCfConfig').mockReturnValueOnce(mockCfConfig);
jest.spyOn(adpTooling, 'getAppParamsFromUI5Yaml').mockReturnValueOnce({
appHostId: 'auto-detected-host',
appName: 'test-app',
appVersion: '1.0.0',
spaceGuid: 'test-space-guid'
});
const mockInbounds = { 'inbound-1': { semanticObject: 'SO', action: 'display' } };
const getCfBaseAppInboundsSpy = jest
.spyOn(adpTooling, 'getCfBaseAppInbounds')
.mockResolvedValueOnce(mockInbounds as unknown as projectAccess.ManifestNamespace.Inbound);

const testProjectPath = join(__dirname, 'fixtures/app.variant1');

const runContext = yeomanTest
.create(
adpFlpConfigGenerator,
{
resolved: generatorPath
},
{
cwd: testProjectPath
}
)
.withOptions({
vscode,
appWizard: mockAppWizard,
launchFlpConfigAsSubGenerator: false
})
.withPrompts(answers);

await runContext.run();
expect(getCfBaseAppInboundsSpy).toHaveBeenCalledWith(
'mockReference',
'auto-detected-host',
mockCfConfig,
expect.anything()
);
});
});
Loading
Loading