diff --git a/package-lock.json b/package-lock.json index 083801310..8b910679b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.15.1-pre-1", + "version": "1.15.1-pre-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.15.1-pre-1", + "version": "1.15.1-pre-2", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index d0a566dc3..aefcd5c95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.15.1-pre-1", + "version": "1.15.1-pre-2", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", diff --git a/src/Assets/IconV2/ic-brain.svg b/src/Assets/IconV2/ic-brain.svg new file mode 100644 index 000000000..a5ff6cc35 --- /dev/null +++ b/src/Assets/IconV2/ic-brain.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/Assets/IconV2/ic-devtron-ai.svg b/src/Assets/IconV2/ic-devtron-ai.svg new file mode 100644 index 000000000..ba63770be --- /dev/null +++ b/src/Assets/IconV2/ic-devtron-ai.svg @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Assets/IconV2/ic-paper-plane.svg b/src/Assets/IconV2/ic-paper-plane.svg new file mode 100644 index 000000000..f0015526d --- /dev/null +++ b/src/Assets/IconV2/ic-paper-plane.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-sparkle-ai-color.svg b/src/Assets/IconV2/ic-sparkle-ai-color.svg new file mode 100644 index 000000000..be65b9a69 --- /dev/null +++ b/src/Assets/IconV2/ic-sparkle-ai-color.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Assets/IconV2/ic-sparkle-color.svg b/src/Assets/IconV2/ic-sparkle-color.svg index a2aa5c5d3..5629a5d5a 100644 --- a/src/Assets/IconV2/ic-sparkle-color.svg +++ b/src/Assets/IconV2/ic-sparkle-color.svg @@ -1 +1,18 @@ - \ No newline at end of file + + + + diff --git a/src/Assets/IconV2/ic-stop-fill.svg b/src/Assets/IconV2/ic-stop-fill.svg new file mode 100644 index 000000000..c87806663 --- /dev/null +++ b/src/Assets/IconV2/ic-stop-fill.svg @@ -0,0 +1,8 @@ + + + diff --git a/src/Shared/Components/Button/Button.component.tsx b/src/Shared/Components/Button/Button.component.tsx index d8eb3a367..23e7d9e33 100644 --- a/src/Shared/Components/Button/Button.component.tsx +++ b/src/Shared/Components/Button/Button.component.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { MutableRefObject, PropsWithChildren, useEffect, useRef, useState } from 'react' +import { forwardRef, MutableRefObject, PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react' import { Link } from 'react-router-dom' import { Progressing } from '@Common/Progressing' @@ -26,78 +26,96 @@ import { getButtonDerivedClass, getButtonIconClassName, getButtonLoaderSize } fr import './button.scss' -const ButtonElement = ({ - component = ButtonComponentType.button, - anchorProps, - linkProps, - buttonProps, - onClick, - elementRef, - ...props -}: PropsWithChildren< - Omit< - ButtonProps, - | 'text' - | 'variant' - | 'size' - | 'style' - | 'startIcon' - | 'endIcon' - | 'showTooltip' - | 'tooltipProps' - | 'dataTestId' - | 'isLoading' - | 'ariaLabel' - | 'showAriaLabelInTippy' - > & { - className: string - 'data-testid': ButtonProps['dataTestId'] - 'aria-label': ButtonProps['ariaLabel'] - elementRef: MutableRefObject - } ->) => { - // Added the specific class to ensure that the link override is applied - const linkOrAnchorClassName = `${props.className} button__link ${props.disabled ? 'dc__disable-click' : ''}` +const ButtonElement = forwardRef< + HTMLButtonElement | HTMLAnchorElement, + PropsWithChildren< + Omit< + ButtonProps, + | 'text' + | 'variant' + | 'size' + | 'style' + | 'startIcon' + | 'endIcon' + | 'showTooltip' + | 'tooltipProps' + | 'dataTestId' + | 'isLoading' + | 'ariaLabel' + | 'showAriaLabelInTippy' + > & { + className: string + 'data-testid': ButtonProps['dataTestId'] + 'aria-label': ButtonProps['ariaLabel'] + elementRef: MutableRefObject + } + > +>( + ( + { component = ButtonComponentType.button, anchorProps, linkProps, buttonProps, onClick, elementRef, ...props }, + forwardedRef, + ) => { + // Added the specific class to ensure that the link override is applied + const linkOrAnchorClassName = `${props.className} button__link ${props.disabled ? 'dc__disable-click' : ''}` - if (component === ButtonComponentType.link) { - return ( - ['onClick']} - ref={elementRef as MutableRefObject} - /> - ) - } + // NOTE: If the ref callback is re-created every render (i.e., not wrapped in useCallback), + // it will be invoked on every render: first with null, then with the new node. + const refCallback = useCallback((el: HTMLButtonElement | HTMLAnchorElement) => { + if (!el) { + return + } + + // eslint-disable-next-line no-param-reassign + elementRef.current = el + + if (forwardedRef && typeof forwardedRef === 'object' && Object.hasOwn(forwardedRef, 'current')) { + // eslint-disable-next-line no-param-reassign + forwardedRef.current = el + } else if (typeof forwardedRef === 'function') { + forwardedRef(el) + } + }, []) + + if (component === ButtonComponentType.link) { + return ( + ['onClick']} + ref={refCallback} + /> + ) + } + + if (component === ButtonComponentType.anchor) { + return ( + ['onClick']} + ref={refCallback} + > + {props.children} + + ) + } - if (component === ButtonComponentType.anchor) { return ( - ['onClick']} - ref={elementRef as MutableRefObject} - > - {props.children} - + ref={refCallback} + /> ) - } - - return ( - + )} { const textareaRef = useRef(null) @@ -58,6 +59,8 @@ const Textarea = ({ // else, it behaves as controlled const [text, setText] = useState('') + const { MIN_HEIGHT, AUTO_EXPANSION_MAX_HEIGHT } = getTextAreaConstraintsForSize(size) + const updateRefsHeight = (height: number) => { const refElement = textareaRef.current if (refElement) { @@ -65,6 +68,19 @@ const Textarea = ({ } } + const refCallback = useCallback((node: HTMLTextAreaElement) => { + if (textareaRefProp) { + if (typeof textareaRefProp === 'function') { + textareaRefProp(node) + } else { + // eslint-disable-next-line no-param-reassign + textareaRefProp.current = node + } + } + + textareaRef.current = node + }, []) + const reInitHeight = () => { const currentHeight = parseInt(textareaRef.current.style.height, 10) let nextHeight = textareaRef.current.scrollHeight || 0 @@ -119,6 +135,10 @@ const Textarea = ({ if (event.key === 'Enter' || event.key === 'Escape') { event.stopPropagation() + if (newlineOnShiftEnter && event.key === 'Enter' && !event.shiftKey) { + event.preventDefault() + } + if (event.key === 'Escape') { textareaRef.current.blur() } @@ -163,11 +183,12 @@ const Textarea = ({ onBlur={handleBlur} onKeyDown={handleKeyDown} className={`${COMPONENT_SIZE_TYPE_TO_FONT_AND_BLOCK_PADDING_MAP[size]} ${COMPONENT_SIZE_TYPE_TO_INLINE_PADDING_MAP[size]} ${deriveBorderRadiusAndBorderClassFromConfig({ borderConfig, borderRadiusConfig })} w-100 dc__overflow-auto textarea`} - ref={textareaRef} + ref={refCallback} style={{ // No max height when user is expanding maxHeight: 'none', minHeight: MIN_HEIGHT, + resize: disableResize ? 'none' : 'vertical', }} /> diff --git a/src/Shared/Components/Textarea/types.ts b/src/Shared/Components/Textarea/types.ts index 4584dfdd6..2fb12cd87 100644 --- a/src/Shared/Components/Textarea/types.ts +++ b/src/Shared/Components/Textarea/types.ts @@ -35,9 +35,25 @@ export interface TextareaProps * * @default ComponentSizeType.large */ - size?: Extract + size?: Extract /** * Value of the textarea */ value: string + /** + * If true, the textarea resize is disabled + * + * @default false + */ + disableResize?: true + /** + * Allows inserting a newline with Shift + Enter instead of Enter alone. + * + * When enabled, pressing Enter submits the form, while Shift + Enter inserts a newline. + * Useful for forms where Enter should trigger submission, but multiline input is still needed. + * + * @default false + */ + newlineOnShiftEnter?: boolean + textareaRef?: React.MutableRefObject | React.RefCallback } diff --git a/src/Shared/Components/Textarea/utils.ts b/src/Shared/Components/Textarea/utils.ts new file mode 100644 index 000000000..a9cfe4699 --- /dev/null +++ b/src/Shared/Components/Textarea/utils.ts @@ -0,0 +1,15 @@ +import { ComponentSizeType } from '@Shared/constants' + +import { TEXTAREA_CONSTRAINTS } from './constants' +import { TextareaProps } from './types' + +export const getTextAreaConstraintsForSize = (size: TextareaProps['size']) => { + if (size === ComponentSizeType.small) { + return { + MIN_HEIGHT: 56, + AUTO_EXPANSION_MAX_HEIGHT: 150, + } + } + + return TEXTAREA_CONSTRAINTS +} diff --git a/src/Shared/Components/Typewriter/BlinkingCursor.tsx b/src/Shared/Components/Typewriter/BlinkingCursor.tsx new file mode 100644 index 000000000..fa49c4a11 --- /dev/null +++ b/src/Shared/Components/Typewriter/BlinkingCursor.tsx @@ -0,0 +1,15 @@ +import { motion } from 'framer-motion' + +export const BlinkingCursor = () => ( + +) diff --git a/src/Shared/Components/Typewriter/index.ts b/src/Shared/Components/Typewriter/index.ts new file mode 100644 index 000000000..441759395 --- /dev/null +++ b/src/Shared/Components/Typewriter/index.ts @@ -0,0 +1,2 @@ +export { BlinkingCursor } from './BlinkingCursor' +export { useTypewriter } from './useTypewriter' diff --git a/src/Shared/Components/Typewriter/useTypewriter.ts b/src/Shared/Components/Typewriter/useTypewriter.ts new file mode 100644 index 000000000..82ece9afd --- /dev/null +++ b/src/Shared/Components/Typewriter/useTypewriter.ts @@ -0,0 +1,20 @@ +import { useEffect } from 'react' +import { animate, useMotionValue, useTransform } from 'framer-motion' + +export const useTypewriter = (text: string) => { + const progress = useMotionValue(0) + + const visibleText = useTransform(progress, (latest) => text.slice(0, Math.floor(latest))) + + useEffect(() => { + const controls = animate(progress, text.length, { + type: 'tween', + duration: 4, + ease: 'linear', + }) + + return controls.stop + }, [text]) + + return visibleText +} diff --git a/src/Shared/Components/index.ts b/src/Shared/Components/index.ts index fceeef3bd..fa263a080 100644 --- a/src/Shared/Components/index.ts +++ b/src/Shared/Components/index.ts @@ -97,6 +97,7 @@ export * from './TargetPlatforms' export * from './Textarea' export * from './ThemeSwitcher' export * from './ToggleResolveScopedVariables' +export * from './Typewriter' export * from './UnsavedChanges' export * from './UnsavedChangesDialog' export * from './UserIdentifier' diff --git a/src/Shared/Providers/index.ts b/src/Shared/Providers/index.ts index a1dcc842c..8ad4a382f 100644 --- a/src/Shared/Providers/index.ts +++ b/src/Shared/Providers/index.ts @@ -18,4 +18,5 @@ export * from './ImageSelectionUtility' export * from './MainContextProvider' export * from './ThemeProvider' export type { MainContext, ReloadVersionConfigTypes, SidePanelConfig } from './types' +export { SidePanelTab } from './types' export * from './UserEmailProvider' diff --git a/src/Shared/Providers/types.ts b/src/Shared/Providers/types.ts index a21a45e9e..01ceaada2 100644 --- a/src/Shared/Providers/types.ts +++ b/src/Shared/Providers/types.ts @@ -30,9 +30,13 @@ export interface ReloadVersionConfigTypes { isRefreshing: boolean } +export enum SidePanelTab { + DOCUMENTATION = 'documentation', + ASK_DEVTRON = 'ask-devtron', +} + export interface SidePanelConfig { - /** Determines whether the side panel is visible */ - open: boolean + state: SidePanelTab | 'closed' /** Optional flag to reset/reinitialize the side panel state */ reinitialize?: boolean /** URL to documentation that should be displayed in the panel */ @@ -79,6 +83,13 @@ type CommonMainContextProps = { setLicenseData: Dispatch> canFetchHelmAppStatus: boolean setIntelligenceConfig: Dispatch> + aiAgentContext: { + path: string + context: Record + } + setAIAgentContext: (aiAgentContext: CommonMainContextProps['aiAgentContext']) => void + + sidePanelConfig: SidePanelConfig setSidePanelConfig: Dispatch> } diff --git a/src/index.ts b/src/index.ts index 1826cf696..4ad98de18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -159,6 +159,7 @@ export interface customEnv { GATEKEEPER_URL?: string FEATURE_AI_INTEGRATION_ENABLE?: boolean LOGIN_PAGE_IMAGE?: string + FEATURE_ASK_DEVTRON_EXPERT?: boolean /** * If true, the manage traffic feature is enabled in apps & app groups. *