From 4e00846eecab42924aaba3a9c8347087c47047c7 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Tue, 5 May 2026 11:01:29 +0530 Subject: [PATCH 1/3] Refactor TypeScript types and improve type safety across components - Updated various components to replace `any` with more specific types, enhancing type safety and clarity. - Modified `DomainTreeView` to include `loadDomains` in dependency array for `useEffect`. - Improved test files by removing unnecessary `eslint-disable` comments and replacing `any` with appropriate types. - Refined `NodeSuggestions` to use a more specific type for `selectRef`. - Changed `WorkflowHistory` interface to use `unknown` instead of `any` for `variables`. - Enhanced `ServiceConnectionDetails` and `TeamsSelectableNew` components to use `unknown` for state management. - Updated utility functions and context definitions to replace `any` with `unknown`, ensuring better type safety. - Removed redundant code and improved readability in `getKeyValues` function in `ServiceConnectionDetailsUtils`. - General cleanup of comments and code structure for better maintainability. --- .../ui/eslint-rules/CONFIGURATION.md | 318 ++++++++++++++ .../resources/ui/eslint-rules/QUICK_START.md | 145 +++++++ .../main/resources/ui/eslint-rules/README.md | 256 ++++++++++++ .../main/resources/ui/eslint-rules/index.js | 21 + .../ui/eslint-rules/no-duplicate-api-calls.js | 293 +++++++++++++ .../ui/eslint-rules/test-example.tsx | 62 +++ .../src/main/resources/ui/eslint.config.mjs | 26 +- .../ActivityFeed/FeedEditor/FeedEditor.tsx | 4 +- .../DataProductsPage.component.tsx | 56 +-- .../components/DomainTreeView.tsx | 2 +- .../NodeChildren.component.test.tsx | 24 +- .../NodeSuggestions.component.tsx | 2 +- .../WorkFlowTab/WorkFlowHistory.interface.ts | 2 +- .../KnowledgeGraph/KnowledgeGraph.test.tsx | 2 + .../MyData/RightSidebar/FollowingWidget.tsx | 2 +- .../FilterConfiguration.tsx | 18 +- .../SearchSettings/TermBoost/TermBoost.tsx | 2 +- .../ServiceConnectionDetails.component.tsx | 2 +- .../TeamsSelectable/TeamsSelectableNew.tsx | 4 +- .../UserProfileRoles.component.tsx | 2 +- .../common/ListView/ListView.component.tsx | 2 +- .../atoms/asyncTreeSelect/useTreeData.tsx | 46 +-- .../ui/src/contexts/WorkflowModeContext.tsx | 2 +- .../main/resources/ui/src/hooks/usePubSub.ts | 7 +- .../src/interface/data-insight.interface.ts | 2 +- .../ui/src/pages/SwaggerPage/RapiDocReact.tsx | 20 +- .../TokenService/TokenServiceUtil.test.ts | 12 +- .../ui/src/utils/CSV/CSVUtilsClassBase.tsx | 27 +- .../CustomizeNavigation.test.ts | 2 +- .../src/utils/EntitySummaryPanelUtilsV1.tsx | 4 +- .../ui/src/utils/ExtensionPointRegistry.ts | 2 +- .../ui/src/utils/QueryBuilderUtils.tsx | 4 +- .../utils/ServiceConnectionDetailsUtils.tsx | 391 ++++++++++-------- .../ui/src/utils/WorkflowConfigUtils.ts | 2 +- 34 files changed, 1457 insertions(+), 309 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/eslint-rules/CONFIGURATION.md create mode 100644 openmetadata-ui/src/main/resources/ui/eslint-rules/QUICK_START.md create mode 100644 openmetadata-ui/src/main/resources/ui/eslint-rules/README.md create mode 100644 openmetadata-ui/src/main/resources/ui/eslint-rules/index.js create mode 100644 openmetadata-ui/src/main/resources/ui/eslint-rules/no-duplicate-api-calls.js create mode 100644 openmetadata-ui/src/main/resources/ui/eslint-rules/test-example.tsx diff --git a/openmetadata-ui/src/main/resources/ui/eslint-rules/CONFIGURATION.md b/openmetadata-ui/src/main/resources/ui/eslint-rules/CONFIGURATION.md new file mode 100644 index 000000000000..a0c0e0b0a2f3 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/eslint-rules/CONFIGURATION.md @@ -0,0 +1,318 @@ +# ESLint Custom Rules Configuration + +## Current Configuration + +### `no-duplicate-api-calls` + +**Status**: โœ… Enabled (as warning) +**Severity**: `warn` +**Location**: [`eslint.config.mjs`](../eslint.config.mjs) + +```javascript +'custom-rules/no-duplicate-api-calls': [ + 'warn', + { + threshold: 2, + checkUseEffect: true, + checkCallbacks: true, + allowedDuplicates: [], + }, +] +``` + +#### Active Configuration + +| Option | Value | Description | +|--------|-------|-------------| +| `threshold` | `2` | Reports when 2+ identical API calls are detected | +| `checkUseEffect` | `true` | Checks for duplicates across multiple `useEffect` hooks | +| `checkCallbacks` | `true` | Checks for duplicates in event handlers and callbacks | +| `allowedDuplicates` | `[]` | No exceptions - all duplicates are reported | + +#### File Exclusions + +The rule is **automatically disabled** for: + +```javascript +// From eslint.config.mjs - Test setup files section +{ + files: [ + 'src/setupTests.js', + 'src/**/*.test.{js,jsx,ts,tsx}', // Jest/Vitest tests + 'src/**/*.spec.{js,jsx,ts,tsx}', // Spec files + 'playwright/**/*.spec.{js,jsx,ts,tsx}', // E2E tests + '**/*.test.{js,jsx,ts,tsx}', // All test files + '**/*.spec.{js,jsx,ts,tsx}', // All spec files + '**/__tests__/**', // Test directories + '**/__mocks__/**', // Mock directories + 'eslint-rules/**/*.tsx', // Example files + ], + rules: { + 'custom-rules/no-duplicate-api-calls': 'off', + }, +} +``` + +**Why these exclusions?** +- Test files and mocks legitimately need duplicate API calls to test different scenarios +- Example files are for documentation purposes +- Mocking patterns often require multiple similar calls with different responses + +## Adjusting the Configuration + +### Change Severity + +To make violations block builds (error instead of warning): + +```javascript +'custom-rules/no-duplicate-api-calls': [ + 'error', // Changed from 'warn' + { /* options */ } +] +``` + +To disable completely: + +```javascript +'custom-rules/no-duplicate-api-calls': 'off' +``` + +### Allow Specific Functions + +If a function legitimately needs to be called multiple times: + +```javascript +'custom-rules/no-duplicate-api-calls': [ + 'warn', + { + threshold: 2, + checkUseEffect: true, + checkCallbacks: true, + allowedDuplicates: [ + 'fetchConfigData', // Config might be fetched multiple times + 'logAnalyticsEvent', // Analytics can fire multiple times + 'validateToken', // Token validation might be needed in parallel + ], + }, +] +``` + +### Increase Threshold + +To only report when 3+ duplicates are detected: + +```javascript +'custom-rules/no-duplicate-api-calls': [ + 'warn', + { + threshold: 3, // Changed from 2 + // ... other options + }, +] +``` + +### Disable Specific Checks + +To disable checking in event handlers but keep useEffect checking: + +```javascript +'custom-rules/no-duplicate-api-calls': [ + 'warn', + { + threshold: 2, + checkUseEffect: true, + checkCallbacks: false, // Disabled + allowedDuplicates: [], + }, +] +``` + +## Per-File Overrides + +To disable for specific files or directories: + +```javascript +// Add to eslint.config.mjs +{ + files: ['src/legacy/**/*.tsx'], // Legacy code + rules: { + 'custom-rules/no-duplicate-api-calls': 'off', + }, +} +``` + +## Inline Suppressions + +In rare cases, suppress in the code itself: + +```typescript +// Disable for next line +// eslint-disable-next-line custom-rules/no-duplicate-api-calls +searchQuery({ query: '*' }).then(handleSpecialCase); + +// Disable for entire file (at top of file) +/* eslint-disable custom-rules/no-duplicate-api-calls */ + +// Disable for a block +/* eslint-disable custom-rules/no-duplicate-api-calls */ +const handleMultipleCalls = () => { + searchQuery({ query: '*' }).then(handler1); + searchQuery({ query: '*' }).then(handler2); +}; +/* eslint-enable custom-rules/no-duplicate-api-calls */ +``` + +## Monitoring and Reports + +### Check Current Violations + +```bash +# Check entire codebase +yarn lint + +# Check specific directory +yarn lint src/components/ + +# Check with auto-fix (where possible) +yarn lint --fix + +# Get detailed report +yarn lint --format json > lint-report.json +``` + +### Filter for This Rule Only + +```bash +# Show only duplicate API call warnings +yarn lint 2>&1 | grep "no-duplicate-api-calls" + +# Count violations +yarn lint 2>&1 | grep -c "no-duplicate-api-calls" +``` + +## Best Practices + +### 1. Run Before Committing + +Add to your git hooks or CI/CD pipeline: + +```json +// package.json +{ + "scripts": { + "precommit": "yarn lint", + "ci:lint": "yarn lint --max-warnings 0" + } +} +``` + +### 2. Gradual Adoption + +If you have many existing violations: + +1. Start with `'warn'` to identify issues +2. Fix violations incrementally +3. Switch to `'error'` when codebase is clean +4. Increase `threshold` temporarily if needed + +### 3. Team Communication + +- Document your `allowedDuplicates` with comments explaining why +- Share patterns for custom hooks to avoid duplicates +- Review violations in code review + +## Migration Guide + +### From Existing Codebase + +If enabling this rule on existing code with violations: + +```javascript +// Step 1: Start with warnings and high threshold +'custom-rules/no-duplicate-api-calls': [ + 'warn', + { threshold: 5 }, // Only report obvious problems +] + +// Step 2: Lower threshold as you fix issues +'custom-rules/no-duplicate-api-calls': [ + 'warn', + { threshold: 3 }, +] + +// Step 3: Final configuration +'custom-rules/no-duplicate-api-calls': [ + 'error', + { threshold: 2 }, +] +``` + +### Common Refactoring Patterns + +When fixing violations, use these patterns: + +**Pattern 1: Custom Hook** +```typescript +// Before: Duplicate calls +const Component1 = () => { + useEffect(() => { fetchUsers().then(setData); }, []); +}; +const Component2 = () => { + useEffect(() => { fetchUsers().then(setData); }, []); +}; + +// After: Shared hook +const useUsers = () => { + const [data, setData] = useState([]); + useEffect(() => { fetchUsers().then(setData); }, []); + return data; +}; +``` + +**Pattern 2: Context Provider** +```typescript +// Create provider for shared data +const DataContext = createContext(); +const DataProvider = ({ children }) => { + const [data, setData] = useState([]); + useEffect(() => { fetchData().then(setData); }, []); + return {children}; +}; +``` + +**Pattern 3: React Query / SWR** +```typescript +// Before: Manual fetching +useEffect(() => { fetchData().then(setData); }, []); + +// After: With caching library +const { data } = useQuery('dataKey', fetchData); +``` + +## Troubleshooting + +### Rule Not Triggering + +1. Check the file is not excluded (see [File Exclusions](#file-exclusions)) +2. Verify the API call matches detected patterns +3. Ensure rule is enabled in `eslint.config.mjs` + +### False Positives + +1. Add function to `allowedDuplicates` +2. Use inline suppression with comment explaining why +3. Report issue if pattern should be excluded globally + +### Performance Issues + +If linting is slow: + +1. Ensure you're not linting `node_modules/` +2. Check ignore patterns in `eslint.config.mjs` +3. Consider increasing `threshold` to reduce checks + +## Support + +- **Documentation**: [README.md](./README.md) +- **Examples**: [test-example.tsx](./test-example.tsx) +- **Rule Source**: [no-duplicate-api-calls.js](./no-duplicate-api-calls.js) diff --git a/openmetadata-ui/src/main/resources/ui/eslint-rules/QUICK_START.md b/openmetadata-ui/src/main/resources/ui/eslint-rules/QUICK_START.md new file mode 100644 index 000000000000..91fa587cfd9a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/eslint-rules/QUICK_START.md @@ -0,0 +1,145 @@ +# Quick Start: No Duplicate API Calls Rule + +## TL;DR + +โœ… **Rule is active** - warns when the same API call appears 2+ times in a component +๐Ÿงช **Test files excluded** - rule automatically disabled for `*.test.tsx`, `*.spec.tsx`, `__tests__/`, `__mocks__/` +โš™๏ธ **Configurable** - adjust threshold, add exceptions, change severity + +## What Gets Detected + +```typescript +// โŒ BAD - Will trigger warning +export const MyComponent = () => { + useEffect(() => { + searchQuery({ query: '*', pageSize: 10 }).then(setData1); + }, []); + + useEffect(() => { + searchQuery({ query: '*', pageSize: 10 }).then(setData2); // Duplicate! + }, []); + + return
...
; +}; +``` + +## How to Fix + +### Option 1: Single Call + Shared State + +```typescript +// โœ… GOOD +export const MyComponent = () => { + const [data, setData] = useState([]); + + useEffect(() => { + searchQuery({ query: '*', pageSize: 10 }).then(setData); + }, []); + + // Use data in multiple places + return
...
; +}; +``` + +### Option 2: Custom Hook + +```typescript +// โœ… BETTER +const useSearchData = (query: string) => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + setLoading(true); + searchQuery({ query, pageSize: 10 }) + .then(setData) + .finally(() => setLoading(false)); + }, [query]); + + return { data, loading }; +}; + +export const MyComponent = () => { + const { data, loading } = useSearchData('*'); + return
...
; +}; +``` + +### Option 3: React Query / SWR (Recommended) + +```typescript +// โœ… BEST - Automatic caching & deduplication +export const MyComponent = () => { + const { data, isLoading } = useQuery( + ['search', '*'], + () => searchQuery({ query: '*', pageSize: 10 }) + ); + + return
...
; +}; +``` + +## Quick Commands + +```bash +# Check your code +yarn lint + +# Check specific file +yarn lint path/to/file.tsx + +# Auto-fix (where possible) +yarn lint --fix + +# Check only this rule +yarn lint 2>&1 | grep "no-duplicate-api-calls" +``` + +## Suppressing When Needed + +```typescript +// For one line +// eslint-disable-next-line custom-rules/no-duplicate-api-calls +searchQuery({ query: '*' }).then(handleSpecial); + +// For entire file (add at top) +/* eslint-disable custom-rules/no-duplicate-api-calls */ +``` + +## Need Help? + +- **Full docs**: [README.md](./README.md) +- **Configuration**: [CONFIGURATION.md](./CONFIGURATION.md) +- **Examples**: [test-example.tsx](./test-example.tsx) + +## Common Questions + +**Q: Why is this a problem?** +A: Duplicate API calls waste network bandwidth, slow down your app, can cause race conditions, and create inconsistent state. + +**Q: What if I need the same endpoint twice with different purposes?** +A: That's likely still a duplicate. Consider: +1. Fetching once and sharing the data +2. Using a caching library (React Query, SWR) +3. If truly different, add the function to `allowedDuplicates` in config + +**Q: The rule isn't triggering on my test file** +A: Correct! Test files are intentionally excluded. Tests often need duplicate calls to test different scenarios. + +**Q: Can I disable this for my component?** +A: Yes, but only if you have a good reason: +```typescript +// eslint-disable-next-line custom-rules/no-duplicate-api-calls +``` +Consider if refactoring would be better. + +**Q: How do I add an exception for a specific API function?** +A: Edit `eslint.config.mjs` and add to `allowedDuplicates`: +```javascript +'custom-rules/no-duplicate-api-calls': [ + 'warn', + { + allowedDuplicates: ['mySpecialFunction'], + }, +] +``` diff --git a/openmetadata-ui/src/main/resources/ui/eslint-rules/README.md b/openmetadata-ui/src/main/resources/ui/eslint-rules/README.md new file mode 100644 index 000000000000..e610d86d4de9 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/eslint-rules/README.md @@ -0,0 +1,256 @@ +# Custom ESLint Rules for OpenMetadata UI + +This directory contains custom ESLint rules specifically designed for the OpenMetadata UI codebase. + +## Available Rules + +### `no-duplicate-api-calls` + +**Type:** Problem +**Severity:** Warning (configurable) +**Category:** Best Practices + +#### Description + +Detects duplicate API calls within the same React component or file. This anti-pattern can cause: + +- **Performance issues**: Unnecessary network requests slow down the application +- **Wasted bandwidth**: Multiple identical requests consume network resources +- **Race conditions**: Multiple concurrent requests to the same endpoint can lead to inconsistent state +- **Poor user experience**: Delayed responses and flickering UI +- **Increased server load**: Redundant requests put unnecessary load on backend services + +#### Examples + +โŒ **Bad** - Duplicate API calls in the same component: + +```typescript +export const BadComponent = () => { + const [users, setUsers] = useState([]); + const [teams, setTeams] = useState([]); + + // First call + useEffect(() => { + searchQuery({ + query: '*', + pageNumber: 1, + pageSize: 10, + searchIndex: 'user_search_index', + }).then((res) => setUsers(res.data)); + }, []); + + // Duplicate call with same parameters! + useEffect(() => { + searchQuery({ + query: '*', + pageNumber: 1, + pageSize: 10, + searchIndex: 'user_search_index', + }).then((res) => setTeams(res.data)); + }, []); + + return
...
; +}; +``` + +โœ… **Good** - Single API call with shared state: + +```typescript +export const GoodComponent = () => { + const [data, setData] = useState([]); + + // Single call + useEffect(() => { + searchQuery({ + query: '*', + pageNumber: 1, + pageSize: 10, + searchIndex: 'user_search_index', + }).then((res) => setData(res.data)); + }, []); + + return
...
; +}; +``` + +โœ… **Better** - Custom hook for reusability: + +```typescript +// Create a custom hook +const useSearchData = (index: string) => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + setLoading(true); + searchQuery({ + query: '*', + pageNumber: 1, + pageSize: 10, + searchIndex: index, + }) + .then((res) => setData(res.data)) + .finally(() => setLoading(false)); + }, [index]); + + return { data, loading }; +}; + +// Use in component +export const BestComponent = () => { + const { data: users } = useSearchData('user_search_index'); + const { data: tables } = useSearchData('table_search_index'); + + return
...
; +}; +``` + +#### Configuration + +```javascript +{ + 'custom-rules/no-duplicate-api-calls': [ + 'warn', // or 'error' + { + threshold: 2, // Minimum number of calls before reporting (default: 2) + checkUseEffect: true, // Check across useEffect hooks (default: true) + checkCallbacks: true, // Check in event handlers (default: true) + allowedDuplicates: ['fetchConfig'] // Functions allowed to be called multiple times + } + ] +} +``` + +#### Options + +- **`threshold`** (number, default: `2`): Number of duplicate calls before reporting. Set to `2` to report any duplicate. + +- **`checkUseEffect`** (boolean, default: `true`): Whether to check for duplicates across multiple `useEffect` hooks in the same component. + +- **`checkCallbacks`** (boolean, default: `true`): Whether to check for duplicates in event handlers and callbacks. + +- **`allowedDuplicates`** (string[], default: `[]`): List of API function names that are allowed to be called multiple times. Use this for legitimate cases where the same API must be called multiple times with different purposes. + +#### Detected API Call Patterns + +The rule detects these common API call patterns: + +- `searchQuery()` - OpenMetadata search API +- `fetch()` - Browser Fetch API +- `axios.*` - Axios HTTP client +- `APIClient.*` - Custom API client +- `get*ByName()` - Entity getter by name +- `get*ById()` - Entity getter by ID +- `get*ByFqn()` - Entity getter by FQN +- `fetch*()` - Fetch-prefixed functions +- `load*()` - Load-prefixed functions +- `create*()` - Create operations +- `update*()` - Update operations +- `delete*()` - Delete operations +- `patch*()` - Patch operations + +#### Automatic Exclusions + +The rule is automatically disabled for: + +- **Test files**: `*.test.{js,jsx,ts,tsx}`, `*.spec.{js,jsx,ts,tsx}` +- **Test directories**: `__tests__/**`, `__mocks__/**` +- **Playwright tests**: `playwright/**/*.spec.{js,jsx,ts,tsx}` +- **Mock files**: Files in `eslint-rules/` for examples and testing + +This is intentional because test files and mocks often need to make duplicate API calls to test different scenarios. + +#### When to Suppress + +You may want to suppress this rule in specific cases: + +```typescript +// Different purposes - these are NOT duplicates +useEffect(() => { + // Initial load + searchQuery({ query: '*', pageNumber: 1 }).then(setData); +}, []); + +useEffect(() => { + // Refresh on specific trigger + if (shouldRefresh) { + searchQuery({ query: searchTerm, pageNumber: 1 }).then(setData); + } +}, [shouldRefresh, searchTerm]); +``` + +```typescript +// Intentional duplicate with different handling +const handlePrimaryAction = () => { + // eslint-disable-next-line custom-rules/no-duplicate-api-calls + fetchData().then(handlePrimary); +}; + +const handleSecondaryAction = () => { + // Different callback, legitimate use case + // eslint-disable-next-line custom-rules/no-duplicate-api-calls + fetchData().then(handleSecondary); +}; +``` + +## Implementation Details + +### How It Works + +1. The rule traverses the AST (Abstract Syntax Tree) of your component +2. It tracks all function declarations and identifies React components (functions starting with uppercase) +3. For each component, it collects all API call expressions +4. It generates a signature for each call based on: + - Function name (e.g., `searchQuery`) + - First argument (if it's a string or object literal) +5. When the same signature appears multiple times, it reports a warning + +### Limitations + +- The rule uses static analysis and may not detect dynamically constructed API calls +- It focuses on literal arguments and may miss duplicates with computed values +- Cross-file duplicates are not detected (by design - different components may legitimately make the same calls) + +## Development + +### Testing the Rule + +```bash +# Test on a specific file +yarn eslint path/to/component.tsx + +# Test on all source files +yarn eslint src/ + +# Auto-fix (where possible) +yarn eslint src/ --fix +``` + +### Adding New API Patterns + +To detect additional API call patterns, edit `eslint-rules/no-duplicate-api-calls.js` and add patterns to the `apiCallPatterns` array: + +```javascript +const apiCallPatterns = [ + 'searchQuery', + 'fetch', + /myCustomApi[A-Z]\w+/, // RegExp for pattern matching + // Add your patterns here +]; +``` + +## Contributing + +When adding new custom rules: + +1. Create a new file in `eslint-rules/` with the rule name +2. Export a rule object with `meta` and `create` properties +3. Add the rule to `eslint-rules/index.js` +4. Update `eslint.config.mjs` to enable the rule +5. Document the rule in this README + +## References + +- [ESLint Custom Rules Documentation](https://eslint.org/docs/latest/extend/custom-rules) +- [AST Explorer](https://astexplorer.net/) - Tool for exploring JavaScript AST +- [OpenMetadata Contribution Guidelines](../../../../../../../CONTRIBUTING.md) diff --git a/openmetadata-ui/src/main/resources/ui/eslint-rules/index.js b/openmetadata-ui/src/main/resources/ui/eslint-rules/index.js new file mode 100644 index 000000000000..3d68b133ac95 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/eslint-rules/index.js @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Custom ESLint rules for OpenMetadata UI + */ +module.exports = { + rules: { + 'no-duplicate-api-calls': require('./no-duplicate-api-calls'), + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/eslint-rules/no-duplicate-api-calls.js b/openmetadata-ui/src/main/resources/ui/eslint-rules/no-duplicate-api-calls.js new file mode 100644 index 000000000000..b64634afffef --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/eslint-rules/no-duplicate-api-calls.js @@ -0,0 +1,293 @@ +/* + * 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. + */ + +/** + * ESLint rule to detect duplicate API calls within the same React component or file. + * This is an anti-pattern that can cause: + * - Performance issues + * - Unnecessary network traffic + * - Race conditions + * - Inconsistent state management + * + * @type {import('eslint').Rule.RuleModule} + */ +module.exports = { + meta: { + type: 'problem', + docs: { + description: + 'Disallow duplicate API calls in the same component/file (anti-pattern)', + category: 'Best Practices', + recommended: true, + }, + messages: { + duplicateApiCall: + 'Duplicate API call detected: "{{ apiCall }}" is called {{ count }} times in this {{ scope }}. Consider consolidating these calls or using shared state/caching.', + duplicateEndpoint: + 'Duplicate endpoint detected: "{{ endpoint }}" is called from {{ count }} different locations in this {{ scope }}. Consider creating a custom hook or service.', + }, + schema: [ + { + type: 'object', + properties: { + threshold: { + type: 'integer', + minimum: 2, + default: 2, + description: + 'Number of duplicate calls before reporting (default: 2)', + }, + checkUseEffect: { + type: 'boolean', + default: true, + description: + 'Check for duplicates across multiple useEffect hooks (default: true)', + }, + checkCallbacks: { + type: 'boolean', + default: true, + description: + 'Check for duplicates in event handlers and callbacks (default: true)', + }, + allowedDuplicates: { + type: 'array', + items: { + type: 'string', + }, + description: + 'List of API function names that are allowed to be called multiple times', + }, + }, + additionalProperties: false, + }, + ], + }, + + create(context) { + const options = context.options[0] || {}; + const threshold = options.threshold || 2; + const checkUseEffect = options.checkUseEffect !== false; + const checkCallbacks = options.checkCallbacks !== false; + const allowedDuplicates = new Set(options.allowedDuplicates || []); + + const apiCallPatterns = [ + 'searchQuery', + 'fetch', + 'axios', + 'APIClient', + /get[A-Z]\w+ByName/, + /get[A-Z]\w+ById/, + /get[A-Z]\w+ByFqn/, + /fetch[A-Z]\w+/, + /load[A-Z]\w+/, + /create[A-Z]\w+/, + /update[A-Z]\w+/, + /delete[A-Z]\w+/, + /patch[A-Z]\w+/, + ]; + + function isApiCall(node) { + if (node.type === 'CallExpression') { + const callee = node.callee; + + if (callee.type === 'Identifier') { + const name = callee.name; + + return apiCallPatterns.some((pattern) => { + if (typeof pattern === 'string') { + return name === pattern; + } + + return pattern.test(name); + }); + } + + if (callee.type === 'MemberExpression') { + const objectName = callee.object.name; + const propertyName = callee.property.name; + const fullName = `${objectName}.${propertyName}`; + + return ( + objectName === 'axios' || + objectName === 'APIClient' || + ['get', 'post', 'put', 'patch', 'delete'].includes(propertyName) || + apiCallPatterns.some((pattern) => { + if (typeof pattern === 'string') { + return fullName.includes(pattern); + } + + return pattern.test(propertyName); + }) + ); + } + } + + return false; + } + + function getCallSignature(node) { + const callee = node.callee; + let signature = ''; + + if (callee.type === 'Identifier') { + signature = callee.name; + } else if (callee.type === 'MemberExpression') { + const obj = callee.object.name || ''; + const prop = callee.property.name || ''; + signature = `${obj}.${prop}`; + } + + if (node.arguments.length > 0) { + const firstArg = node.arguments[0]; + if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') { + signature += `("${firstArg.value}")`; + } else if (firstArg.type === 'TemplateLiteral') { + const parts = firstArg.quasis.map((q) => q.value.cooked).join('${...}'); + signature += `(\`${parts}\`)`; + } else if (firstArg.type === 'ObjectExpression') { + const props = firstArg.properties + .slice(0, 2) + .map((p) => { + if (p.key) { + return p.key.name || p.key.value; + } + + return ''; + }) + .filter(Boolean); + signature += `({${props.join(', ')}${firstArg.properties.length > 2 ? ', ...' : ''}})`; + } + } + + return signature; + } + + function getApiFunction(node) { + const callee = node.callee; + if (callee.type === 'Identifier') { + return callee.name; + } else if (callee.type === 'MemberExpression') { + return callee.property.name || ''; + } + + return ''; + } + + let currentComponent = null; + const componentStack = []; + + function checkDuplicates(callsMap, scope) { + for (const [signature, data] of callsMap.entries()) { + if (data.count >= threshold) { + data.nodes.forEach((node, index) => { + if (index > 0) { + context.report({ + node, + messageId: 'duplicateApiCall', + data: { + apiCall: signature, + count: data.count, + scope, + }, + }); + } + }); + } + } + } + + return { + 'FunctionDeclaration, FunctionExpression, ArrowFunctionExpression'(node) { + const parent = node.parent; + const isComponent = + (parent.type === 'VariableDeclarator' && + parent.id.name && + /^[A-Z]/.test(parent.id.name)) || + (node.type === 'FunctionDeclaration' && + node.id && + /^[A-Z]/.test(node.id.name)) || + (parent.type === 'Property' && + parent.key.name && + /^[A-Z]/.test(parent.key.name)); + + if (isComponent) { + componentStack.push({ + name: + node.id?.name || + parent.id?.name || + parent.key?.name || + 'Anonymous', + calls: new Map(), + }); + currentComponent = componentStack[componentStack.length - 1]; + } + }, + + 'FunctionDeclaration, FunctionExpression, ArrowFunctionExpression:exit'( + node + ) { + const parent = node.parent; + const isComponent = + (parent.type === 'VariableDeclarator' && + parent.id.name && + /^[A-Z]/.test(parent.id.name)) || + (node.type === 'FunctionDeclaration' && + node.id && + /^[A-Z]/.test(node.id.name)) || + (parent.type === 'Property' && + parent.key.name && + /^[A-Z]/.test(parent.key.name)); + + if (isComponent && componentStack.length > 0) { + const component = componentStack.pop(); + checkDuplicates(component.calls, `component "${component.name}"`); + + if (componentStack.length === 0) { + currentComponent = null; + } else { + currentComponent = componentStack[componentStack.length - 1]; + } + } + }, + + CallExpression(node) { + if (isApiCall(node)) { + if (currentComponent) { + if (!currentComponent.calls.has('global')) { + currentComponent.calls.set('global', new Map()); + } + const callsMap = currentComponent.calls.get('global'); + const signature = getCallSignature(node); + const apiFunction = getApiFunction(node); + + if (allowedDuplicates.has(apiFunction)) { + return; + } + + if (!callsMap.has(signature)) { + callsMap.set(signature, { + count: 0, + nodes: [], + apiFunction, + }); + } + + const entry = callsMap.get(signature); + entry.count++; + entry.nodes.push(node); + } + } + }, + }; + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/eslint-rules/test-example.tsx b/openmetadata-ui/src/main/resources/ui/eslint-rules/test-example.tsx new file mode 100644 index 000000000000..a7ebdfd6769b --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/eslint-rules/test-example.tsx @@ -0,0 +1,62 @@ +/* + * 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 { useEffect, useState } from 'react'; +import { searchQuery } from '../rest/searchAPI'; + +// This is a test file to demonstrate the no-duplicate-api-calls rule + +// BAD EXAMPLE - This will trigger warnings +export const BadComponent = () => { + const [_data1, _setData1] = useState([]); + const [_data2, _setData2] = useState([]); + + useEffect(() => { + // First call to searchQuery + searchQuery({ + query: '*', + pageNumber: 1, + pageSize: 10, + searchIndex: 'table_search_index', + }).then((res) => _setData1(res.data)); + }, []); + + useEffect(() => { + // Duplicate call to searchQuery with same parameters + searchQuery({ + query: '*', + pageNumber: 1, + pageSize: 10, + searchIndex: 'table_search_index', + }).then((res) => _setData2(res.data)); + }, []); + + return
Bad Component
; +}; + +// GOOD EXAMPLE - Single API call with shared state +export const GoodComponent = () => { + const [_data, _setData] = useState([]); + + useEffect(() => { + // Single call to searchQuery + searchQuery({ + query: '*', + pageNumber: 1, + pageSize: 10, + searchIndex: 'table_search_index', + }).then((res) => _setData(res.data)); + }, []); + + return
Good Component
; +}; diff --git a/openmetadata-ui/src/main/resources/ui/eslint.config.mjs b/openmetadata-ui/src/main/resources/ui/eslint.config.mjs index 6017f475f94b..a755a4a18bf4 100644 --- a/openmetadata-ui/src/main/resources/ui/eslint.config.mjs +++ b/openmetadata-ui/src/main/resources/ui/eslint.config.mjs @@ -22,6 +22,7 @@ import reactHooks from 'eslint-plugin-react-hooks'; import globals from 'globals'; import jsoncParser from 'jsonc-eslint-parser'; import tseslint from 'typescript-eslint'; +import customRules from './eslint-rules/index.js'; export default [ // Base recommended configs @@ -98,6 +99,7 @@ export default [ jest, 'jest-formatting': jestFormatting, i18next, + 'custom-rules': customRules, }, rules: { @@ -185,11 +187,20 @@ export default [ varsIgnorePattern: '^_', }, ], - '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-explicit-any': 'error', - // i18next rules - temporarily disabled due to ESLint 9 compatibility issues - // TODO: Re-enable when eslint-plugin-i18next fully supports ESLint 9 flat config - 'i18next/no-literal-string': 'off', + // Custom rules for OpenMetadata + // Detects duplicate API calls in the same component (anti-pattern) + // Automatically disabled for test files (see test files config below) + 'custom-rules/no-duplicate-api-calls': [ + 'warn', + { + threshold: 2, + checkUseEffect: true, + checkCallbacks: true, + allowedDuplicates: [], + }, + ], }, }, @@ -277,9 +288,16 @@ export default [ 'src/**/*.test.{js,jsx,ts,tsx}', 'src/**/*.spec.{js,jsx,ts,tsx}', 'playwright/**/*.spec.{js,jsx,ts,tsx}', + '**/*.test.{js,jsx,ts,tsx}', + '**/*.spec.{js,jsx,ts,tsx}', + '**/__tests__/**', + '**/__mocks__/**', + 'eslint-rules/**/*.tsx', ], rules: { '@typescript-eslint/no-require-imports': 'off', + 'custom-rules/no-duplicate-api-calls': 'off', + '@typescript-eslint/no-explicit-any': 'off', }, }, ]; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/FeedEditor/FeedEditor.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/FeedEditor/FeedEditor.tsx index 8bebde552234..f4b21841828a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/FeedEditor/FeedEditor.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/FeedEditor/FeedEditor.tsx @@ -224,9 +224,9 @@ export const FeedEditor = forwardRef( toggleMentionList(true); }, onSelect: ( - item: Record, + item: Record, - insertItem: (item: Record) => void + insertItem: (item: Record) => void ) => { toggleMentionList(true); insertItem(item); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsPage/DataProductsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsPage/DataProductsPage.component.tsx index 93d4bba22349..6b8b0a3a017f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsPage/DataProductsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataProducts/DataProductsPage/DataProductsPage.component.tsx @@ -117,6 +117,34 @@ const DataProductsPage = () => { } }; + const fetchVersionsInfo = async (activeDataProduct: DataProduct) => { + if (!activeDataProduct) { + return; + } + + try { + const res = await getDataProductVersionsList(activeDataProduct.id); + setVersionList(res); + } catch (error) { + showErrorToast(error as AxiosError); + } + }; + + const fetchActiveVersion = async (activeDataProduct: DataProduct) => { + if (!activeDataProduct) { + return; + } + try { + const res = await getDataProductVersionData( + activeDataProduct.id, + version + ); + setSelectedVersionData(res); + } catch (error) { + showErrorToast(error as AxiosError); + } + }; + const fetchDataProductByFqn = async (fqn: string) => { setIsMainContentLoading(true); try { @@ -147,34 +175,6 @@ const DataProductsPage = () => { } }; - const fetchVersionsInfo = async (activeDataProduct: DataProduct) => { - if (!activeDataProduct) { - return; - } - - try { - const res = await getDataProductVersionsList(activeDataProduct.id); - setVersionList(res); - } catch (error) { - showErrorToast(error as AxiosError); - } - }; - - const fetchActiveVersion = async (activeDataProduct: DataProduct) => { - if (!activeDataProduct) { - return; - } - try { - const res = await getDataProductVersionData( - activeDataProduct.id, - version - ); - setSelectedVersionData(res); - } catch (error) { - showErrorToast(error as AxiosError); - } - }; - const onVersionChange = (selectedVersion: string) => { const path = getVersionPath( EntityType.DATA_PRODUCT, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/components/DomainTreeView.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/components/DomainTreeView.tsx index d67210b55832..18a85fd2227e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/components/DomainTreeView.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DomainListing/components/DomainTreeView.tsx @@ -201,7 +201,7 @@ const DomainTreeView = ({ loadDomains(firstDomain.fullyQualifiedName as string); } }, - [updateExpansionForFqn, searchQuery] + [updateExpansionForFqn, searchQuery, loadDomains] ); const searchDomain = useCallback( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeChildren/NodeChildren.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeChildren/NodeChildren.component.test.tsx index 4fd4c7e0e776..5b158cf58e11 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeChildren/NodeChildren.component.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeChildren/NodeChildren.component.test.tsx @@ -26,7 +26,7 @@ const mockUpdateColumnsInCurrentPages = jest.fn(); const mockGetTestCaseExecutionSummary = getTestCaseExecutionSummary as jest.Mock; -// eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockNode: any = { id: 'test-id', entityType: EntityType.TABLE, @@ -46,7 +46,7 @@ const mockNode: any = { ], }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any + const mockNodeWithTestSuite: any = { ...mockNode, testSuite: { @@ -335,7 +335,7 @@ describe('NodeChildren Component', () => { describe('Lineage Filter', () => { it('should show only columns with lineage when filter is active', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nodeWithMultipleColumns: any = { ...mockNode, columns: [ @@ -419,7 +419,7 @@ describe('NodeChildren Component', () => { }); it('should apply search on filtered columns when lineage filter is active', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nodeWithMultipleColumns: any = { ...mockNode, columns: [ @@ -568,7 +568,7 @@ describe('NodeChildren Component', () => { describe('CSS Classes and Styling', () => { it('should apply "any-column-selected" class when a column is selected', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockLineageStoreState.selectedColumn = 'test.fqn.column1' as any; render( @@ -601,7 +601,7 @@ describe('NodeChildren Component', () => { }); it('should apply both classes when column is selected and creating edge', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + mockLineageStoreState.selectedColumn = 'test.fqn.column1' as any; mockLineageStoreState.isCreatingEdge = true; @@ -622,7 +622,7 @@ describe('NodeChildren Component', () => { describe('Different Entity Types', () => { it('should render columns for DASHBOARD_DATA_MODEL entity', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const dashboardDataModelNode: any = { ...mockNode, entityType: EntityType.DASHBOARD_DATA_MODEL, @@ -640,7 +640,7 @@ describe('NodeChildren Component', () => { }); it('should render fields for TOPIC entity', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const topicNode: any = { id: 'topic-id', entityType: EntityType.TOPIC, @@ -669,7 +669,7 @@ describe('NodeChildren Component', () => { }); it('should render features for MLMODEL entity', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mlModelNode: any = { id: 'mlmodel-id', entityType: EntityType.MLMODEL, @@ -698,7 +698,7 @@ describe('NodeChildren Component', () => { describe('Edge Cases', () => { it('should handle nodes with empty column names', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nodeWithEmptyNames: any = { ...mockNode, columns: [ @@ -722,7 +722,7 @@ describe('NodeChildren Component', () => { }); it('should handle search with special characters', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nodeWithSpecialChars: any = { ...mockNode, columns: [ @@ -756,7 +756,7 @@ describe('NodeChildren Component', () => { }); it('should handle columns without fullyQualifiedName', () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nodeWithMissingFQN: any = { ...mockNode, columns: [{ name: 'column1', dataType: 'STRING' }], diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeSuggestions.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeSuggestions.component.tsx index a11dcbba6c56..f785c773b432 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeSuggestions.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeSuggestions.component.tsx @@ -51,7 +51,7 @@ const NodeSuggestions: FC = ({ onSelectHandler, }) => { const { t } = useTranslation(); - const selectRef = useRef(null); + const selectRef = useRef<{ focus: () => void } | null>(null); const [data, setData] = useState>([]); const [searchValue, setSearchValue] = useState(''); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/WorkFlowTab/WorkFlowHistory.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/WorkFlowTab/WorkFlowHistory.interface.ts index 77e11a047ae1..47503ac2c87c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/WorkFlowTab/WorkFlowHistory.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/WorkFlowTab/WorkFlowHistory.interface.ts @@ -15,7 +15,7 @@ interface WorkflowStage { startedAt: number; endedAt: number; tasks: string[]; - variables: Record; + variables: Record; } export interface WorkflowHistoryItem { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeGraph/KnowledgeGraph.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeGraph/KnowledgeGraph.test.tsx index a1d5d35e0c7f..f373fe6f3b16 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeGraph/KnowledgeGraph.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeGraph/KnowledgeGraph.test.tsx @@ -66,6 +66,8 @@ jest.mock('../../utils/KnowledgeGraph.utils', () => ({ NODE_HEIGHT: 36, })); +const mockNavigate = jest.fn(); + jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: jest.fn(() => ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/RightSidebar/FollowingWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/RightSidebar/FollowingWidget.tsx index bbcc2a324a93..32bdca71039b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/RightSidebar/FollowingWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/RightSidebar/FollowingWidget.tsx @@ -155,7 +155,7 @@ function FollowingWidget({ return extraInfo; }; - const getEntityIcon = (item: any) => { + const getEntityIcon = (item: { serviceType?: string; name: string; type?: string }) => { if (item.serviceType) { return ( { [] ); + const handleCheckboxChange = (fieldName: string) => { + setCheckedItems((prev) => + prev.includes(fieldName) + ? prev.filter((item) => item !== fieldName) + : [...prev, fieldName] + ); + }; + const menuItems = useMemo( () => ({ items: entityFields.map((field) => ({ @@ -57,17 +65,9 @@ const FilterConfiguration = () => { })), className: 'menu-items', }), - [entityFields, checkedItems] + [entityFields, checkedItems, handleCheckboxChange] ); - const handleCheckboxChange = (fieldName: string) => { - setCheckedItems((prev) => - prev.includes(fieldName) - ? prev.filter((item) => item !== fieldName) - : [...prev, fieldName] - ); - }; - return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SearchSettings/TermBoost/TermBoost.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SearchSettings/TermBoost/TermBoost.tsx index 7bc42b7ec418..5e804bdf11a0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SearchSettings/TermBoost/TermBoost.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/SearchSettings/TermBoost/TermBoost.tsx @@ -109,7 +109,7 @@ const TermBoostComponent: React.FC = ({ } }; - const handleTagChange = (value: string, option: any) => { + const handleTagChange = (value: string, option: { field: string }) => { const updatedData = { ...termBoostData, field: option.field, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConnectionDetails/ServiceConnectionDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConnectionDetails/ServiceConnectionDetails.component.tsx index 1835a5eeda1c..2753e1a52319 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConnectionDetails/ServiceConnectionDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConnectionDetails/ServiceConnectionDetails.component.tsx @@ -48,7 +48,7 @@ const ServiceConnectionDetails = ({ serviceFQN, extraInfo, }: Readonly) => { - const [schema, setSchema] = useState>({}); + const [schema, setSchema] = useState>({}); const [data, setData] = useState(); useEffect(() => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamsSelectable/TeamsSelectableNew.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamsSelectable/TeamsSelectableNew.tsx index 8c9215ac1937..3077fd9bfed2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamsSelectable/TeamsSelectableNew.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamsSelectable/TeamsSelectableNew.tsx @@ -26,7 +26,7 @@ import { showErrorToast } from '../../../../utils/ToastUtils'; import { TagRenderer } from '../../../common/TagRenderer/TagRenderer'; import { TeamsSelectableProps } from './TeamsSelectable.interface'; -const TeamsSelectableNew = forwardRef( +const TeamsSelectableNew = forwardRef( ( { showTeamsAlert, @@ -129,7 +129,7 @@ const TeamsSelectableNew = forwardRef( placeholder={placeholder} placement="bottomLeft" popupClassName="teams-custom-dropdown-class" - ref={ref as any} + ref={ref as React.Ref | undefined} showCheckedStrategy={TreeSelect.SHOW_CHILD} style={{ width: '100%' }} tagRender={TagRenderer} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileRoles/UserProfileRoles.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileRoles/UserProfileRoles.component.tsx index 55c058ce6c10..2cbbaf3e797a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileRoles/UserProfileRoles.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileRoles/UserProfileRoles.component.tsx @@ -288,7 +288,7 @@ const UserProfileRoles = ({ open={isDropdownOpen} options={useRolesOption} popupClassName="roles-custom-dropdown-class" - ref={dropdownRef as any} + ref={dropdownRef as React.Ref | undefined} tagRender={TagRenderer} value={selectedRoles} onChange={setSelectedRoles} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ListView/ListView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ListView/ListView.component.tsx index 884ab3f809ca..afa3408cf425 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ListView/ListView.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ListView/ListView.component.tsx @@ -27,7 +27,7 @@ import Searchbar from '../SearchBarComponent/SearchBar.component'; import Table from '../Table/Table'; import { ListViewOptions, ListViewProps } from './ListView.interface'; -export const ListView = ({ +export const ListView = >({ tableProps, cardRenderer, searchProps: { search, onSearch }, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/asyncTreeSelect/useTreeData.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/asyncTreeSelect/useTreeData.tsx index 81895775de00..f551f46567f7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/asyncTreeSelect/useTreeData.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/asyncTreeSelect/useTreeData.tsx @@ -39,6 +39,29 @@ interface UseTreeDataReturn { hasCache: (parentId: string) => boolean; } +function insertChildrenIntoTree( + nodes: TreeNode[], + parentId: string, + children: TreeNode[] +): TreeNode[] { + return nodes.map((node) => { + if (node.id === parentId) { + return { + ...node, + children: children, + }; + } + if (node.children) { + return { + ...node, + children: insertChildrenIntoTree(node.children, parentId, children), + }; + } + + return node; + }); +} + export const useTreeData = ({ fetchData, searchTerm = '', @@ -203,26 +226,3 @@ export const useTreeData = ({ hasCache, }; }; - -function insertChildrenIntoTree( - nodes: TreeNode[], - parentId: string, - children: TreeNode[] -): TreeNode[] { - return nodes.map((node) => { - if (node.id === parentId) { - return { - ...node, - children: children, - }; - } - if (node.children) { - return { - ...node, - children: insertChildrenIntoTree(node.children, parentId, children), - }; - } - - return node; - }); -} diff --git a/openmetadata-ui/src/main/resources/ui/src/contexts/WorkflowModeContext.tsx b/openmetadata-ui/src/main/resources/ui/src/contexts/WorkflowModeContext.tsx index addfe2e8d7bf..7e7e1afbbc71 100644 --- a/openmetadata-ui/src/main/resources/ui/src/contexts/WorkflowModeContext.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/contexts/WorkflowModeContext.tsx @@ -20,7 +20,7 @@ import { interface WorkflowModeContextProps { children: ReactNode; workflowFqn?: string; - workflowDefinition?: any; + workflowDefinition?: Record; } const WorkflowModeContext = createContext( diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/usePubSub.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/usePubSub.ts index 58fcd6d53a97..f75c529f99a3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/usePubSub.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/usePubSub.ts @@ -13,12 +13,12 @@ import { EventEmitter } from 'eventemitter3'; import { DependencyList, useEffect } from 'react'; -type EventCallback = (data: T) => void; +type EventCallback = (data: T) => void; type UnsubscribeFunction = () => void; const emitter = new EventEmitter(); -export const useSub = ( +export const useSub = ( event: string, callback: EventCallback, dependencies?: DependencyList @@ -30,7 +30,6 @@ export const useSub = ( useEffect(() => { emitter.on(event, callback); - // If dependencies are provided, remove the callback when the component unmounts return () => { emitter.off(event, callback); }; @@ -40,7 +39,7 @@ export const useSub = ( }; export const usePub = () => { - return (event: string, data: T) => { + return (event: string, data: T) => { emitter.emit(event, data); }; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/interface/data-insight.interface.ts b/openmetadata-ui/src/main/resources/ui/src/interface/data-insight.interface.ts index 2124704ab74e..b78227ff586b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/interface/data-insight.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/interface/data-insight.interface.ts @@ -38,7 +38,7 @@ export interface ChartFilter { endTs: number; } -export interface DataInsightChartTooltipProps extends TooltipProps { +export interface DataInsightChartTooltipProps extends TooltipProps { cardStyles?: React.CSSProperties; customValueKey?: string; displayDateInHeader?: boolean; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SwaggerPage/RapiDocReact.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SwaggerPage/RapiDocReact.tsx index 63851d0e3ca0..c70d112216da 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SwaggerPage/RapiDocReact.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SwaggerPage/RapiDocReact.tsx @@ -84,11 +84,11 @@ interface RapiDocProps 'api-key-value'?: string | null; 'fetch-credentials'?: 'omit' | 'same-origin' | 'include'; // Events - beforeRender?: (spec: any) => void; - specLoaded?: (spec: any) => void; - beforeTry?: (request: any) => any; - afterTry?: (data: any) => any; - apiServerChange?: (server: any) => any; + beforeRender?: (spec: Record) => void; + specLoaded?: (spec: Record) => void; + beforeTry?: (request: Record) => unknown; + afterTry?: (data: Record) => unknown; + apiServerChange?: (server: Record) => unknown; } declare global { @@ -124,23 +124,23 @@ const RapiDocReact = React.forwardRef( ? ref?.current : localRef.current; - const handleBeforeRender = (spec: any) => { + const handleBeforeRender = (spec: Record) => { beforeRender?.(spec); }; - const handleSpecLoaded = (spec: any) => { + const handleSpecLoaded = (spec: Record) => { specLoaded?.(spec); }; - const handleBeforeTry = (request: any) => { + const handleBeforeTry = (request: Record) => { beforeTry?.(request); }; - const handleAfterTry = (data: any) => { + const handleAfterTry = (data: Record) => { afterTry?.(data); }; - const handleApiServerChange = (server: any) => { + const handleApiServerChange = (server: Record) => { apiServerChange?.(server); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Auth/TokenService/TokenServiceUtil.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Auth/TokenService/TokenServiceUtil.test.ts index 84ff67662a18..9bab753127a8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Auth/TokenService/TokenServiceUtil.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Auth/TokenService/TokenServiceUtil.test.ts @@ -32,7 +32,7 @@ describe('TokenService', () => { localStorage.clear(); jest.useFakeTimers(); // Reset the singleton instance for each test - // eslint-disable-next-line @typescript-eslint/no-explicit-any + (TokenService as any)._instance = undefined; // Mock indexedDB @@ -77,7 +77,7 @@ describe('TokenService', () => { it('should setup service worker listener if available', () => { // Reset instance to trigger constructor again - // eslint-disable-next-line @typescript-eslint/no-explicit-any + (TokenService as any)._instance = undefined; TokenService.getInstance(); @@ -89,7 +89,7 @@ describe('TokenService', () => { it('should handle TOKEN_UPDATE message', () => { const refreshSuccessCallback = jest.fn(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any + (TokenService as any)._instance = undefined; const service = TokenService.getInstance(); service.refreshSuccessCallback = refreshSuccessCallback; @@ -105,7 +105,7 @@ describe('TokenService', () => { it('should not trigger callback for TOKEN_CLEARED message', () => { const refreshSuccessCallback = jest.fn(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any + (TokenService as any)._instance = undefined; const service = TokenService.getInstance(); service.refreshSuccessCallback = refreshSuccessCallback; @@ -247,7 +247,7 @@ describe('TokenService', () => { newValue: 'true', }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any + (handler as any)(validEvent); expect(callback).toHaveBeenCalled(); @@ -269,7 +269,7 @@ describe('TokenService', () => { newValue: 'true', }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any + (handler as any)(invalidEvent); expect(callback).not.toHaveBeenCalled(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx index a976e59bd793..a5901d002ca1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx @@ -39,6 +39,9 @@ import Fqn from '../Fqn'; import { t } from '../i18next/LocalUtil'; import { getCustomPropertyEntityType } from './CSV.utils'; +type CSVRow = Record; +type EditCellProps = RenderEditCellProps; + class CSVUtilsClassBase { public hideImportsColumnList() { return ['glossaryStatus', 'inspectionQuery']; @@ -70,7 +73,7 @@ class CSVUtilsClassBase { user: boolean; team: boolean; } - ): ((props: RenderEditCellProps) => ReactNode) | undefined { + ): ((props: EditCellProps) => ReactNode) | undefined { switch (column) { case 'owner': return ({ @@ -78,7 +81,7 @@ class CSVUtilsClassBase { onRowChange, onClose, column, - }: RenderEditCellProps) => { + }: EditCellProps) => { const value = row?.[column.key]; const owners = value?.split(';') ?? []; const ownerEntityRef = owners.map((owner: string) => { @@ -123,7 +126,7 @@ class CSVUtilsClassBase { onRowChange, onClose, column, - }: RenderEditCellProps) => { + }: EditCellProps) => { const value = row[column.key]; const handleSave = async (description: string) => { onRowChange({ ...row, [column.key]: description }, true); @@ -149,7 +152,7 @@ class CSVUtilsClassBase { onRowChange, onClose, column, - }: RenderEditCellProps) => { + }: EditCellProps) => { const containerRef = useRef(null); const dropdownContainerRef = useRef(null); useMultiContainerFocusTrap({ @@ -203,7 +206,7 @@ class CSVUtilsClassBase { onRowChange, onClose, column, - }: RenderEditCellProps) => { + }: EditCellProps) => { const containerRef = useRef(null); const dropdownContainerRef = useRef(null); useMultiContainerFocusTrap({ @@ -254,7 +257,7 @@ class CSVUtilsClassBase { onRowChange, onClose, column, - }: RenderEditCellProps) => { + }: EditCellProps) => { const value = row[column.key]; const handleChange = async (tag?: Tag) => { onRowChange( @@ -283,7 +286,7 @@ class CSVUtilsClassBase { onRowChange, onClose, column, - }: RenderEditCellProps) => { + }: EditCellProps) => { const value = row[column.key]; const handleChange = async (tag?: Tag) => { onRowChange( @@ -311,7 +314,7 @@ class CSVUtilsClassBase { row, onRowChange, column, - }: RenderEditCellProps) => { + }: EditCellProps) => { const value = row[column.key]; const domains = value ? (value?.split(';') ?? []).map((domain: string) => { @@ -376,7 +379,7 @@ class CSVUtilsClassBase { onRowChange, onClose, column, - }: RenderEditCellProps) => { + }: EditCellProps) => { const value = row[column.key]; const reviewers = value?.split(';') ?? []; const reviewersEntityRef = reviewers.map((reviewer: string) => { @@ -436,7 +439,7 @@ class CSVUtilsClassBase { onRowChange, onClose, column, - }: RenderEditCellProps) => { + }: EditCellProps) => { const value = row[column.key]; const handleSave = async (extension?: string) => { onRowChange({ ...row, [column.key]: extension }, true); @@ -462,7 +465,7 @@ class CSVUtilsClassBase { onRowChange, onClose, column, - }: RenderEditCellProps) => { + }: EditCellProps) => { const value = row[column.key]; const handleChange = (typeValue: string) => { onRowChange({ ...row, [column.key]: typeValue }); @@ -499,7 +502,7 @@ class CSVUtilsClassBase { onRowChange, onClose, column, - }: RenderEditCellProps) => { + }: EditCellProps) => { const value = row[column.key]; const handleChange = (value: string) => { onRowChange({ ...row, [column.key]: value }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizaNavigation/CustomizeNavigation.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizaNavigation/CustomizeNavigation.test.ts index 06516f01208e..e4d72cd70cb9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizaNavigation/CustomizeNavigation.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizaNavigation/CustomizeNavigation.test.ts @@ -706,7 +706,7 @@ describe('CustomizeNavigation Utils', () => { const result = mergePluginSidebarItems( mockBaseItems, - // eslint-disable-next-line @typescript-eslint/no-explicit-any + pluginItems as any, navItems ); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntitySummaryPanelUtilsV1.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntitySummaryPanelUtilsV1.tsx index 478485bdfbac..e82a6747107d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntitySummaryPanelUtilsV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntitySummaryPanelUtilsV1.tsx @@ -1005,7 +1005,7 @@ const APIEndpointSchemaV1: React.FC<{ dataIndex: 'name', key: 'name', width: 200, - render: (name: string, record: Record) => ( + render: (name: string, record: Record) => (
{record.displayName || name}
@@ -1016,7 +1016,7 @@ const APIEndpointSchemaV1: React.FC<{ dataIndex: 'dataType', key: 'dataType', width: 150, - render: (dataType: string, record: Record) => ( + render: (dataType: string, record: Record) => ( {record.dataTypeDisplay || dataType || 'Unknown'} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ExtensionPointRegistry.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ExtensionPointRegistry.ts index 42f81af2abf3..4c31b8fda0e1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ExtensionPointRegistry.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ExtensionPointRegistry.ts @@ -21,7 +21,7 @@ /** * A contribution from a plugin to an extension point */ -export interface ExtensionContribution { +export interface ExtensionContribution { /** The extension point this contribution is for */ extensionPointId: string; /** The actual contribution data */ diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/QueryBuilderUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/QueryBuilderUtils.tsx index ed70ceddc641..ea235d6feb26 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/QueryBuilderUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/QueryBuilderUtils.tsx @@ -264,7 +264,7 @@ export const getJsonTreePropertyFromQueryFilter = ( fields?: Fields ) => { const convertedObj = queryFilter.reduce( - (acc, curr: QueryFieldInterface): Record => { + (acc, curr: QueryFieldInterface): Record => { if (!isUndefined(curr.term?.deleted)) { return { ...acc, @@ -385,7 +385,7 @@ export const getJsonTreePropertyFromQueryFilter = ( return acc; }, - {} as Record + {} as Record ); return convertedObj; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceConnectionDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceConnectionDetailsUtils.tsx index a313f04c25d8..e743c3c9984d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceConnectionDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceConnectionDetailsUtils.tsx @@ -28,7 +28,6 @@ type KeyValuesProps = { serviceCategory: string; }; -// Renders a basic input field with label and optional tooltip const renderInputField = ( key: string, value: string, @@ -78,7 +77,6 @@ const renderInputField = ( ); -// Renders filter pattern fields const renderFilterPattern = ( key: string, value: { includes: string[]; excludes: string[] }, @@ -115,7 +113,7 @@ const renderFilterPattern = ( key )}:`} - {(value as string[]).join(', ')} + {value.join(', ')} ); @@ -126,26 +124,207 @@ const renderFilterPattern = ( ); }; -export const getKeyValues = ({ +export function getKeyValues({ obj, schemaPropertyObject, schema, serviceCategory, -}: KeyValuesProps): ReactNode => { +}: KeyValuesProps): ReactNode { + const handleSpecialServiceConfig = ( + serviceType: string, + key: string, + value: Record, + schemaPropertyObject: Record + ): ReactNode | null => { + if ( + serviceType === EntityType.PIPELINE_SERVICE && + key === 'connection' && + value.type?.toString().toLowerCase() === 'airflow' + ) { + const airflowSchema = ( + schemaPropertyObject[key] as { + oneOf: Array<{ title: string; properties?: Record }>; + } + ).oneOf.find( + (item: { title: string }) => item.title === `${value.type}Connection` + )?.properties; + + return ( + airflowSchema && + getKeyValues({ + obj: value, + schemaPropertyObject: airflowSchema, + schema, + serviceCategory, + }) + ); + } + + if (serviceType === EntityType.DATABASE_SERVICE && key === 'credentials') { + const gcpSchema = ( + schemaPropertyObject[key] as { + definitions: { gcpCredentialsPath: Record }; + } + ).definitions.gcpCredentialsPath; + + return getKeyValues({ + obj: value, + schemaPropertyObject: gcpSchema, + schema, + serviceCategory, + }); + } + + if ( + serviceType === EntityType.METADATA_SERVICE && + key === 'securityConfig' + ) { + const jwtSchema = ( + schemaPropertyObject[key] as { + oneOf: Array<{ title: string; properties?: Record }>; + } + ).oneOf.find( + (item: { title: string }) => item.title === JWT_CONFIG + )?.properties; + + return ( + jwtSchema && + getKeyValues({ + obj: value, + schemaPropertyObject: jwtSchema, + schema, + serviceCategory, + }) + ); + } + + if ( + serviceType === EntityType.DASHBOARD_SERVICE && + key === 'githubCredentials' + ) { + const githubSchema = ( + schemaPropertyObject[key] as { + oneOf: Array<{ title: string; properties?: Record }>; + } + ).oneOf.find( + (item: { title: string }) => item.title === 'GitHubCredentials' + )?.properties; + + return ( + githubSchema && + getKeyValues({ + obj: value, + schemaPropertyObject: githubSchema, + schema, + serviceCategory, + }) + ); + } + + return null; + }; + + const handleDatabaseConfigSource = ( + key: string, + value: Record + ): ReactNode | null => { + const securityConfig = value.securityConfig as Record; + if (!isObject(securityConfig)) { + return null; + } + + if (securityConfig.gcpConfig) { + const gcpConfigSchema = isObject(securityConfig.gcpConfig) + ? get( + schema, + 'definitions.GCPConfig.properties.securityConfig.definitions.GCPValues.properties', + {} + ) + : get( + schema, + 'definitions.GCPConfig.properties.securityConfig.definitions.gcpCredentialsPath', + {} + ); + + return getKeyValues({ + obj: isObject(securityConfig.gcpConfig) + ? (securityConfig.gcpConfig as Record) + : value, + schemaPropertyObject: gcpConfigSchema as Record, + schema, + serviceCategory, + }); + } + + const internalRef = '$ref'; + const oneOf = 'oneOf'; + + if ( + Object.keys( + schemaPropertyObject[key] as Record + ).includes(oneOf) && + (securityConfig?.awsAccessKeyId || securityConfig?.awsSecretAccessKey) + ) { + return getKeyValues({ + obj: securityConfig, + schemaPropertyObject: get( + schema, + 'definitions.S3Config.properties.securityConfig.properties', + {} + ) as Record, + schema, + serviceCategory, + }); + } + + if ( + Object.keys( + schemaPropertyObject[key] as Record + ).includes(internalRef) + ) { + const definition = (schemaPropertyObject[key] as { $ref: string }).$ref + .split('/') + .splice(2); + + return getKeyValues({ + obj: value, + schemaPropertyObject: ( + schema as { definitions: Record> } + ).definitions[definition.join('.')], + schema, + serviceCategory, + }); + } + + return null; + }; + try { return Object.keys(obj).map((key) => { const value = obj[key]; - // Return early if value is null or key is in DEF_UI_SCHEMA if (isNull(value) || key in DEF_UI_SCHEMA) { return null; } - // Handle non-object and array values if (!isObject(value) || isArray(value)) { - const { description, format, title } = schemaPropertyObject[key] ?? {}; - - return renderInputField(key, value, description, format, title); + const { + description = '', + format = '', + title = '', + } = (schemaPropertyObject[key] as { + description?: string; + format?: string; + title?: string; + }) ?? {}; + + return renderInputField( + key, + value as string, + description, + format, + title + ); } const serviceType = serviceCategory.slice(0, -1); @@ -154,51 +333,56 @@ export const getKeyValues = ({ serviceType as keyof typeof FILTER_PATTERN_BY_SERVICE_TYPE ] ?? []; - // Handle filter pattern fields if ( filterPatternFields.includes( key as ServiceConnectionFilterPatternFields ) ) { - const { description, title } = schemaPropertyObject[key] ?? {}; + const { description, title } = + (schemaPropertyObject[key] as { + description?: string; + title?: string; + }) ?? {}; - return renderFilterPattern(key, value, description, title); + return renderFilterPattern( + key, + value as { includes: string[]; excludes: string[] }, + description, + title + ); } - // Handle special service configurations const specialConfig = handleSpecialServiceConfig( serviceType, key, - value, - schemaPropertyObject, - schema, - serviceCategory + value as Record, + schemaPropertyObject ); if (specialConfig !== null) { return specialConfig; } - // Handle database config source if ( serviceType === EntityType.DATABASE_SERVICE && key === 'configSource' ) { const configSource = handleDatabaseConfigSource( key, - value, - schemaPropertyObject, - schema, - serviceCategory + value as Record ); if (configSource !== null) { return configSource; } } - // Default object handling return getKeyValues({ - obj: value, - schemaPropertyObject: schemaPropertyObject[key]?.properties ?? {}, + obj: value as Record, + schemaPropertyObject: + ( + schemaPropertyObject[key] as { + properties?: Record; + } + )?.properties ?? {}, schema, serviceCategory, }); @@ -206,157 +390,4 @@ export const getKeyValues = ({ } catch { return ; } -}; - -// Handles special service type configurations -const handleSpecialServiceConfig = ( - serviceType: string, - key: string, - value: unknown, - schemaPropertyObject: Record, - schema: Record, - serviceCategory: string -): ReactNode | null => { - // Pipeline service - Airflow connection - if ( - serviceType === EntityType.PIPELINE_SERVICE && - key === 'connection' && - value.type?.toLowerCase() === 'airflow' - ) { - const airflowSchema = schemaPropertyObject[key].oneOf.find( - (item: { title: string }) => item.title === `${value.type}Connection` - )?.properties; - - return ( - airflowSchema && - getKeyValues({ - obj: value, - schemaPropertyObject: airflowSchema, - schema, - serviceCategory, - }) - ); - } - - // Database service - GCP credentials - if (serviceType === EntityType.DATABASE_SERVICE && key === 'credentials') { - const gcpSchema = schemaPropertyObject[key].definitions.gcpCredentialsPath; - - return getKeyValues({ - obj: value, - schemaPropertyObject: gcpSchema, - schema, - serviceCategory, - }); - } - - // Metadata service - Security config - if (serviceType === EntityType.METADATA_SERVICE && key === 'securityConfig') { - const jwtSchema = schemaPropertyObject[key].oneOf.find( - (item: { title: string }) => item.title === JWT_CONFIG - )?.properties; - - return ( - jwtSchema && - getKeyValues({ - obj: value, - schemaPropertyObject: jwtSchema, - schema, - serviceCategory, - }) - ); - } - - // Dashboard service - GitHub credentials - if ( - serviceType === EntityType.DASHBOARD_SERVICE && - key === 'githubCredentials' - ) { - const githubSchema = schemaPropertyObject[key].oneOf.find( - (item: { title: string }) => item.title === 'GitHubCredentials' - )?.properties; - - return ( - githubSchema && - getKeyValues({ - obj: value, - schemaPropertyObject: githubSchema, - schema, - serviceCategory, - }) - ); - } - - return null; -}; - -// Handles database service config source -const handleDatabaseConfigSource = ( - key: string, - value: unknown, - schemaPropertyObject: Record, - schema: Record, - serviceCategory: string -): ReactNode | null => { - if (!isObject(value.securityConfig)) { - return null; - } - - if (value.securityConfig.gcpConfig) { - const gcpConfigSchema = isObject(value.securityConfig.gcpConfig) - ? get( - schema, - 'definitions.GCPConfig.properties.securityConfig.definitions.GCPValues.properties', - {} - ) - : get( - schema, - 'definitions.GCPConfig.properties.securityConfig.definitions.gcpCredentialsPath', - {} - ); - - return getKeyValues({ - obj: isObject(value.securityConfig.gcpConfig) - ? value.securityConfig.gcpConfig - : value, - schemaPropertyObject: gcpConfigSchema, - schema, - serviceCategory, - }); - } - - const internalRef = '$ref'; - const oneOf = 'oneOf'; - - if ( - Object.keys(schemaPropertyObject[key]).includes(oneOf) && - (value.securityConfig?.awsAccessKeyId || - value.securityConfig?.awsSecretAccessKey) - ) { - return getKeyValues({ - obj: value.securityConfig, - schemaPropertyObject: get( - schema, - 'definitions.S3Config.properties.securityConfig.properties', - {} - ), - schema, - serviceCategory, - }); - } - - if (Object.keys(schemaPropertyObject[key]).includes(internalRef)) { - const definition = schemaPropertyObject[key][internalRef] - .split('/') - .splice(2); - - return getKeyValues({ - obj: value, - schemaPropertyObject: schema.definitions[definition], - schema, - serviceCategory, - }); - } - - return null; -}; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/WorkflowConfigUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/WorkflowConfigUtils.ts index 079ddccb0d38..9ecfd0ecefd5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/WorkflowConfigUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/WorkflowConfigUtils.ts @@ -16,7 +16,7 @@ import { NodeConfig } from '../interface/workflow-builder-components.interface'; export const getSelectedEntityTypes = ( config: NodeConfig, - workflowDefinition: any + workflowDefinition: Record ): EntityType | EntityType[] => { if (config.dataAssets && config.dataAssets.length > 0) { const entityTypes = config.dataAssets.filter(Boolean); From 03821304f3b60f550a490c0826e9eb4bbc46f46d Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Wed, 13 May 2026 17:14:21 +0530 Subject: [PATCH 2/3] update typescript check --- .github/workflows/ui-checkstyle.yml | 17 +++-- .../ui/eslint-rules/no-duplicate-api-calls.js | 8 +- .../NodeChildren.component.test.tsx | 12 --- .../MyData/RightSidebar/FollowingWidget.tsx | 6 +- .../Applications/plugins/AppPlugin.ts | 2 +- .../src/interface/data-insight.interface.ts | 3 +- .../TokenService/TokenServiceUtil.test.ts | 10 +-- .../ui/src/utils/CSV/CSVUtilsClassBase.tsx | 76 +++---------------- .../CustomizeNavigation.test.ts | 36 ++++----- 9 files changed, 60 insertions(+), 110 deletions(-) diff --git a/.github/workflows/ui-checkstyle.yml b/.github/workflows/ui-checkstyle.yml index f7a3863689bf..f35ec71c846e 100644 --- a/.github/workflows/ui-checkstyle.yml +++ b/.github/workflows/ui-checkstyle.yml @@ -204,7 +204,6 @@ jobs: # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ tsc-src: needs: authorize - if: false # TODO: re-enable once tsc errors are resolved runs-on: ubuntu-latest permissions: contents: read @@ -393,7 +392,7 @@ jobs: # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ tsc-playwright: needs: authorize - if: false # TODO: re-enable once playwright tsc errors are resolved + if: false # TODO: re-enable once playwright tsc errors are resolved runs-on: ubuntu-latest permissions: contents: read @@ -470,7 +469,6 @@ jobs: working-directory: ${{ env.CORE_COMPONENTS_WORKING_DIRECTORY }} run: yarn install --frozen-lockfile - - name: Get changed core-components files id: changed-files uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 @@ -506,7 +504,16 @@ jobs: # Job 9: Report โ€” Post single consolidated summary comment # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ report: - needs: [authorize, lint-src, license-header, i18n-sync, app-docs, lint-playwright, lint-core-components] + needs: + [ + authorize, + lint-src, + license-header, + i18n-sync, + app-docs, + lint-playwright, + lint-core-components, + ] if: ${{ always() && needs.authorize.result == 'success' && github.event_name == 'pull_request_target' }} runs-on: ubuntu-latest permissions: @@ -518,7 +525,7 @@ jobs: with: issue-number: ${{ github.event.pull_request.number }} comment-author: github-actions[bot] - body-includes: '' + body-includes: "" - name: Post or update summary comment uses: actions/github-script@v7 diff --git a/openmetadata-ui/src/main/resources/ui/eslint-rules/no-duplicate-api-calls.js b/openmetadata-ui/src/main/resources/ui/eslint-rules/no-duplicate-api-calls.js index b64634afffef..5208a0c6a393 100644 --- a/openmetadata-ui/src/main/resources/ui/eslint-rules/no-duplicate-api-calls.js +++ b/openmetadata-ui/src/main/resources/ui/eslint-rules/no-duplicate-api-calls.js @@ -152,7 +152,9 @@ module.exports = { if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') { signature += `("${firstArg.value}")`; } else if (firstArg.type === 'TemplateLiteral') { - const parts = firstArg.quasis.map((q) => q.value.cooked).join('${...}'); + const parts = firstArg.quasis + .map((q) => q.value.cooked) + .join('${...}'); signature += `(\`${parts}\`)`; } else if (firstArg.type === 'ObjectExpression') { const props = firstArg.properties @@ -165,7 +167,9 @@ module.exports = { return ''; }) .filter(Boolean); - signature += `({${props.join(', ')}${firstArg.properties.length > 2 ? ', ...' : ''}})`; + signature += `({${props.join(', ')}${ + firstArg.properties.length > 2 ? ', ...' : '' + }})`; } } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeChildren/NodeChildren.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeChildren/NodeChildren.component.test.tsx index 5b158cf58e11..993bd68ee651 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeChildren/NodeChildren.component.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/NodeChildren/NodeChildren.component.test.tsx @@ -26,7 +26,6 @@ const mockUpdateColumnsInCurrentPages = jest.fn(); const mockGetTestCaseExecutionSummary = getTestCaseExecutionSummary as jest.Mock; - const mockNode: any = { id: 'test-id', entityType: EntityType.TABLE, @@ -46,7 +45,6 @@ const mockNode: any = { ], }; - const mockNodeWithTestSuite: any = { ...mockNode, testSuite: { @@ -335,7 +333,6 @@ describe('NodeChildren Component', () => { describe('Lineage Filter', () => { it('should show only columns with lineage when filter is active', () => { - const nodeWithMultipleColumns: any = { ...mockNode, columns: [ @@ -419,7 +416,6 @@ describe('NodeChildren Component', () => { }); it('should apply search on filtered columns when lineage filter is active', () => { - const nodeWithMultipleColumns: any = { ...mockNode, columns: [ @@ -568,7 +564,6 @@ describe('NodeChildren Component', () => { describe('CSS Classes and Styling', () => { it('should apply "any-column-selected" class when a column is selected', () => { - mockLineageStoreState.selectedColumn = 'test.fqn.column1' as any; render( @@ -601,7 +596,6 @@ describe('NodeChildren Component', () => { }); it('should apply both classes when column is selected and creating edge', () => { - mockLineageStoreState.selectedColumn = 'test.fqn.column1' as any; mockLineageStoreState.isCreatingEdge = true; @@ -622,7 +616,6 @@ describe('NodeChildren Component', () => { describe('Different Entity Types', () => { it('should render columns for DASHBOARD_DATA_MODEL entity', () => { - const dashboardDataModelNode: any = { ...mockNode, entityType: EntityType.DASHBOARD_DATA_MODEL, @@ -640,7 +633,6 @@ describe('NodeChildren Component', () => { }); it('should render fields for TOPIC entity', () => { - const topicNode: any = { id: 'topic-id', entityType: EntityType.TOPIC, @@ -669,7 +661,6 @@ describe('NodeChildren Component', () => { }); it('should render features for MLMODEL entity', () => { - const mlModelNode: any = { id: 'mlmodel-id', entityType: EntityType.MLMODEL, @@ -698,7 +689,6 @@ describe('NodeChildren Component', () => { describe('Edge Cases', () => { it('should handle nodes with empty column names', () => { - const nodeWithEmptyNames: any = { ...mockNode, columns: [ @@ -722,7 +712,6 @@ describe('NodeChildren Component', () => { }); it('should handle search with special characters', () => { - const nodeWithSpecialChars: any = { ...mockNode, columns: [ @@ -756,7 +745,6 @@ describe('NodeChildren Component', () => { }); it('should handle columns without fullyQualifiedName', () => { - const nodeWithMissingFQN: any = { ...mockNode, columns: [{ name: 'column1', dataType: 'STRING' }], diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/RightSidebar/FollowingWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/RightSidebar/FollowingWidget.tsx index 32bdca71039b..e2bba7558342 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/RightSidebar/FollowingWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/RightSidebar/FollowingWidget.tsx @@ -155,7 +155,11 @@ function FollowingWidget({ return extraInfo; }; - const getEntityIcon = (item: { serviceType?: string; name: string; type?: string }) => { + const getEntityIcon = (item: { + serviceType?: string; + name: string; + type?: string; + }) => { if (item.serviceType) { return ( { +export interface DataInsightChartTooltipProps + extends TooltipProps { cardStyles?: React.CSSProperties; customValueKey?: string; displayDateInHeader?: boolean; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Auth/TokenService/TokenServiceUtil.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Auth/TokenService/TokenServiceUtil.test.ts index 9bab753127a8..a0c6d2016f9c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Auth/TokenService/TokenServiceUtil.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Auth/TokenService/TokenServiceUtil.test.ts @@ -32,7 +32,7 @@ describe('TokenService', () => { localStorage.clear(); jest.useFakeTimers(); // Reset the singleton instance for each test - + (TokenService as any)._instance = undefined; // Mock indexedDB @@ -77,7 +77,7 @@ describe('TokenService', () => { it('should setup service worker listener if available', () => { // Reset instance to trigger constructor again - + (TokenService as any)._instance = undefined; TokenService.getInstance(); @@ -89,7 +89,7 @@ describe('TokenService', () => { it('should handle TOKEN_UPDATE message', () => { const refreshSuccessCallback = jest.fn(); - + (TokenService as any)._instance = undefined; const service = TokenService.getInstance(); service.refreshSuccessCallback = refreshSuccessCallback; @@ -105,7 +105,7 @@ describe('TokenService', () => { it('should not trigger callback for TOKEN_CLEARED message', () => { const refreshSuccessCallback = jest.fn(); - + (TokenService as any)._instance = undefined; const service = TokenService.getInstance(); service.refreshSuccessCallback = refreshSuccessCallback; @@ -247,7 +247,6 @@ describe('TokenService', () => { newValue: 'true', }); - (handler as any)(validEvent); expect(callback).toHaveBeenCalled(); @@ -269,7 +268,6 @@ describe('TokenService', () => { newValue: 'true', }); - (handler as any)(invalidEvent); expect(callback).not.toHaveBeenCalled(); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx index a5901d002ca1..cea08dc9016c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSVUtilsClassBase.tsx @@ -76,12 +76,7 @@ class CSVUtilsClassBase { ): ((props: EditCellProps) => ReactNode) | undefined { switch (column) { case 'owner': - return ({ - row, - onRowChange, - onClose, - column, - }: EditCellProps) => { + return ({ row, onRowChange, onClose, column }: EditCellProps) => { const value = row?.[column.key]; const owners = value?.split(';') ?? []; const ownerEntityRef = owners.map((owner: string) => { @@ -121,12 +116,7 @@ class CSVUtilsClassBase { ); }; case 'description': - return ({ - row, - onRowChange, - onClose, - column, - }: EditCellProps) => { + return ({ row, onRowChange, onClose, column }: EditCellProps) => { const value = row[column.key]; const handleSave = async (description: string) => { onRowChange({ ...row, [column.key]: description }, true); @@ -147,12 +137,7 @@ class CSVUtilsClassBase { ); }; case 'tags': - return ({ - row, - onRowChange, - onClose, - column, - }: EditCellProps) => { + return ({ row, onRowChange, onClose, column }: EditCellProps) => { const containerRef = useRef(null); const dropdownContainerRef = useRef(null); useMultiContainerFocusTrap({ @@ -201,12 +186,7 @@ class CSVUtilsClassBase { }; case 'glossaryTerms': case 'relatedTerms': - return ({ - row, - onRowChange, - onClose, - column, - }: EditCellProps) => { + return ({ row, onRowChange, onClose, column }: EditCellProps) => { const containerRef = useRef(null); const dropdownContainerRef = useRef(null); useMultiContainerFocusTrap({ @@ -252,12 +232,7 @@ class CSVUtilsClassBase { ); }; case 'tiers': - return ({ - row, - onRowChange, - onClose, - column, - }: EditCellProps) => { + return ({ row, onRowChange, onClose, column }: EditCellProps) => { const value = row[column.key]; const handleChange = async (tag?: Tag) => { onRowChange( @@ -281,12 +256,7 @@ class CSVUtilsClassBase { }; case 'certification': - return ({ - row, - onRowChange, - onClose, - column, - }: EditCellProps) => { + return ({ row, onRowChange, onClose, column }: EditCellProps) => { const value = row[column.key]; const handleChange = async (tag?: Tag) => { onRowChange( @@ -310,11 +280,7 @@ class CSVUtilsClassBase { ); }; case 'domains': - return ({ - row, - onRowChange, - column, - }: EditCellProps) => { + return ({ row, onRowChange, column }: EditCellProps) => { const value = row[column.key]; const domains = value ? (value?.split(';') ?? []).map((domain: string) => { @@ -374,12 +340,7 @@ class CSVUtilsClassBase { ); }; case 'reviewers': - return ({ - row, - onRowChange, - onClose, - column, - }: EditCellProps) => { + return ({ row, onRowChange, onClose, column }: EditCellProps) => { const value = row[column.key]; const reviewers = value?.split(';') ?? []; const reviewersEntityRef = reviewers.map((reviewer: string) => { @@ -434,12 +395,7 @@ class CSVUtilsClassBase { ); }; case 'extension': - return ({ - row, - onRowChange, - onClose, - column, - }: EditCellProps) => { + return ({ row, onRowChange, onClose, column }: EditCellProps) => { const value = row[column.key]; const handleSave = async (extension?: string) => { onRowChange({ ...row, [column.key]: extension }, true); @@ -460,12 +416,7 @@ class CSVUtilsClassBase { ); }; case 'entityType*': - return ({ - row, - onRowChange, - onClose, - column, - }: EditCellProps) => { + return ({ row, onRowChange, onClose, column }: EditCellProps) => { const value = row[column.key]; const handleChange = (typeValue: string) => { onRowChange({ ...row, [column.key]: typeValue }); @@ -497,12 +448,7 @@ class CSVUtilsClassBase { }; case 'code': - return ({ - row, - onRowChange, - onClose, - column, - }: EditCellProps) => { + return ({ row, onRowChange, onClose, column }: EditCellProps) => { const value = row[column.key]; const handleChange = (value: string) => { onRowChange({ ...row, [column.key]: value }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizaNavigation/CustomizeNavigation.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizaNavigation/CustomizeNavigation.test.ts index e4d72cd70cb9..5029f77c99f6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomizaNavigation/CustomizeNavigation.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomizaNavigation/CustomizeNavigation.test.ts @@ -11,6 +11,9 @@ * limitations under the License. */ +import { Compass01, Home01 } from '@untitledui/icons'; +import { LeftSidebarItem } from 'components/MyData/LeftSidebar/LeftSidebar.interface'; +import { LeftSidebarItemExample } from 'components/Settings/Applications/plugins/AppPlugin'; import { NavigationItem } from '../../generated/system/ui/uiCustomization'; import leftSidebarClassBase from '../LeftSidebarClassBase'; import { @@ -425,17 +428,17 @@ describe('CustomizeNavigation Utils', () => { }); describe('mergePluginSidebarItems', () => { - const mockBaseItems = [ + const mockBaseItems: LeftSidebarItem[] = [ { key: 'home', title: 'Home', - icon: 'home-icon', + icon: Home01, dataTestId: 'home', }, { key: 'explore', title: 'Explore', - icon: 'explore-icon', + icon: Compass01, dataTestId: 'explore', }, ]; @@ -447,11 +450,11 @@ describe('CustomizeNavigation Utils', () => { }); it('should append plugin items without index at the end', () => { - const pluginItems = [ + const pluginItems: LeftSidebarItemExample[] = [ { key: 'plugin1', title: 'Plugin 1', - icon: 'plugin-icon', + icon: Compass01, dataTestId: 'plugin1', }, ]; @@ -467,7 +470,7 @@ describe('CustomizeNavigation Utils', () => { { key: 'plugin1', title: 'Plugin 1', - icon: 'plugin-icon', + icon: Compass01, dataTestId: 'plugin1', index: 1, }, @@ -486,7 +489,7 @@ describe('CustomizeNavigation Utils', () => { { key: 'plugin1', title: 'Plugin 1', - icon: 'plugin-icon', + icon: Compass01, dataTestId: 'plugin1', index: 0, }, @@ -505,21 +508,21 @@ describe('CustomizeNavigation Utils', () => { { key: 'plugin2', title: 'Plugin 2', - icon: 'plugin-icon-2', + icon: Compass01, dataTestId: 'plugin2', index: 2, }, { key: 'plugin1', title: 'Plugin 1', - icon: 'plugin-icon-1', + icon: Compass01, dataTestId: 'plugin1', index: 0, }, { key: 'plugin3', title: 'Plugin 3', - icon: 'plugin-icon-3', + icon: Compass01, dataTestId: 'plugin3', }, ]; @@ -539,7 +542,7 @@ describe('CustomizeNavigation Utils', () => { { key: 'plugin1', title: 'Plugin 1', - icon: 'plugin-icon', + icon: Compass01, dataTestId: 'plugin1', index: 999, }, @@ -556,7 +559,7 @@ describe('CustomizeNavigation Utils', () => { { key: 'plugin1', title: 'Plugin 1', - icon: 'plugin-icon', + icon: Compass01, dataTestId: 'plugin1', index: 1, }, @@ -586,7 +589,7 @@ describe('CustomizeNavigation Utils', () => { { key: 'plugin1', title: 'Plugin 1', - icon: 'plugin-icon', + icon: Compass01, dataTestId: 'plugin1', index: 1, }, @@ -616,7 +619,7 @@ describe('CustomizeNavigation Utils', () => { { key: 'plugin1', title: 'Plugin 1', - icon: 'plugin-icon', + icon: Compass01, dataTestId: 'plugin1', index: 1, }, @@ -633,7 +636,7 @@ describe('CustomizeNavigation Utils', () => { { key: 'plugin1', title: 'Plugin 1', - icon: 'plugin-icon', + icon: Compass01, dataTestId: 'plugin1', index: 1, }, @@ -706,7 +709,6 @@ describe('CustomizeNavigation Utils', () => { const result = mergePluginSidebarItems( mockBaseItems, - pluginItems as any, navItems ); @@ -742,7 +744,7 @@ describe('CustomizeNavigation Utils', () => { { key: 'plugin-item', title: 'Plugin Item', - icon: 'plugin-icon', + icon: Compass01, dataTestId: 'plugin-item', index: 1, }, From 60e8a5a40373404d4f890e388210c168900dae00 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 08:56:45 +0000 Subject: [PATCH 3/3] fix: address review feedback - useCallback, type guards, module-scope helpers, import path Agent-Logs-Url: https://github.com/open-metadata/OpenMetadata/sessions/e1e68460-c1c8-457f-af82-43e837951aee Co-authored-by: chirag-madlani <12962843+chirag-madlani@users.noreply.github.com> --- .../ui/eslint-rules/test-example.tsx | 2 +- .../FilterConfiguration.tsx | 6 +- .../utils/ServiceConnectionDetailsUtils.tsx | 295 +++++++++--------- .../ui/src/utils/WorkflowConfigUtils.ts | 11 +- 4 files changed, 168 insertions(+), 146 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/eslint-rules/test-example.tsx b/openmetadata-ui/src/main/resources/ui/eslint-rules/test-example.tsx index a7ebdfd6769b..2843d5fdf619 100644 --- a/openmetadata-ui/src/main/resources/ui/eslint-rules/test-example.tsx +++ b/openmetadata-ui/src/main/resources/ui/eslint-rules/test-example.tsx @@ -12,7 +12,7 @@ */ import { useEffect, useState } from 'react'; -import { searchQuery } from '../rest/searchAPI'; +import { searchQuery } from '../src/rest/searchAPI'; // This is a test file to demonstrate the no-duplicate-api-calls rule diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SearchSettings/FilterConfiguration/FilterConfiguration.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SearchSettings/FilterConfiguration/FilterConfiguration.tsx index c1e1c30025cd..523ccd38a1bf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SearchSettings/FilterConfiguration/FilterConfiguration.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/SearchSettings/FilterConfiguration/FilterConfiguration.tsx @@ -21,7 +21,7 @@ import { Typography, } from 'antd'; import { startCase } from 'lodash'; -import { useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as CloseIcon } from '../../../assets/svg/close.svg'; import { ReactComponent as FilterIcon } from '../../../assets/svg/filter-primary.svg'; @@ -43,13 +43,13 @@ const FilterConfiguration = () => { [] ); - const handleCheckboxChange = (fieldName: string) => { + const handleCheckboxChange = useCallback((fieldName: string) => { setCheckedItems((prev) => prev.includes(fieldName) ? prev.filter((item) => item !== fieldName) : [...prev, fieldName] ); - }; + }, []); const menuItems = useMemo( () => ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceConnectionDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceConnectionDetailsUtils.tsx index e743c3c9984d..68c98dd6ee45 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ServiceConnectionDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ServiceConnectionDetailsUtils.tsx @@ -124,29 +124,29 @@ const renderFilterPattern = ( ); }; -export function getKeyValues({ - obj, - schemaPropertyObject, - schema, - serviceCategory, -}: KeyValuesProps): ReactNode { - const handleSpecialServiceConfig = ( - serviceType: string, - key: string, - value: Record, - schemaPropertyObject: Record - ): ReactNode | null => { +const handleSpecialServiceConfig = ( + serviceType: string, + key: string, + value: Record, + schemaPropertyObject: Record, + schema: Record, + serviceCategory: string +): ReactNode | null => { + if ( + serviceType === EntityType.PIPELINE_SERVICE && + key === 'connection' + ) { + const valueType = value.type; if ( - serviceType === EntityType.PIPELINE_SERVICE && - key === 'connection' && - value.type?.toString().toLowerCase() === 'airflow' + typeof valueType === 'string' && + valueType.toLowerCase() === 'airflow' ) { const airflowSchema = ( schemaPropertyObject[key] as { oneOf: Array<{ title: string; properties?: Record }>; } ).oneOf.find( - (item: { title: string }) => item.title === `${value.type}Connection` + (item: { title: string }) => item.title === `${valueType}Connection` )?.properties; return ( @@ -159,146 +159,156 @@ export function getKeyValues({ }) ); } + } - if (serviceType === EntityType.DATABASE_SERVICE && key === 'credentials') { - const gcpSchema = ( - schemaPropertyObject[key] as { - definitions: { gcpCredentialsPath: Record }; - } - ).definitions.gcpCredentialsPath; + if (serviceType === EntityType.DATABASE_SERVICE && key === 'credentials') { + const gcpSchema = ( + schemaPropertyObject[key] as { + definitions: { gcpCredentialsPath: Record }; + } + ).definitions.gcpCredentialsPath; - return getKeyValues({ + return getKeyValues({ + obj: value, + schemaPropertyObject: gcpSchema, + schema, + serviceCategory, + }); + } + + if ( + serviceType === EntityType.METADATA_SERVICE && + key === 'securityConfig' + ) { + const jwtSchema = ( + schemaPropertyObject[key] as { + oneOf: Array<{ title: string; properties?: Record }>; + } + ).oneOf.find( + (item: { title: string }) => item.title === JWT_CONFIG + )?.properties; + + return ( + jwtSchema && + getKeyValues({ obj: value, - schemaPropertyObject: gcpSchema, + schemaPropertyObject: jwtSchema, schema, serviceCategory, - }); - } - - if ( - serviceType === EntityType.METADATA_SERVICE && - key === 'securityConfig' - ) { - const jwtSchema = ( - schemaPropertyObject[key] as { - oneOf: Array<{ title: string; properties?: Record }>; - } - ).oneOf.find( - (item: { title: string }) => item.title === JWT_CONFIG - )?.properties; - - return ( - jwtSchema && - getKeyValues({ - obj: value, - schemaPropertyObject: jwtSchema, - schema, - serviceCategory, - }) - ); - } - - if ( - serviceType === EntityType.DASHBOARD_SERVICE && - key === 'githubCredentials' - ) { - const githubSchema = ( - schemaPropertyObject[key] as { - oneOf: Array<{ title: string; properties?: Record }>; - } - ).oneOf.find( - (item: { title: string }) => item.title === 'GitHubCredentials' - )?.properties; - - return ( - githubSchema && - getKeyValues({ - obj: value, - schemaPropertyObject: githubSchema, - schema, - serviceCategory, - }) - ); - } - - return null; - }; - - const handleDatabaseConfigSource = ( - key: string, - value: Record - ): ReactNode | null => { - const securityConfig = value.securityConfig as Record; - if (!isObject(securityConfig)) { - return null; - } + }) + ); + } - if (securityConfig.gcpConfig) { - const gcpConfigSchema = isObject(securityConfig.gcpConfig) - ? get( - schema, - 'definitions.GCPConfig.properties.securityConfig.definitions.GCPValues.properties', - {} - ) - : get( - schema, - 'definitions.GCPConfig.properties.securityConfig.definitions.gcpCredentialsPath', - {} - ); + if ( + serviceType === EntityType.DASHBOARD_SERVICE && + key === 'githubCredentials' + ) { + const githubSchema = ( + schemaPropertyObject[key] as { + oneOf: Array<{ title: string; properties?: Record }>; + } + ).oneOf.find( + (item: { title: string }) => item.title === 'GitHubCredentials' + )?.properties; - return getKeyValues({ - obj: isObject(securityConfig.gcpConfig) - ? (securityConfig.gcpConfig as Record) - : value, - schemaPropertyObject: gcpConfigSchema as Record, + return ( + githubSchema && + getKeyValues({ + obj: value, + schemaPropertyObject: githubSchema, schema, serviceCategory, - }); - } + }) + ); + } - const internalRef = '$ref'; - const oneOf = 'oneOf'; + return null; +}; - if ( - Object.keys( - schemaPropertyObject[key] as Record - ).includes(oneOf) && - (securityConfig?.awsAccessKeyId || securityConfig?.awsSecretAccessKey) - ) { - return getKeyValues({ - obj: securityConfig, - schemaPropertyObject: get( +const handleDatabaseConfigSource = ( + key: string, + value: Record, + schemaPropertyObject: Record, + schema: Record, + serviceCategory: string +): ReactNode | null => { + const securityConfig = value.securityConfig as Record; + if (!isObject(securityConfig)) { + return null; + } + + if (securityConfig.gcpConfig) { + const gcpConfigSchema = isObject(securityConfig.gcpConfig) + ? get( schema, - 'definitions.S3Config.properties.securityConfig.properties', + 'definitions.GCPConfig.properties.securityConfig.definitions.GCPValues.properties', {} - ) as Record, - schema, - serviceCategory, - }); - } + ) + : get( + schema, + 'definitions.GCPConfig.properties.securityConfig.definitions.gcpCredentialsPath', + {} + ); - if ( - Object.keys( - schemaPropertyObject[key] as Record - ).includes(internalRef) - ) { - const definition = (schemaPropertyObject[key] as { $ref: string }).$ref - .split('/') - .splice(2); + return getKeyValues({ + obj: isObject(securityConfig.gcpConfig) + ? (securityConfig.gcpConfig as Record) + : value, + schemaPropertyObject: gcpConfigSchema as Record, + schema, + serviceCategory, + }); + } - return getKeyValues({ - obj: value, - schemaPropertyObject: ( - schema as { definitions: Record> } - ).definitions[definition.join('.')], + const internalRef = '$ref'; + const oneOf = 'oneOf'; + + if ( + Object.keys( + schemaPropertyObject[key] as Record + ).includes(oneOf) && + (securityConfig?.awsAccessKeyId || securityConfig?.awsSecretAccessKey) + ) { + return getKeyValues({ + obj: securityConfig, + schemaPropertyObject: get( schema, - serviceCategory, - }); - } + 'definitions.S3Config.properties.securityConfig.properties', + {} + ) as Record, + schema, + serviceCategory, + }); + } - return null; - }; + if ( + Object.keys( + schemaPropertyObject[key] as Record + ).includes(internalRef) + ) { + const definition = (schemaPropertyObject[key] as { $ref: string }).$ref + .split('/') + .splice(2); + + return getKeyValues({ + obj: value, + schemaPropertyObject: ( + schema as { definitions: Record> } + ).definitions[definition.join('.')], + schema, + serviceCategory, + }); + } + + return null; +}; +export function getKeyValues({ + obj, + schemaPropertyObject, + schema, + serviceCategory, +}: KeyValuesProps): ReactNode { try { return Object.keys(obj).map((key) => { const value = obj[key]; @@ -356,7 +366,9 @@ export function getKeyValues({ serviceType, key, value as Record, - schemaPropertyObject + schemaPropertyObject, + schema, + serviceCategory ); if (specialConfig !== null) { return specialConfig; @@ -368,7 +380,10 @@ export function getKeyValues({ ) { const configSource = handleDatabaseConfigSource( key, - value as Record + value as Record, + schemaPropertyObject, + schema, + serviceCategory ); if (configSource !== null) { return configSource; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/WorkflowConfigUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/WorkflowConfigUtils.ts index 9ecfd0ecefd5..adfc1ec3f24c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/WorkflowConfigUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/WorkflowConfigUtils.ts @@ -36,8 +36,15 @@ export const getSelectedEntityTypes = ( typeof workflowDefinition.trigger === 'object' && workflowDefinition.trigger !== null && !Array.isArray(workflowDefinition.trigger) - ? workflowDefinition.trigger.config - : {}; + ? ( + workflowDefinition.trigger as { + config?: { + entityType?: EntityType; + entityTypes?: string[]; + }; + } + ).config + : undefined; if (triggerConfig?.entityType) { return triggerConfig.entityType;