diff --git a/.eslintignore b/.eslintignore index 0adc76113..ffd57a46d 100755 --- a/.eslintignore +++ b/.eslintignore @@ -39,7 +39,6 @@ src/Common/PopupMenu.tsx src/Common/Progressing.tsx src/Common/RJSF/config.ts src/Common/RJSF/templates/ArrayFieldTemplate.tsx -src/Common/RJSF/templates/BaseInput.tsx src/Common/RJSF/templates/ButtonTemplates/AddButton.tsx src/Common/RJSF/templates/ButtonTemplates/RemoveButton.tsx src/Common/RJSF/templates/ButtonTemplates/SubmitButton.tsx diff --git a/.eslintrc.cjs b/.eslintrc.cjs index c2e95c310..ab8bbe579 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -151,6 +151,10 @@ module.exports = { group: ['IconBase'], message: 'Please use "Icon" component instead.', }, + { + group: ['IllustrationBase'], + message: 'Please use "Illustration" component instead.', + }, ], }, ], diff --git a/.husky/pre-commit b/.husky/pre-commit index 929d7364e..b40501eae 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -20,9 +20,9 @@ echo "Running pre-commit hook..." # Check for changes in the Icon folder -CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep -E 'IconV2/|generate-icon.cjs' || true) +ICON_CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep -E 'IconV2/|generate-icon.cjs' || true) -if [ -n "$CHANGED_FILES" ]; then +if [ -n "$ICON_CHANGED_FILES" ]; then echo "Changes detected in the Icon folder or icon generation script. Running icon generation script..." if ! npm run generate-icon; then @@ -36,6 +36,23 @@ else echo "No changes in the IconsV2 folder. Skipping icon generation." fi +# Check for changes in the Illustration folder +ILLUSTRATION_CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep -E 'Illustration/|generate-illustration.cjs' || true) + +if [ -n "$ILLUSTRATION_CHANGED_FILES" ]; then + echo "Changes detected in the Illustration folder or illustration generation script. Running illustration generation script..." + + if ! npm run generate-illustration; then + echo "Error: Illustration generation script failed." + exit 1 + fi + + echo "Illustration.tsx updated. Adding to commit." + git add src/Shared/Components/Illustration/Illustration.tsx +else + echo "No changes in the Illustration folder. Skipping illustration generation." +fi + # TypeScript check if ! npx tsc --noEmit; then echo "Error: TypeScript check failed." diff --git a/package-lock.json b/package-lock.json index 71b0db8b0..67706c96d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.17.1", + "version": "1.18.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.17.1", + "version": "1.18.0", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 52d979718..86fd09563 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.17.1", + "version": "1.18.0", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", @@ -33,7 +33,8 @@ "preview": "vite preview", "lint-staged": "lint-staged", "postinstall": "patch-package", - "generate-icon": "node ./scripts/generate-icon.cjs" + "generate-icon": "node ./scripts/generate-icon.cjs", + "generate-illustration": "node ./scripts/generate-illustration.cjs" }, "devDependencies": { "@esbuild-plugins/node-globals-polyfill": "0.2.3", diff --git a/scripts/generate-illustration.cjs b/scripts/generate-illustration.cjs new file mode 100644 index 000000000..68b9ba84b --- /dev/null +++ b/scripts/generate-illustration.cjs @@ -0,0 +1,104 @@ +const fs = require('fs') +const path = require('path') +const { execFile } = require('child_process') + +// Base path relative to the current script +const basePath = path.resolve(__dirname, '../src') + +// Directory containing SVG, Webp illustrations and the output file +const illustrationsDir = path.join(basePath, 'Assets', 'Illustration') +const outputFile = path.join(basePath, 'Shared', 'Components', 'Illustration', 'Illustration.tsx') + +const runESLint = (filePath) => { + execFile('npx', ['eslint', filePath, '--fix'], (error, stdout, stderr) => { + if (error) { + console.error(`Error running ESLint: ${error.message}`) + return + } + if (stderr) { + console.error(`ESLint stderr: ${stderr}`) + } + if (stdout) { + console.log(`ESLint output:\n${stdout}`) + } + console.log('ESLint completed successfully.') + }) +} + +const generateIllustrationComponent = () => { + // Read all files in the illustrations directory + const files = fs.readdirSync(illustrationsDir) + + // Filter for SVG files + const svgFiles = files.filter((file) => file.endsWith('.svg')) + // Filter for WEBP files + const webpFiles = files.filter((file) => file.endsWith('.webp')) + + // Generate import statements and the illustration map + const imports = [] + const illustrationMapEntries = [] + + svgFiles.forEach((file) => { + // Remove the .svg extension + const illustrationName = path.basename(file, '.svg') + // Convert illustration-name to IllustrationName for importName + const importName = illustrationName + .split('-') + .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) + .join('') + // Push imports statement + imports.push(`import { ReactComponent as ${importName} } from '@Illustrations/${file}'`) + // Push illustrations to illustrationMap + illustrationMapEntries.push(`["${illustrationName}"]: ${importName},`) + }) + + webpFiles.forEach((file) => { + // Remove the .webp extension + const illustrationName = path.basename(file, '.webp') + // Convert illustration-name to IllustrationName for importName + const importName = illustrationName.replace(/(^\w|-\w)/g, (match) => match.replace('-', '').toUpperCase()) + // Push imports statement + imports.push(`import ${importName} from '@Illustrations/${file}'`) + // Push illustrations to illustrationMap + illustrationMapEntries.push(`["${illustrationName}"]: ${importName},`) + }) + + // Generate the Illustration.tsx content + const content = ` + // NOTE: This file is auto-generated. Do not edit directly. Run the script \`npm run generate-illustration\` to update. + + ${imports.join('\n')} + + // eslint-disable-next-line no-restricted-imports + import { IllustrationBase } from './IllustrationBase'; + import { IllustrationBaseProps } from './types'; + + export const illustrationMap = { + ${illustrationMapEntries.join('\n')} + }; + + export type IllustrationName = keyof typeof illustrationMap; + + export interface IllustrationProps extends Omit { + /** + * The name of the illustration to render. + * @note The component will return either an img component or an SVG component based on the type of illustration (.svg, .webp) + */ + name: keyof typeof illustrationMap; + } + + export const Illustration = (props: IllustrationProps) => { + return ; + }; +` + + // Write the content to the Illustration.tsx file + fs.writeFileSync(outputFile, content.trim(), 'utf-8') + console.log(`Illustration component file generated at: ${outputFile}`) + + // Run ESLint on the generated file + runESLint(outputFile) +} + +// Run the script +generateIllustrationComponent() diff --git a/src/Assets/IconV2/ic-activity.svg b/src/Assets/IconV2/ic-activity.svg new file mode 100644 index 000000000..de027535e --- /dev/null +++ b/src/Assets/IconV2/ic-activity.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-folder.svg b/src/Assets/IconV2/ic-folder.svg new file mode 100644 index 000000000..586cba681 --- /dev/null +++ b/src/Assets/IconV2/ic-folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-helm-app.svg b/src/Assets/IconV2/ic-helm-app.svg new file mode 100644 index 000000000..c76c9be5c --- /dev/null +++ b/src/Assets/IconV2/ic-helm-app.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Assets/IconV2/ic-list-bullets.svg b/src/Assets/IconV2/ic-list-bullets.svg new file mode 100644 index 000000000..305aeb0ae --- /dev/null +++ b/src/Assets/IconV2/ic-list-bullets.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/Assets/IconV2/ic-namespace.svg b/src/Assets/IconV2/ic-namespace.svg new file mode 100644 index 000000000..cbaf35fcc --- /dev/null +++ b/src/Assets/IconV2/ic-namespace.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/Illustration/img-code.webp b/src/Assets/Illustration/img-code.webp new file mode 100644 index 000000000..9fbbc7af1 Binary files /dev/null and b/src/Assets/Illustration/img-code.webp differ diff --git a/src/Assets/Illustration/img-man-on-rocket.webp b/src/Assets/Illustration/img-man-on-rocket.webp new file mode 100644 index 000000000..8edd9f0b2 Binary files /dev/null and b/src/Assets/Illustration/img-man-on-rocket.webp differ diff --git a/src/Assets/Illustration/img-no-result.webp b/src/Assets/Illustration/img-no-result.webp new file mode 100644 index 000000000..445a663bf Binary files /dev/null and b/src/Assets/Illustration/img-no-result.webp differ diff --git a/src/Common/EmptyState/GenericEmptyState.tsx b/src/Common/EmptyState/GenericEmptyState.tsx index 3b1ea4a12..1cc6626d0 100644 --- a/src/Common/EmptyState/GenericEmptyState.tsx +++ b/src/Common/EmptyState/GenericEmptyState.tsx @@ -14,6 +14,8 @@ * limitations under the License. */ +import { Illustration } from '@Shared/Components' + import AppNotDeployed from '../../Assets/Img/app-not-deployed.svg' import { GenericEmptyStateType, ImageType } from '../Types' @@ -35,6 +37,7 @@ const GenericEmptyState = ({ layout = 'column', contentClassName = '', imageStyles = {}, + imgName, }: GenericEmptyStateType): JSX.Element => { const isRowLayout = layout === 'row' @@ -54,7 +57,20 @@ const GenericEmptyState = ({ data-testid="generic-empty-state" > {!SvgImage ? ( - !noImage && ( + !noImage && + (imgName ? ( + + ) : ( empty-state - ) + )) ) : ( { + const [isTextTruncated, setIsTextTruncated] = useState(false) + + const handleMouseEnterEvent: React.MouseEventHandler = (event) => { + const { currentTarget: node } = event + const isTextOverflowing = + node.scrollWidth > node.clientWidth + SUB_PIXEL_ERROR || + node.scrollHeight > node.clientHeight + SUB_PIXEL_ERROR + if (isTextOverflowing && !isTextTruncated) { + setIsTextTruncated(true) + } else if (!isTextOverflowing && isTextTruncated) { + setIsTextTruncated(false) + } + } + + return { + isTextTruncated, + handleMouseEnterEvent, + } +} + +export default useIsTextTruncated diff --git a/src/Common/Hooks/UseIsTextTruncated/constants.ts b/src/Common/Hooks/UseIsTextTruncated/constants.ts new file mode 100644 index 000000000..377520560 --- /dev/null +++ b/src/Common/Hooks/UseIsTextTruncated/constants.ts @@ -0,0 +1 @@ +export const SUB_PIXEL_ERROR = 1 diff --git a/src/Common/Hooks/UseIsTextTruncated/index.ts b/src/Common/Hooks/UseIsTextTruncated/index.ts new file mode 100644 index 000000000..9bc03650e --- /dev/null +++ b/src/Common/Hooks/UseIsTextTruncated/index.ts @@ -0,0 +1 @@ +export { default as useIsTextTruncated } from './UseIsTextTruncated' diff --git a/src/Common/Hooks/index.ts b/src/Common/Hooks/index.ts index fc57ae76b..b6bfc1049 100644 --- a/src/Common/Hooks/index.ts +++ b/src/Common/Hooks/index.ts @@ -16,6 +16,7 @@ export { useClickOutside } from './UseClickOutside/UseClickOutside' export { useGetUserRoles } from './UseGetUserRoles' +export { useIsTextTruncated } from './UseIsTextTruncated' export * from './UseRegisterShortcut' export * from './useStateFilters' export * from './useUrlFilters' diff --git a/src/Common/Modals/Modal.tsx b/src/Common/Modals/Modal.tsx index 92c159caa..e58bd9578 100644 --- a/src/Common/Modals/Modal.tsx +++ b/src/Common/Modals/Modal.tsx @@ -45,7 +45,7 @@ export const Modal = ({ function disableWheel(e) { if (!preventWheelDisable) { - if (innerRef?.current.contains(e.target)) { + if (innerRef.current?.contains(e.target)) { if (innerRef.current.clientHeight === innerRef.current.scrollHeight) { e.preventDefault() } diff --git a/src/Common/RJSF/Form.tsx b/src/Common/RJSF/Form.tsx index c8b650439..dc4491a29 100644 --- a/src/Common/RJSF/Form.tsx +++ b/src/Common/RJSF/Form.tsx @@ -20,6 +20,7 @@ import RJSF from '@rjsf/core' import { SCHEMA_07_VALIDATOR } from '@Shared/validations' import { templates, widgets } from './config' +import { RJSF_FORM_SELECT_PORTAL_TARGET_ID } from './constants' import { FormProps } from './types' import { getFormStateFromFormData, @@ -82,28 +83,32 @@ export const RJSFForm = forwardRef((props: FormProps, ref: FormProps['ref']) => } return ( -
+ <> + + {/* NOTE: due to stacking context issues, we send this id to SelectPicker menuPortalTarget prop */} +
+ ) }) diff --git a/src/Common/RJSF/common/FieldRow.tsx b/src/Common/RJSF/common/FieldRow.tsx index 5286413c0..9640cdac4 100644 --- a/src/Common/RJSF/common/FieldRow.tsx +++ b/src/Common/RJSF/common/FieldRow.tsx @@ -31,14 +31,14 @@ export const FieldRowWithLabel = ({
{showLabel && (