mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-30 11:26:23 +00:00
fix(ui): app schema ref resolve (#23016)
* resolve application schema refs * add details and install comps for plugin * add tests * use parse schema
This commit is contained in:
parent
b1a7d4d8ae
commit
20486c23c5
4
.gitignore
vendored
4
.gitignore
vendored
@ -88,9 +88,7 @@ openmetadata-ui/src/main/resources/ui/playwright/.auth
|
|||||||
openmetadata-ui/src/main/resources/ui/blob-report
|
openmetadata-ui/src/main/resources/ui/blob-report
|
||||||
|
|
||||||
#UI - Dereferenced Schemas
|
#UI - Dereferenced Schemas
|
||||||
openmetadata-ui/src/main/resources/ui/src/jsons/connectionSchemas
|
openmetadata-ui/src/main/resources/ui/src/jsons/*
|
||||||
openmetadata-ui/src/main/resources/ui/src/jsons/ingestionSchemas
|
|
||||||
openmetadata-ui/src/main/resources/ui/src/jsons/governanceSchemas
|
|
||||||
|
|
||||||
|
|
||||||
#vscode
|
#vscode
|
||||||
|
69
CLAUDE.md
69
CLAUDE.md
@ -29,9 +29,12 @@ make yarn_install_cache # Install UI dependencies
|
|||||||
cd openmetadata-ui/src/main/resources/ui
|
cd openmetadata-ui/src/main/resources/ui
|
||||||
yarn start # Start development server on localhost:3000
|
yarn start # Start development server on localhost:3000
|
||||||
yarn test # Run Jest unit tests
|
yarn test # Run Jest unit tests
|
||||||
|
yarn test path/to/test.spec.ts # Run a specific test file
|
||||||
|
yarn test:watch # Run tests in watch mode
|
||||||
yarn playwright:run # Run E2E tests
|
yarn playwright:run # Run E2E tests
|
||||||
yarn lint # ESLint check
|
yarn lint # ESLint check
|
||||||
yarn lint:fix # ESLint with auto-fix
|
yarn lint:fix # ESLint with auto-fix
|
||||||
|
yarn build # Production build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Backend Development
|
### Backend Development
|
||||||
@ -76,9 +79,15 @@ OpenMetadata uses a schema-first approach with JSON Schema definitions driving c
|
|||||||
make generate # Generate all models from schemas
|
make generate # Generate all models from schemas
|
||||||
make py_antlr # Generate Python ANTLR parsers
|
make py_antlr # Generate Python ANTLR parsers
|
||||||
make js_antlr # Generate JavaScript ANTLR parsers
|
make js_antlr # Generate JavaScript ANTLR parsers
|
||||||
yarn parse-schema # Parse JSON schemas for frontend
|
yarn parse-schema # Parse JSON schemas for frontend (connection and ingestion schemas)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Schema Architecture
|
||||||
|
- **Source schemas** in `openmetadata-spec/` define the canonical data models
|
||||||
|
- **Connection schemas** are pre-processed at build time via `parseSchemas.js` to resolve all `$ref` references
|
||||||
|
- **Application schemas** in `openmetadata-ui/.../ApplicationSchemas/` are resolved at runtime using `schemaResolver.ts`
|
||||||
|
- JSON schemas with `$ref` references to external files require resolution before use in forms
|
||||||
|
|
||||||
## Key Directories
|
## Key Directories
|
||||||
|
|
||||||
- `openmetadata-service/` - Core Java backend services and REST APIs
|
- `openmetadata-service/` - Core Java backend services and REST APIs
|
||||||
@ -92,11 +101,54 @@ yarn parse-schema # Parse JSON schemas for frontend
|
|||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
1. **Schema Changes**: Modify JSON schemas in `openmetadata-spec/`, then run `mvn clean install` on openmetadata-spec to update models
|
1. **Schema Changes**: Modify JSON schemas in `openmetadata-spec/`, then run `mvn clean install` on openmetadata-spec to update models
|
||||||
2. **Backend**: Develop in Java using Dropwizard patterns, test with `mvn test`
|
2. **Backend**: Develop in Java using Dropwizard patterns, test with `mvn test`, format with `mvn spotless:apply`
|
||||||
3. **Frontend**: Use React/TypeScript with Ant Design components, test with Jest/Playwright
|
3. **Frontend**: Use React/TypeScript with Ant Design components, test with Jest/Playwright
|
||||||
4. **Ingestion**: Python connectors follow plugin pattern, use `make install_dev_env` for development
|
4. **Ingestion**: Python connectors follow plugin pattern, use `make install_dev_env` for development
|
||||||
5. **Full Testing**: Use `make run_e2e_tests` before major changes
|
5. **Full Testing**: Use `make run_e2e_tests` before major changes
|
||||||
|
|
||||||
|
## Frontend Architecture Patterns
|
||||||
|
|
||||||
|
### React Component Patterns
|
||||||
|
- **File Naming**: Components use `ComponentName.component.tsx`, interfaces use `ComponentName.interface.ts`
|
||||||
|
- **State Management**: Use `useState` with proper typing, avoid `any`
|
||||||
|
- **Side Effects**: Use `useEffect` with proper dependency arrays
|
||||||
|
- **Performance**: Use `useCallback` for event handlers, `useMemo` for expensive computations
|
||||||
|
- **Custom Hooks**: Prefix with `use`, place in `src/hooks/`, return typed objects
|
||||||
|
- **Internationalization**: Use `useTranslation` hook from react-i18next, access with `t('key')`
|
||||||
|
- **Component Structure**: Functional components only, no class components
|
||||||
|
- **Props**: Define interfaces for all component props, place in `.interface.ts` files
|
||||||
|
- **Loading States**: Use object state for multiple loading states: `useState<Record<string, boolean>>({})`
|
||||||
|
- **Error Handling**: Use `showErrorToast` and `showSuccessToast` utilities from ToastUtils
|
||||||
|
- **Navigation**: Use `useNavigate` from react-router-dom, not direct history manipulation
|
||||||
|
- **Data Fetching**: Async functions with try-catch blocks, update loading states appropriately
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
- Use Zustand stores for global state (e.g., `useLimitStore`, `useWelcomeStore`)
|
||||||
|
- Keep component state local when possible with `useState`
|
||||||
|
- Use context providers for feature-specific shared state (e.g., `ApplicationsProvider`)
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
- Use Ant Design components as the primary UI library
|
||||||
|
- Custom styles in `.less` files with component-specific naming
|
||||||
|
- Follow BEM naming convention for custom CSS classes
|
||||||
|
- Use CSS modules where appropriate
|
||||||
|
|
||||||
|
### Application Configuration
|
||||||
|
- Applications use `ApplicationsClassBase` for schema loading and configuration
|
||||||
|
- Dynamic imports handle application-specific schemas and assets
|
||||||
|
- Form schemas use React JSON Schema Form (RJSF) with custom UI widgets
|
||||||
|
|
||||||
|
### Service Utilities
|
||||||
|
- Each service type has dedicated utility files (e.g., `DatabaseServiceUtils.tsx`)
|
||||||
|
- Connection schemas are imported statically and pre-resolved
|
||||||
|
- Service configurations use switch statements to map types to schemas
|
||||||
|
|
||||||
|
### Type Safety
|
||||||
|
- All API responses have generated TypeScript interfaces in `generated/`
|
||||||
|
- Custom types extend base interfaces when needed
|
||||||
|
- Avoid type assertions unless absolutely necessary
|
||||||
|
- Use discriminated unions for action types and state variants
|
||||||
|
|
||||||
## Database and Migrations
|
## Database and Migrations
|
||||||
|
|
||||||
- Flyway handles schema migrations in `bootstrap/sql/migrations/`
|
- Flyway handles schema migrations in `bootstrap/sql/migrations/`
|
||||||
@ -128,6 +180,19 @@ yarn parse-schema # Parse JSON schemas for frontend
|
|||||||
- Follow existing project patterns and conventions
|
- Follow existing project patterns and conventions
|
||||||
- Generate production-ready code, not tutorial code
|
- Generate production-ready code, not tutorial code
|
||||||
|
|
||||||
|
### TypeScript/Frontend Code Requirements
|
||||||
|
- **NEVER use `any` type** in TypeScript code - always use proper types
|
||||||
|
- Use `unknown` when the type is truly unknown and add type guards
|
||||||
|
- Import types from existing type definitions (e.g., `RJSFSchema` from `@rjsf/utils`)
|
||||||
|
- Follow ESLint rules strictly - the project enforces no-console, proper formatting
|
||||||
|
- Add `// eslint-disable-next-line` comments only when absolutely necessary
|
||||||
|
- **Import Organization** (in order):
|
||||||
|
1. External libraries (React, Ant Design, etc.)
|
||||||
|
2. Internal absolute imports from `generated/`, `constants/`, `hooks/`, etc.
|
||||||
|
3. Relative imports for utilities and components
|
||||||
|
4. Asset imports (SVGs, styles)
|
||||||
|
5. Type imports grouped separately when needed
|
||||||
|
|
||||||
### Response Format
|
### Response Format
|
||||||
- Provide clean code blocks without unnecessary explanations
|
- Provide clean code blocks without unnecessary explanations
|
||||||
- Assume readers are experienced developers
|
- Assume readers are experienced developers
|
||||||
|
@ -130,6 +130,57 @@ async function main(rootDir, srcDir, destDir, shouldDereference = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to parse Application schemas
|
||||||
|
async function parseApplicationSchemas() {
|
||||||
|
const appSchemaDir = 'src/utils/ApplicationSchemas';
|
||||||
|
const destDir = 'src/jsons/applicationSchemas';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create destination directory if it doesn't exist
|
||||||
|
if (!fs.existsSync(destDir)) {
|
||||||
|
fs.mkdirSync(destDir, { recursive: true });
|
||||||
|
} else {
|
||||||
|
// Clean existing destination directory
|
||||||
|
fs.rmSync(destDir, { recursive: true });
|
||||||
|
fs.mkdirSync(destDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all JSON files in ApplicationSchemas directory
|
||||||
|
const files = fs.readdirSync(appSchemaDir).filter(file => file.endsWith('.json'));
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(appSchemaDir, file);
|
||||||
|
const destPath = path.join(destDir, file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Change to the source directory for relative path resolution
|
||||||
|
const fileDir = path.dirname(filePath);
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
process.chdir(fileDir);
|
||||||
|
|
||||||
|
// Parse and dereference the schema
|
||||||
|
let parsedSchema = await parser.parse(file);
|
||||||
|
parsedSchema = await parser.dereference(parsedSchema);
|
||||||
|
|
||||||
|
// Remove $id fields
|
||||||
|
const updatedSchema = removeObjectByKey(parsedSchema, '$id');
|
||||||
|
|
||||||
|
// Change back to original directory
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
|
||||||
|
// Write the processed schema to destination
|
||||||
|
fs.writeFileSync(destPath, JSON.stringify(updatedSchema, null, 2));
|
||||||
|
console.log(`Processed ApplicationSchema: ${file}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error processing ${file}:`, err.message);
|
||||||
|
process.chdir(cwd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error parsing Application schemas:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Execute the parsing for connection and ingestion schemas
|
// Execute the parsing for connection and ingestion schemas
|
||||||
async function runParsers() {
|
async function runParsers() {
|
||||||
// For connection schemas
|
// For connection schemas
|
||||||
@ -152,6 +203,9 @@ async function runParsers() {
|
|||||||
'schema/governance/workflows/elements/nodes',
|
'schema/governance/workflows/elements/nodes',
|
||||||
'src/jsons/governanceSchemas'
|
'src/jsons/governanceSchemas'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Parse Application schemas
|
||||||
|
await parseApplicationSchemas();
|
||||||
}
|
}
|
||||||
|
|
||||||
runParsers();
|
runParsers();
|
||||||
|
@ -109,7 +109,7 @@ const AppDetails = () => {
|
|||||||
|
|
||||||
const schema = await applicationsClassBase.importSchema(fqn);
|
const schema = await applicationsClassBase.importSchema(fqn);
|
||||||
|
|
||||||
setJsonSchema(schema.default);
|
setJsonSchema(schema);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorToast(error as AxiosError);
|
showErrorToast(error as AxiosError);
|
||||||
} finally {
|
} finally {
|
||||||
@ -325,15 +325,15 @@ const AppDetails = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if there's a plugin configuration component for this app
|
// Check if there's a plugin app details component for this app
|
||||||
const pluginConfigComponent = useMemo(() => {
|
const pluginAppDetailsComponent = useMemo(() => {
|
||||||
if (!appData?.name || !plugins.length) {
|
if (!appData?.name || !plugins.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugin = plugins.find((p) => p.name === appData.name);
|
const plugin = plugins.find((p) => p.name === appData.name);
|
||||||
|
|
||||||
return plugin?.getConfigComponent?.(appData) || null;
|
return plugin?.getAppDetails?.(appData) || null;
|
||||||
}, [appData?.name, plugins]);
|
}, [appData?.name, plugins]);
|
||||||
|
|
||||||
const tabs = useMemo(() => {
|
const tabs = useMemo(() => {
|
||||||
@ -348,11 +348,7 @@ const AppDetails = () => {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
key: ApplicationTabs.CONFIGURATION,
|
key: ApplicationTabs.CONFIGURATION,
|
||||||
children: pluginConfigComponent ? (
|
children: (
|
||||||
// Use plugin configuration component if available
|
|
||||||
React.createElement(pluginConfigComponent)
|
|
||||||
) : (
|
|
||||||
// Fall back to default ApplicationConfiguration
|
|
||||||
<ApplicationConfiguration
|
<ApplicationConfiguration
|
||||||
appData={appData}
|
appData={appData}
|
||||||
isLoading={loadingState.isSaveLoading}
|
isLoading={loadingState.isSaveLoading}
|
||||||
@ -535,12 +531,18 @@ const AppDetails = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
<Col className="app-details-page-tabs" span={24}>
|
<Col className="app-details-page-tabs" span={24}>
|
||||||
|
{pluginAppDetailsComponent ? (
|
||||||
|
// Render plugin's custom app details component
|
||||||
|
React.createElement(pluginAppDetailsComponent)
|
||||||
|
) : (
|
||||||
|
// Render default tabs interface
|
||||||
<Tabs
|
<Tabs
|
||||||
destroyInactiveTabPane
|
destroyInactiveTabPane
|
||||||
className="tabs-new"
|
className="tabs-new"
|
||||||
data-testid="tabs"
|
data-testid="tabs"
|
||||||
items={tabs}
|
items={tabs}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Collate.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AppType } from '../../../../generated/entity/applications/app';
|
||||||
|
import applicationsClassBase from './ApplicationsClassBase';
|
||||||
|
|
||||||
|
describe('ApplicationsClassBase', () => {
|
||||||
|
describe('importSchema', () => {
|
||||||
|
it('should import pre-parsed schema', async () => {
|
||||||
|
// Mock the dynamic import
|
||||||
|
jest.doMock(
|
||||||
|
'../../../../jsons/applicationSchemas/SearchIndexingApplication.json',
|
||||||
|
() => ({
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
default: 'SearchIndexing',
|
||||||
|
},
|
||||||
|
cacheSize: {
|
||||||
|
type: 'integer',
|
||||||
|
default: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ virtual: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const schema = await applicationsClassBase.importSchema(
|
||||||
|
'SearchIndexingApplication'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(schema).toBeDefined();
|
||||||
|
expect(schema.type).toBe('object');
|
||||||
|
expect(schema.properties).toBeDefined();
|
||||||
|
// Should not contain any $ref since schemas are pre-parsed
|
||||||
|
expect(JSON.stringify(schema)).not.toContain('$ref');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getJSONUISchema', () => {
|
||||||
|
it('should return UI schema configuration', () => {
|
||||||
|
const uiSchema = applicationsClassBase.getJSONUISchema();
|
||||||
|
|
||||||
|
expect(uiSchema).toBeDefined();
|
||||||
|
expect(uiSchema.moduleConfiguration?.dataAssets?.serviceFilter).toEqual({
|
||||||
|
'ui:widget': 'hidden',
|
||||||
|
});
|
||||||
|
expect(uiSchema.entityLink).toEqual({
|
||||||
|
'ui:widget': 'hidden',
|
||||||
|
});
|
||||||
|
expect(uiSchema.type).toEqual({
|
||||||
|
'ui:widget': 'hidden',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getScheduleOptionsForApp', () => {
|
||||||
|
it('should return week schedule for DataInsightsReportApplication', () => {
|
||||||
|
const options = applicationsClassBase.getScheduleOptionsForApp(
|
||||||
|
'DataInsightsReportApplication',
|
||||||
|
AppType.Internal
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(options).toEqual(['week']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return day schedule for External apps', () => {
|
||||||
|
const options = applicationsClassBase.getScheduleOptionsForApp(
|
||||||
|
'SomeExternalApp',
|
||||||
|
AppType.External
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(options).toEqual(['day']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when no schedules provided for other apps', () => {
|
||||||
|
const options = applicationsClassBase.getScheduleOptionsForApp(
|
||||||
|
'SomeApp',
|
||||||
|
AppType.Internal
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(options).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -17,8 +17,13 @@ import { getScheduleOptionsFromSchedules } from '../../../../utils/SchedularUtil
|
|||||||
import { AppPlugin } from '../plugins/AppPlugin';
|
import { AppPlugin } from '../plugins/AppPlugin';
|
||||||
|
|
||||||
class ApplicationsClassBase {
|
class ApplicationsClassBase {
|
||||||
public importSchema(fqn: string) {
|
public async importSchema(fqn: string) {
|
||||||
return import(`../../../../utils/ApplicationSchemas/${fqn}.json`);
|
const module = await import(
|
||||||
|
`../../../../jsons/applicationSchemas/${fqn}.json`
|
||||||
|
);
|
||||||
|
const schema = module.default || module;
|
||||||
|
|
||||||
|
return schema;
|
||||||
}
|
}
|
||||||
public getJSONUISchema() {
|
public getJSONUISchema() {
|
||||||
return {
|
return {
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { RouteProps } from 'react-router-dom';
|
import { RouteProps } from 'react-router-dom';
|
||||||
import { App } from '../../../../generated/entity/applications/app';
|
import { App } from '../../../../generated/entity/applications/app';
|
||||||
|
import { AppMarketPlaceDefinition } from '../../../../generated/entity/applications/marketplace/appMarketPlaceDefinition';
|
||||||
import { LeftSidebarItem } from '../../../MyData/LeftSidebar/LeftSidebar.interface';
|
import { LeftSidebarItem } from '../../../MyData/LeftSidebar/LeftSidebar.interface';
|
||||||
|
|
||||||
export interface LeftSidebarItemExample extends LeftSidebarItem {
|
export interface LeftSidebarItemExample extends LeftSidebarItem {
|
||||||
@ -39,14 +40,15 @@ export interface AppPlugin {
|
|||||||
isInstalled: boolean;
|
isInstalled: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional method that returns a React component for plugin configuration.
|
* Optional method that returns a React component for the entire app details view.
|
||||||
* It is the responsibility of this component to update application data using patchApplication API
|
* If provided, this component will replace the default tabs interface.
|
||||||
|
* It is the responsibility of this component to handle all app details functionality.
|
||||||
*
|
*
|
||||||
* @param app - The App entity containing application details and configuration
|
* @param app - The App entity containing application details and configuration
|
||||||
* @returns A React functional component for plugin settings/configuration,
|
* @returns A React functional component for the complete app details view,
|
||||||
* or null if no configuration is needed.
|
* or null if the default tabs interface should be used.
|
||||||
*/
|
*/
|
||||||
getConfigComponent?(app: App): FC | null;
|
getAppDetails?(app: App): FC | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional method that provides custom routes for the plugin.
|
* Optional method that provides custom routes for the plugin.
|
||||||
@ -63,4 +65,15 @@ export interface AppPlugin {
|
|||||||
* left sidebar when the plugin is active.
|
* left sidebar when the plugin is active.
|
||||||
*/
|
*/
|
||||||
getSidebarActions?(): Array<LeftSidebarItemExample>;
|
getSidebarActions?(): Array<LeftSidebarItemExample>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional method that returns a React component for the app installation page.
|
||||||
|
* If provided, this component will replace the default stepper and tabs interface
|
||||||
|
* in the App Installation page.
|
||||||
|
*
|
||||||
|
* @param app - The AppMarketPlaceDefinition entity containing application details and configuration
|
||||||
|
* @returns A React functional component for the complete app installation view,
|
||||||
|
* or null if the default installation interface should be used.
|
||||||
|
*/
|
||||||
|
getAppInstallComponent?(app: AppMarketPlaceDefinition): FC | null;
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ import { RJSFSchema } from '@rjsf/utils';
|
|||||||
import { Col, Row, Typography } from 'antd';
|
import { Col, Row, Typography } from 'antd';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||||
@ -27,6 +27,7 @@ import {
|
|||||||
} from '../../components/Settings/Applications/AppDetails/ApplicationsClassBase';
|
} from '../../components/Settings/Applications/AppDetails/ApplicationsClassBase';
|
||||||
import AppInstallVerifyCard from '../../components/Settings/Applications/AppInstallVerifyCard/AppInstallVerifyCard.component';
|
import AppInstallVerifyCard from '../../components/Settings/Applications/AppInstallVerifyCard/AppInstallVerifyCard.component';
|
||||||
import ApplicationConfiguration from '../../components/Settings/Applications/ApplicationConfiguration/ApplicationConfiguration';
|
import ApplicationConfiguration from '../../components/Settings/Applications/ApplicationConfiguration/ApplicationConfiguration';
|
||||||
|
import { AppPlugin } from '../../components/Settings/Applications/plugins/AppPlugin';
|
||||||
import ScheduleInterval from '../../components/Settings/Services/AddIngestion/Steps/ScheduleInterval';
|
import ScheduleInterval from '../../components/Settings/Services/AddIngestion/Steps/ScheduleInterval';
|
||||||
import { WorkflowExtraConfig } from '../../components/Settings/Services/AddIngestion/Steps/ScheduleInterval.interface';
|
import { WorkflowExtraConfig } from '../../components/Settings/Services/AddIngestion/Steps/ScheduleInterval.interface';
|
||||||
import IngestionStepper from '../../components/Settings/Services/Ingestion/IngestionStepper/IngestionStepper.component';
|
import IngestionStepper from '../../components/Settings/Services/Ingestion/IngestionStepper/IngestionStepper.component';
|
||||||
@ -65,6 +66,7 @@ const AppInstall = () => {
|
|||||||
const [activeServiceStep, setActiveServiceStep] = useState(1);
|
const [activeServiceStep, setActiveServiceStep] = useState(1);
|
||||||
const [appConfiguration, setAppConfiguration] = useState();
|
const [appConfiguration, setAppConfiguration] = useState();
|
||||||
const [jsonSchema, setJsonSchema] = useState<RJSFSchema>();
|
const [jsonSchema, setJsonSchema] = useState<RJSFSchema>();
|
||||||
|
const [pluginComponent, setPluginComponent] = useState<FC | null>(null);
|
||||||
const { config, getResourceLimit } = useLimitStore();
|
const { config, getResourceLimit } = useLimitStore();
|
||||||
|
|
||||||
const { pipelineSchedules } =
|
const { pipelineSchedules } =
|
||||||
@ -112,6 +114,20 @@ const AppInstall = () => {
|
|||||||
const schema = await applicationSchemaClassBase.importSchema(fqn);
|
const schema = await applicationSchemaClassBase.importSchema(fqn);
|
||||||
|
|
||||||
setJsonSchema(schema);
|
setJsonSchema(schema);
|
||||||
|
|
||||||
|
// Check if this app has a plugin with a custom install component
|
||||||
|
if (data.name) {
|
||||||
|
const PluginClass = applicationsClassBase.appPluginRegistry[data.name];
|
||||||
|
if (PluginClass) {
|
||||||
|
const pluginInstance: AppPlugin = new PluginClass(data.name, false);
|
||||||
|
if (pluginInstance.getAppInstallComponent) {
|
||||||
|
const Component = pluginInstance.getAppInstallComponent(data);
|
||||||
|
if (Component) {
|
||||||
|
setPluginComponent(() => Component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
showErrorToast(
|
showErrorToast(
|
||||||
t('message.no-application-schema-found', { appName: fqn })
|
t('message.no-application-schema-found', { appName: fqn })
|
||||||
@ -260,6 +276,10 @@ const AppInstall = () => {
|
|||||||
<PageLayoutV1
|
<PageLayoutV1
|
||||||
className="app-install-page"
|
className="app-install-page"
|
||||||
pageTitle={t('label.application-plural')}>
|
pageTitle={t('label.application-plural')}>
|
||||||
|
{pluginComponent ? (
|
||||||
|
// Render plugin's custom app details component
|
||||||
|
React.createElement(pluginComponent)
|
||||||
|
) : (
|
||||||
<Row gutter={[0, 16]}>
|
<Row gutter={[0, 16]}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<IngestionStepper
|
<IngestionStepper
|
||||||
@ -271,6 +291,7 @@ const AppInstall = () => {
|
|||||||
{renderSelectedTab}
|
{renderSelectedTab}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
)}
|
||||||
</PageLayoutV1>
|
</PageLayoutV1>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user