diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 46c1123d3..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,110 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - comment: true, - ecmaFeatures: { - jsx: true, - }, - ecmaVersion: 'latest', - project: true, - sourceType: 'module', - }, - extends: [ - 'plugin:react/recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:@typescript-eslint/recommended-requiring-type-checking', - ], - overrides: [ - { - files: ['**/src/**/*.ts', '**/src/**/*.tsx'], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], - extends: ['plugin:@typescript-eslint/recommended'], - rules: { - 'react/prop-types': 'off', - '@typescript-eslint/no-unused-vars': 'error', - }, - }, - ], - settings: { - react: { - version: 'detect', - }, - }, - plugins: ['@typescript-eslint', 'react-hooks', 'eslint-plugin-react-hooks'], - rules: { - 'sort-imports': [ - 'error', - { - ignoreDeclarationSort: true, - }, - ], - 'no-restricted-imports': [ - 'error', - { - patterns: [ - { - group: ['@patternfly/react-icons/dist/esm**'], - message: 'Import using the full js path `@patternfly/react-icons/dist/js/icons` instead', - }, - { - group: ['@patternfly/react-tokens/dist/esm**'], - message: 'Import using the full js path `@patternfly/react-tokens/dist/js/icons` instead', - }, - ], - paths: [ - { - name: '@patternfly/react-icons', - message: 'Import using full path `@patternfly/react-icons/dist/js/icons` instead', - }, - { - name: '@patternfly/react-tokens', - message: 'Import using full path `@patternfly/react-tokens/dist/js/` instead', - }, - { - name: '@patternfly/react-core', - importNames: ['DescriptionList'], - message: 'Use FlightCtlDescriptionList wrapper', - }, - { - name: '@patternfly/react-core', - importNames: ['Form'], - message: 'Use FlightCtlForm wrapper', - }, - { - name: '@patternfly/react-core', - importNames: ['WizardFooterWrapper', 'WizardFooter'], - message: 'Use FlightCtlWizardFooter wrapper', - }, - { - name: 'react-i18next', - importNames: ['useTranslation'], - message: 'Import useTranslation from @flightctl/ui-components/hooks/useTranslation instead', - }, - { - name: 'lodash', - message: 'Import using full path `lodash/` instead', - }, - ], - }, - ], - '@typescript-eslint/explicit-function-return-type': 'off', - 'react/self-closing-comp': 'error', - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'error', - 'react/prop-types': 'off', - '@typescript-eslint/interface-name-prefix': 'off', - 'prettier/prettier': 'off', - 'import/no-unresolved': 'off', - 'import/extensions': 'off', - 'no-console': 'error', - '@typescript-eslint/no-misused-promises': 'off', - '@typescript-eslint/no-floating-promises': 'off', - '@typescript-eslint/no-unsafe-enum-comparison': 'off', - }, - env: { - browser: true, - node: true, - }, - ignorePatterns: ['*.json'], -}; diff --git a/apps/ocp-plugin/package.json b/apps/ocp-plugin/package.json index 0fee8fa16..92a37b31d 100644 --- a/apps/ocp-plugin/package.json +++ b/apps/ocp-plugin/package.json @@ -53,7 +53,7 @@ "devDependencies": { "@types/react-dom": "^18.2.17", "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^7.0.4", + "css-minimizer-webpack-plugin": "^8.0.0", "html-webpack-plugin": "^5.6.3", "mini-css-extract-plugin": "^2.10.0", "monaco-editor-webpack-plugin": "^7.1.0", @@ -66,7 +66,7 @@ "tsconfig-paths-webpack-plugin": "^4.1.0", "ts-loader": "^9.4.4", "url-loader": "^4.1.1", - "webpack": "5.76.3", + "webpack": "^5.105.4", "webpack-dev-server": "^5.2.3" }, "dependencies": { diff --git a/apps/standalone/package.json b/apps/standalone/package.json index c0a2a44f0..a34c5b096 100644 --- a/apps/standalone/package.json +++ b/apps/standalone/package.json @@ -20,9 +20,9 @@ }, "devDependencies": { "@types/react-dom": "^18.2.17", - "copy-webpack-plugin": "^13.0.0", + "copy-webpack-plugin": "^14.0.0", "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^7.0.4", + "css-minimizer-webpack-plugin": "^8.0.0", "dotenv": "^16.3.1", "html-webpack-plugin": "^5.6.3", "mini-css-extract-plugin": "^2.10.0", @@ -36,7 +36,7 @@ "ts-loader": "^9.4.4", "tsconfig-paths-webpack-plugin": "^4.1.0", "url-loader": "^4.1.1", - "webpack": "5.76.3", + "webpack": "^5.105.4", "webpack-dev-server": "^5.2.3" }, "dependencies": { diff --git a/apps/standalone/src/app/components/AppLayout/AppLayout.tsx b/apps/standalone/src/app/components/AppLayout/AppLayout.tsx index a7997c645..662a83a6d 100644 --- a/apps/standalone/src/app/components/AppLayout/AppLayout.tsx +++ b/apps/standalone/src/app/components/AppLayout/AppLayout.tsx @@ -89,7 +89,9 @@ const AppLayoutContent = () => { onClick={(event) => { event.preventDefault(); const primaryContentContainer = document.getElementById(pageId); - primaryContentContainer && primaryContentContainer.focus(); + if (primaryContentContainer) { + primaryContentContainer.focus(); + } }} href={`#${pageId}`} > diff --git a/apps/standalone/src/app/components/Login/LoginPage.tsx b/apps/standalone/src/app/components/Login/LoginPage.tsx index 4580a25de..7623bf6bf 100644 --- a/apps/standalone/src/app/components/Login/LoginPage.tsx +++ b/apps/standalone/src/app/components/Login/LoginPage.tsx @@ -52,7 +52,7 @@ const LoginPage = () => { setIsRedirecting(true); try { await redirectToProviderLogin(provider); - } catch (err) { + } catch { setIsRedirecting(false); setUserSelectedProvider(null); setError( @@ -86,7 +86,7 @@ const LoginPage = () => { setIsRedirecting(true); try { await redirectToProviderLogin(providers[0]); - } catch (err) { + } catch { setIsRedirecting(false); setError(t('Failed to initiate login')); } @@ -94,7 +94,7 @@ const LoginPage = () => { } else { setError(t('No authentication providers found. Please contact your administrator.')); } - } catch (err) { + } catch { setError(t('Failed to load the authentication providers')); } finally { setLoading(false); diff --git a/apps/standalone/src/app/context/AuthContext.ts b/apps/standalone/src/app/context/AuthContext.ts index 2368f672b..2bf025306 100644 --- a/apps/standalone/src/app/context/AuthContext.ts +++ b/apps/standalone/src/app/context/AuthContext.ts @@ -71,7 +71,7 @@ export const useAuthContext = () => { errorMessage = text; } } - } catch (parseErr) { + } catch { // If parsing fails, use default error message errorMessage = 'Authentication failed'; } @@ -119,7 +119,7 @@ export const useAuthContext = () => { errorMessage = text; } } - } catch (parseErr) { + } catch { // If parsing fails, use default error message } setError(errorMessage); @@ -140,7 +140,7 @@ export const useAuthContext = () => { errorMessage = text; } } - } catch (parseErr) { + } catch { // If parsing fails, use default error message } setError(errorMessage); @@ -206,7 +206,7 @@ export const useAuthContext = () => { } else { localStorage.removeItem(EXPIRATION); } - } catch (err) { + } catch { // By deleting the expiration, the next API request will get 401 and redirect to login localStorage.removeItem(EXPIRATION); } diff --git a/apps/standalone/src/app/utils/apiCalls.ts b/apps/standalone/src/app/utils/apiCalls.ts index a5dd83f79..55741e6dd 100644 --- a/apps/standalone/src/app/utils/apiCalls.ts +++ b/apps/standalone/src/app/utils/apiCalls.ts @@ -65,7 +65,11 @@ const getFullApiUrl = (path: string): { api: 'flightctl' | 'imagebuilder' | 'ale export const logout = async () => { const response = await fetch(`${uiProxyAPI}/logout`, { credentials: 'include' }); const { url } = (await response.json()) as { url: string }; - url ? (window.location.href = url) : window.location.reload(); + if (url) { + window.location.href = url; + } else { + window.location.reload(); + } }; export const redirectToLogin = () => { diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..a6ca83e17 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,127 @@ +const { defineConfig, globalIgnores } = require('eslint/config'); + +const tsParser = require('@typescript-eslint/parser'); +const typescriptEslint = require('@typescript-eslint/eslint-plugin'); +const reactHooks = require('eslint-plugin-react-hooks'); + +const { fixupPluginRules } = require('@eslint/compat'); + +const globals = require('globals'); +const js = require('@eslint/js'); + +const { FlatCompat } = require('@eslint/eslintrc'); + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +module.exports = defineConfig([ + { + languageOptions: { + parser: tsParser, + ecmaVersion: 'latest', + sourceType: 'module', + + parserOptions: { + comment: true, + + ecmaFeatures: { + jsx: true, + }, + + project: true, + }, + + globals: { + ...globals.browser, + ...globals.node, + }, + }, + + extends: compat.extends( + 'plugin:react/recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', + ), + + settings: { + react: { + version: 'detect', + }, + }, + + plugins: { + '@typescript-eslint': typescriptEslint, + 'react-hooks': fixupPluginRules(reactHooks), + }, + + rules: { + 'sort-imports': [ + 'error', + { + ignoreDeclarationSort: true, + }, + ], + + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['@patternfly/react-icons/dist/esm**'], + message: 'Import using the full js path `@patternfly/react-icons/dist/js/icons` instead', + }, + { + group: ['@patternfly/react-tokens/dist/esm**'], + message: 'Import using the full js path `@patternfly/react-tokens/dist/js/icons` instead', + }, + ], + + paths: [ + { + name: '@patternfly/react-icons', + message: 'Import using full path `@patternfly/react-icons/dist/js/icons` instead', + }, + { + name: '@patternfly/react-tokens', + message: 'Import using full path `@patternfly/react-tokens/dist/js/` instead', + }, + { + name: '@patternfly/react-core', + importNames: ['Form'], + message: 'Use FlightCtlForm wrapper', + }, + { + name: '@patternfly/react-core', + importNames: ['WizardFooterWrapper', 'WizardFooter'], + message: 'Use FlightCtlWizardFooter wrapper', + }, + { + name: 'react-i18next', + importNames: ['useTranslation'], + message: 'Import useTranslation from @flightctl/ui-components/hooks/useTranslation instead', + }, + { + name: 'lodash', + message: 'Import using full path `lodash/` instead', + }, + ], + }, + ], + + '@typescript-eslint/explicit-function-return-type': 'off', + 'react/self-closing-comp': 'error', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + 'react/prop-types': 'off', + 'no-console': 'error', + '@typescript-eslint/no-misused-promises': 'off', + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/no-unsafe-enum-comparison': 'off', + '@typescript-eslint/no-unused-vars': 'error', + }, + }, + globalIgnores(['**/*.json']), +]); diff --git a/libs/ui-components/src/components/Catalog/CatalogPage.tsx b/libs/ui-components/src/components/Catalog/CatalogPage.tsx index 3701168af..10dd3c071 100644 --- a/libs/ui-components/src/components/Catalog/CatalogPage.tsx +++ b/libs/ui-components/src/components/Catalog/CatalogPage.tsx @@ -236,26 +236,30 @@ export const CatalogPageContent = ({ { - catalogFilter.catalogs.includes(item.id || '') - ? catalogFilter.setCatalogs((catalogs) => catalogs.filter((c) => c !== item.id || '')) - : catalogFilter.setCatalogs((catalogs) => [...catalogs, item.id || '']); + const itemId = item.id as string; + if (catalogFilter.catalogs.includes(itemId)) { + catalogFilter.setCatalogs((catalogs) => catalogs.filter((c) => c !== itemId)); + } else { + catalogFilter.setCatalogs((catalogs) => [...catalogs, itemId]); + } }} hasCheckboxes data={catalogList.items.map((c) => { const canDelete = !c.metadata.owner && canDeleteCatalog; + const itemId = c.metadata.name as string; return { - name: c.spec.displayName || c.metadata.name || '', - id: c.metadata.name || '', + name: c.spec.displayName || itemId, + id: itemId, checkProps: { - checked: !!c.metadata.name && catalogFilter.catalogs.includes(c.metadata.name), + checked: !!itemId && catalogFilter.catalogs.includes(itemId), }, action: showCatalogMgmt ? ( { if (isOpen) { - setCatalogMenuOpen(c.metadata.name); - } else if (catalogMenuOpen === c.metadata.name) { + setCatalogMenuOpen(itemId); + } else if (catalogMenuOpen === itemId) { setCatalogMenuOpen(undefined); } }} @@ -263,8 +267,8 @@ export const CatalogPageContent = ({ toggle={(toggleRef) => ( setCatalogMenuOpen(c.metadata.name)} + isExpanded={catalogMenuOpen === itemId} + onClick={() => setCatalogMenuOpen(itemId)} variant="plain" icon={} aria-label={t('Actions dropdown')} diff --git a/libs/ui-components/src/components/Catalog/DeprecateModal.tsx b/libs/ui-components/src/components/Catalog/DeprecateModal.tsx index 70dcc928b..f3ae2c85d 100644 --- a/libs/ui-components/src/components/Catalog/DeprecateModal.tsx +++ b/libs/ui-components/src/components/Catalog/DeprecateModal.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { Alert, Button, - Form, FormGroup, Modal, ModalBody, @@ -16,6 +15,7 @@ import { Trans } from 'react-i18next'; import { useTranslation } from '../../hooks/useTranslation'; import { getErrorMessage } from '../../utils/error'; +import FlightCtlForm from '../form/FlightCtlForm'; type DeprecateModalProps = { onDeprecate: (message: string) => Promise; @@ -42,7 +42,7 @@ export const DeprecateModal = ({ onDeprecate, onClose, itemName }: DeprecateModa -
+