diff --git a/.eslintignore b/.eslintignore index ffd57a46d..82e735037 100755 --- a/.eslintignore +++ b/.eslintignore @@ -23,7 +23,6 @@ src/Common/DebouncedSearch/__tests__/DebouncedSearch.test.tsx src/Common/DevtronProgressing/DevtronProgressing.tsx src/Common/Dialogs/DialogForm.tsx src/Common/DraggableWrapper/DraggableButton.tsx -src/Common/DraggableWrapper/DraggableWrapper.tsx src/Common/Drawer/Drawer.tsx src/Common/Grid/Grid.tsx src/Common/Helper.tsx diff --git a/package-lock.json b/package-lock.json index d03b34458..c4f52f4ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.22.8", + "version": "1.23.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.22.8", + "version": "1.23.0", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 9a1df824d..a41641579 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.22.8", + "version": "1.23.0", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", diff --git a/src/Assets/IconV2/ic-bell.svg b/src/Assets/IconV2/ic-bell.svg new file mode 100644 index 000000000..a67b2859b --- /dev/null +++ b/src/Assets/IconV2/ic-bell.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-bulb.svg b/src/Assets/IconV2/ic-bulb.svg new file mode 100644 index 000000000..a167e3b3a --- /dev/null +++ b/src/Assets/IconV2/ic-bulb.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Assets/IconV2/ic-close-circle.svg b/src/Assets/IconV2/ic-close-circle.svg new file mode 100644 index 000000000..5c48b3451 --- /dev/null +++ b/src/Assets/IconV2/ic-close-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-comment.svg b/src/Assets/IconV2/ic-comment.svg new file mode 100644 index 000000000..096533e21 --- /dev/null +++ b/src/Assets/IconV2/ic-comment.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-dot-circle.svg b/src/Assets/IconV2/ic-dot-circle.svg new file mode 100644 index 000000000..149c94bab --- /dev/null +++ b/src/Assets/IconV2/ic-dot-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-medium-megaphone.svg b/src/Assets/IconV2/ic-medium-megaphone.svg new file mode 100644 index 000000000..8e94590f0 --- /dev/null +++ b/src/Assets/IconV2/ic-medium-megaphone.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Assets/IconV2/ic-new-chat.svg b/src/Assets/IconV2/ic-new-chat.svg new file mode 100644 index 000000000..a5d366419 --- /dev/null +++ b/src/Assets/IconV2/ic-new-chat.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Assets/IconV2/ic-piggybank.svg b/src/Assets/IconV2/ic-piggybank.svg new file mode 100644 index 000000000..44619eebe --- /dev/null +++ b/src/Assets/IconV2/ic-piggybank.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-priority-high-fill.svg b/src/Assets/IconV2/ic-priority-high-fill.svg new file mode 100644 index 000000000..9e084e5ee --- /dev/null +++ b/src/Assets/IconV2/ic-priority-high-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-priority-low-fill.svg b/src/Assets/IconV2/ic-priority-low-fill.svg new file mode 100644 index 000000000..0b7c7a735 --- /dev/null +++ b/src/Assets/IconV2/ic-priority-low-fill.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/Assets/IconV2/ic-priority-urgent-fill.svg b/src/Assets/IconV2/ic-priority-urgent-fill.svg new file mode 100644 index 000000000..ec8140320 --- /dev/null +++ b/src/Assets/IconV2/ic-priority-urgent-fill.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/Assets/IconV2/ic-red-bulb.svg b/src/Assets/IconV2/ic-red-bulb.svg new file mode 100644 index 000000000..f6d6482af --- /dev/null +++ b/src/Assets/IconV2/ic-red-bulb.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/Assets/IconV2/ic-ses.svg b/src/Assets/IconV2/ic-ses.svg new file mode 100644 index 000000000..11b2727e7 --- /dev/null +++ b/src/Assets/IconV2/ic-ses.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Assets/IconV2/ic-slack.svg b/src/Assets/IconV2/ic-slack.svg new file mode 100644 index 000000000..fc9268e74 --- /dev/null +++ b/src/Assets/IconV2/ic-slack.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Assets/IconV2/ic-smtp.svg b/src/Assets/IconV2/ic-smtp.svg new file mode 100644 index 000000000..e0be557be --- /dev/null +++ b/src/Assets/IconV2/ic-smtp.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Assets/IconV2/ic-thumb-down-fill.svg b/src/Assets/IconV2/ic-thumb-down-fill.svg new file mode 100644 index 000000000..424e814a4 --- /dev/null +++ b/src/Assets/IconV2/ic-thumb-down-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-thumb-up-fill.svg b/src/Assets/IconV2/ic-thumb-up-fill.svg new file mode 100644 index 000000000..7b5a06ab7 --- /dev/null +++ b/src/Assets/IconV2/ic-thumb-up-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-webhook-config.svg b/src/Assets/IconV2/ic-webhook-config.svg new file mode 100644 index 000000000..a3ca9e44c --- /dev/null +++ b/src/Assets/IconV2/ic-webhook-config.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/Common/API/CoreAPI.ts b/src/Common/API/CoreAPI.ts index c5056e8d9..0cb8f0165 100644 --- a/src/Common/API/CoreAPI.ts +++ b/src/Common/API/CoreAPI.ts @@ -55,6 +55,13 @@ class CoreAPI { method: type, signal, body: data ? JSON.stringify(data) : undefined, + ...(data && !isMultipartRequest + ? { + headers: { + 'Content-Type': 'application/json', + }, + } + : {}), } // eslint-disable-next-line dot-notation options['credentials'] = 'include' as RequestCredentials diff --git a/src/Common/API/reactQueryHooks.ts b/src/Common/API/reactQueryHooks.ts index 4b1ceed8d..f37b12533 100644 --- a/src/Common/API/reactQueryHooks.ts +++ b/src/Common/API/reactQueryHooks.ts @@ -16,6 +16,9 @@ import { QueryKey, + useInfiniteQuery as rqUseInfiniteQuery, + UseInfiniteQueryOptions, + UseInfiniteQueryResult, useMutation as rqUseMutation, UseMutationOptions, UseMutationResult, @@ -43,7 +46,22 @@ export const useQuery = < ): UseQueryResult => rqUseQuery(options) export const useMutation = ( - options: UseMutationOptions, ServerErrors, TVariables, TContext>, -): UseMutationResult, ServerErrors, TVariables, TContext> => rqUseMutation(options) + options: UseMutationOptions, +): UseMutationResult => rqUseMutation(options) + +export const useInfiniteQuery = < + TQueryFnData = unknown, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + WrapWithResponseType extends boolean = true, +>( + options: UseInfiniteQueryOptions< + WrapWithResponseType extends true ? ResponseType : TQueryFnData, + ServerErrors, + TData, + WrapWithResponseType extends true ? ResponseType : TQueryFnData, + TQueryKey + >, +): UseInfiniteQueryResult => rqUseInfiniteQuery(options) export { useQueryClient } diff --git a/src/Common/ClipboardButton/ClipboardButton.tsx b/src/Common/ClipboardButton/ClipboardButton.tsx index db62bd6da..dffdb197f 100644 --- a/src/Common/ClipboardButton/ClipboardButton.tsx +++ b/src/Common/ClipboardButton/ClipboardButton.tsx @@ -99,8 +99,6 @@ export const ClipboardButton = ({ const iconClassName = `icon-dim-${iconSize} dc__no-shrink` - const ariaLabel = `Copy ${content}` - const renderIcon = () => (
{copied ? : } @@ -109,10 +107,10 @@ export const ClipboardButton = ({ const tooltipContent = copied ? copiedTippyText : initialTippyText - if (variant === 'button--secondary') { + if (variant === 'button--secondary' || variant === 'borderLess') { return ( diff --git a/src/Common/SortableTableHeaderCell/types.ts b/src/Common/SortableTableHeaderCell/types.ts index 15f3f058f..346c80a3f 100644 --- a/src/Common/SortableTableHeaderCell/types.ts +++ b/src/Common/SortableTableHeaderCell/types.ts @@ -26,6 +26,10 @@ export type SortableTableHeaderCellProps = { * @default false */ showTippyOnTruncate?: boolean + /** + * If provided, shown in a tooltip on info-icon-outline beside the label + */ + infoTooltipText?: string } & ( | { /** diff --git a/src/Pages-Devtron-2.0/Navigation/types.ts b/src/Pages-Devtron-2.0/Navigation/types.ts index c0dcc41cf..8e7cfa478 100644 --- a/src/Pages-Devtron-2.0/Navigation/types.ts +++ b/src/Pages-Devtron-2.0/Navigation/types.ts @@ -30,7 +30,6 @@ export type NavigationItemID = | 'automation-and-enablement-alerting' | 'automation-and-enablement-incident-response' | 'automation-and-enablement-api-portal' - | 'automation-and-enablement-runbook-automation' | 'global-configuration-sso-login-services' | 'global-configuration-host-urls' | 'global-configuration-external-links' diff --git a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx index 22a73354f..242d0bed4 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx @@ -20,6 +20,7 @@ import { getAIAnalyticsEvents } from '@Common/Helper' import { Tooltip } from '@Common/Tooltip' import { ComponentSizeType } from '@Shared/constants' import { getAppDetailsURL } from '@Shared/Helpers' +import { MainContext, useMainContext } from '@Shared/Providers' import { Button, ButtonComponentType, ButtonVariantType } from '../Button' import { DeploymentStatusDetailBreakdown } from '../CICDHistory' @@ -86,7 +87,10 @@ export const AppStatusBody = ({ deploymentStatusDetailsBreakdownData, selectedTab, debugWithAIButton: ExplainWithAIButton, + handleClose, }: AppStatusBodyProps) => { + const { aiAgentContext } = useMainContext() + const appStatus = appDetails.resourceTree?.status?.toUpperCase() || appDetails.appStatus const getAppStatusInfoCardItems = (): (Omit, 'isLast'> & { id: string })[] => { @@ -101,6 +105,32 @@ export const AppStatusBody = ({ ) const debugObject = `${debugNode?.kind}/${debugNode?.name}` + const intelligenceConfig: MainContext['intelligenceConfig'] = { + clusterId: appDetails.clusterId, + metadata: { + ...(debugNode ? { object: debugObject } : { message }), + namespace: appDetails.namespace, + status: debugNode?.health?.status ?? appStatus, + }, + prompt: `Debug ${message || 'error'} ${debugNode ? `of ${debugObject}` : ''} in ${appDetails.namespace}`, + analyticsCategory: getAIAnalyticsEvents('APP_STATUS', appDetails.appType), + } + + // Have to add this to handle case of devtron-stack manager and software distribution hub. + const debugAgentContext = aiAgentContext + ? ({ + ...aiAgentContext, + prompt: `Why is application '${appDetails.appName}' of '${appDetails.environmentName}' env in ${appStatus} state?`, + data: { + ...aiAgentContext.data, + ...(debugNode ? { debugNodeKind: debugNode.kind, debugNodeName: debugNode.name } : {}), + ...(message ? { debugError: message } : {}), + namespace: appDetails.namespace, + status: debugNode?.health?.status ?? appStatus, + }, + } as MainContext['debugAgentContext']) + : null + return [ { id: 'app-status-row', @@ -112,19 +142,13 @@ export const AppStatusBody = ({ envId={appDetails.environmentId} actionItem={ ExplainWithAIButton && + debugAgentContext && appStatus?.toLowerCase() !== StatusType.HEALTHY.toLowerCase() && (debugNode || message) ? ( ) : null } diff --git a/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx index e8b053ca8..8c4dfc2ed 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx @@ -315,6 +315,7 @@ const AppStatusModal = ({ deploymentStatusDetailsBreakdownData={deploymentStatusDetailsBreakdownData} selectedTab={selectedTab} debugWithAIButton={debugWithAIButton} + handleClose={handleClose} /> {type === 'stack-manager' && ( diff --git a/src/Shared/Components/AppStatusModal/types.ts b/src/Shared/Components/AppStatusModal/types.ts index f1b15456d..27648d648 100644 --- a/src/Shared/Components/AppStatusModal/types.ts +++ b/src/Shared/Components/AppStatusModal/types.ts @@ -17,6 +17,7 @@ import { FunctionComponent, PropsWithChildren, ReactNode } from 'react' import { APIOptions, DeploymentAppTypes } from '@Common/Types' +import { MainContext } from '@Shared/Providers' import { AppDetails, ConfigDriftModalProps, @@ -38,7 +39,11 @@ export type AppStatusModalProps = { processVirtualEnvironmentDeploymentData: ( data?: DeploymentStatusDetailsType, ) => DeploymentStatusDetailsBreakdownDataType - debugWithAIButton: FunctionComponent<{ intelligenceConfig: IntelligenceConfig }> + debugWithAIButton: FunctionComponent<{ + intelligenceConfig: IntelligenceConfig + debugAgentContext: MainContext['debugAgentContext'] + onClick?: () => void + }> } & ( | { type: 'release' @@ -59,7 +64,7 @@ export type AppStatusModalProps = { ) export interface AppStatusBodyProps - extends Required> { + extends Required> { handleShowConfigDriftModal: () => void selectedTab: AppStatusModalTabType deploymentStatusDetailsBreakdownData: DeploymentStatusDetailsBreakdownDataType diff --git a/src/Shared/Components/CICDHistory/ConflictedResourcesTable.tsx b/src/Shared/Components/CICDHistory/ConflictedResourcesTable.tsx index 238293eae..9e24fc76f 100644 --- a/src/Shared/Components/CICDHistory/ConflictedResourcesTable.tsx +++ b/src/Shared/Components/CICDHistory/ConflictedResourcesTable.tsx @@ -10,7 +10,6 @@ import './ConflictedResourcesTable.scss' const Wrapper = ({ children }: TableViewWrapperProps) => (
{children}
) -const filter = () => true const ConflictedResourcesTable = ({ resourceConflictDetails }: ConflictedResourcesTableProps) => { const rows: RowsType = useMemo( @@ -42,7 +41,7 @@ const ConflictedResourcesTable = ({ resourceConflictDetails }: ConflictedResourc }} filtersVariant={FiltersTypeEnum.STATE} ViewWrapper={Wrapper} - filter={filter} + filter={null} /> ) } diff --git a/src/Shared/Components/Charts/types.ts b/src/Shared/Components/Charts/types.ts index 00304ec0e..7af462a1c 100644 --- a/src/Shared/Components/Charts/types.ts +++ b/src/Shared/Components/Charts/types.ts @@ -130,9 +130,7 @@ export type ChartProps = { placement?: TooltipProps['placement'] datasetValueFormatter?: (value: number) => string | number } - /** A title for x axis */ xScaleTitle?: string - /** A title for y axis */ yScaleTitle?: string } & TypeAndDatasetsType diff --git a/src/Shared/Components/DocLink/constants.ts b/src/Shared/Components/DocLink/constants.ts index 029e3d3af..22ca51192 100644 --- a/src/Shared/Components/DocLink/constants.ts +++ b/src/Shared/Components/DocLink/constants.ts @@ -142,9 +142,6 @@ export const DOCUMENTATION = { // Security Center SECURITY_CENTER: 'docs/user-guide/security-features', - // AI Recommendations - AI_RECOMMENDATIONS: 'docs/user-guide/ai-recommendations', - // Automation & Enablement AUTOMATION_AND_ENABLEMENT: 'docs/user-guide/automation', } as const diff --git a/src/Shared/Components/FloatingVariablesSuggestions/FloatingVariablesSuggestions.tsx b/src/Shared/Components/FloatingVariablesSuggestions/FloatingVariablesSuggestions.tsx index f685c4a07..44bad9c51 100644 --- a/src/Shared/Components/FloatingVariablesSuggestions/FloatingVariablesSuggestions.tsx +++ b/src/Shared/Components/FloatingVariablesSuggestions/FloatingVariablesSuggestions.tsx @@ -14,17 +14,16 @@ * limitations under the License. */ -import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' -import Draggable from 'react-draggable' +import React, { memo, useCallback, useState } from 'react' import Tippy from '@tippyjs/react' import { ReactComponent as ICDrag } from '@Icons/ic-drag.svg' +import { DraggablePositionVariant, DraggableWrapper } from '@Common/DraggableWrapper' import { useAsync } from '@Common/Helper' -import { useWindowSize } from '@Common/Hooks' import { ALLOW_ACTION_OUTSIDE_FOCUS_TRAP } from '@Shared/constants' import { Icon } from '../Icon' -import { SUGGESTIONS_SIZE } from './constants' +import { DRAG_SELECTOR } from './constants' import { getScopedVariables } from './service' import Suggestions from './Suggestions' import { FloatingVariablesSuggestionsProps } from './types' @@ -35,98 +34,26 @@ import { FloatingVariablesSuggestionsProps } from './types' * @param appId - To fetch the scoped variables * @param envId - (Optional) * @param clusterId - (Optional) - * @param bounds - (Optional) To set the bounds of the suggestions * @param hideObjectVariables - (Optional) To hide the object/array variables, default is true * @returns */ const FloatingVariablesSuggestions = ({ - zIndex, appId, envId, clusterId, - bounds, hideObjectVariables = true, showValueOnHover = true, isTemplateView, + boundaryGap, }: FloatingVariablesSuggestionsProps) => { const [isActive, setIsActive] = useState(false) - const [collapsedPosition, setCollapsedPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }) - const [expandedPosition, setExpandedPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }) const [loadingScopedVariables, variablesData, error, reloadScopedVariables] = useAsync( () => getScopedVariables(appId, envId, clusterId, { hideObjectVariables, isTemplateView }), [appId, envId, clusterId], ) - const windowSize = useWindowSize() - // In case of StrictMode, we get error findDOMNode is deprecated in StrictMode - // So we use useRef to get the DOM node - const nodeRef = useRef(null) - - // nodeRef.current is dependency even though its a ref as initially its null and we need to get the - // first value that it gets and after that is not going to trigger again - const initialPosition = useMemo(() => { - const initialPositionData = nodeRef.current?.getBoundingClientRect() || { - x: 0, - y: 0, - } - return { x: initialPositionData.x, y: initialPositionData.y } - }, [nodeRef.current]) - - // The size of the active state can expand say in case user expands SuggestionsInfo and the widget is at bottom of screen - useEffect(() => { - const resizeObserver = new ResizeObserver((entries) => { - if (entries?.length > 0 && isActive) { - const { height } = entries[0].contentRect - if (initialPosition.y + expandedPosition.y + height > windowSize.height) { - setExpandedPosition({ - x: expandedPosition.x, - y: windowSize.height - height - initialPosition.y, - }) - } - } - }) - resizeObserver.observe(nodeRef.current) - return () => { - resizeObserver.disconnect() - } - }, [isActive, expandedPosition, windowSize, initialPosition]) - const handleActivation = () => { - const currentPosInScreen = { - x: initialPosition.x + collapsedPosition.x, - y: initialPosition.y + collapsedPosition.y, - } - - setExpandedPosition({ - x: collapsedPosition.x, - y: collapsedPosition.y, - }) - - if (currentPosInScreen.y > windowSize.height - SUGGESTIONS_SIZE.height) { - setExpandedPosition({ - x: collapsedPosition.x, - y: windowSize.height - SUGGESTIONS_SIZE.height - initialPosition.y, - }) - } - - if (currentPosInScreen.x > windowSize.width - SUGGESTIONS_SIZE.width) { - setExpandedPosition({ - x: windowSize.width - SUGGESTIONS_SIZE.width - initialPosition.x, - y: collapsedPosition.y, - }) - } - - if ( - currentPosInScreen.x > windowSize.width - SUGGESTIONS_SIZE.width && - currentPosInScreen.y > windowSize.height - SUGGESTIONS_SIZE.height - ) { - setExpandedPosition({ - x: windowSize.width - SUGGESTIONS_SIZE.width - initialPosition.x, - y: windowSize.height - SUGGESTIONS_SIZE.height - initialPosition.y, - }) - } - setIsActive(true) } @@ -136,103 +63,66 @@ const FloatingVariablesSuggestions = ({ setIsActive(false) }, []) - // e will be unused, but we need to pass it as a parameter since Draggable expects it - const handleCollapsedDrag = (e, data: { x: number; y: number }) => { - const currentPosInScreen = { - x: initialPosition.x + data.x, - y: initialPosition.y + data.y, - } - if ( - currentPosInScreen.y < 0 || - currentPosInScreen.x < 0 || - currentPosInScreen.x + (nodeRef.current?.getBoundingClientRect().width || 0) > windowSize.width || - currentPosInScreen.y + (nodeRef.current?.getBoundingClientRect().height || 0) > windowSize.height - ) { - return - } - - setCollapsedPosition(data) - } - - const handleExpandedDrag = (e, data: { x: number; y: number }) => { - const currentPosInScreen = { - x: initialPosition.x + data.x, - y: initialPosition.y + data.y, - } - if ( - currentPosInScreen.y < 0 || - currentPosInScreen.x < 0 || - currentPosInScreen.x + (nodeRef.current?.getBoundingClientRect().width || 0) > windowSize.width || - currentPosInScreen.y + (nodeRef.current?.getBoundingClientRect().height || 0) > windowSize.height - ) { - return - } - setExpandedPosition(data) - // Only Need to retain the collapsed position if the user has not dragged the suggestions, so need to update - setCollapsedPosition(data) - } - - if (!isActive) { - return ( - -
+
+ - - - - - -
- - ) - } - return ( - -
- + + + +
+ +
+ +
+ +
+ +
+
-
+ ) } diff --git a/src/Shared/Components/FloatingVariablesSuggestions/Suggestions.tsx b/src/Shared/Components/FloatingVariablesSuggestions/Suggestions.tsx index 5dd91db7e..1e0057f3c 100644 --- a/src/Shared/Components/FloatingVariablesSuggestions/Suggestions.tsx +++ b/src/Shared/Components/FloatingVariablesSuggestions/Suggestions.tsx @@ -24,7 +24,7 @@ import { ALLOW_ACTION_OUTSIDE_FOCUS_TRAP, ComponentSizeType } from '@Shared/cons import { Button, ButtonStyleType, ButtonVariantType } from '../Button' import { Icon } from '../Icon' -import { NO_DEFINED_DESCRIPTION, NO_DEFINED_VALUE } from './constants' +import { DRAG_SELECTOR, NO_DEFINED_DESCRIPTION, NO_DEFINED_VALUE } from './constants' import SuggestionItem from './SuggestionItem' import SuggestionsInfo from './SuggestionsInfo' import { ScopedVariableType, SuggestionsProps } from './types' @@ -75,7 +75,9 @@ const Suggestions = ({ const renderHeader = (): JSX.Element => (
-
+

Scoped variables

diff --git a/src/Shared/Components/FloatingVariablesSuggestions/constants.ts b/src/Shared/Components/FloatingVariablesSuggestions/constants.ts index d2c1d6c5c..eaa74d7d8 100644 --- a/src/Shared/Components/FloatingVariablesSuggestions/constants.ts +++ b/src/Shared/Components/FloatingVariablesSuggestions/constants.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -export const SUGGESTIONS_SIZE = { - width: 356, - height: 504, -} - export const NO_DEFINED_DESCRIPTION = 'No Defined Description' export const NO_DEFINED_VALUE = 'No Defined Value' export const SUGGESTIONS_INFO_TITLE = 'What is scoped variable?' +export const DRAG_SELECTOR = 'handle-drag' diff --git a/src/Shared/Components/FloatingVariablesSuggestions/types.ts b/src/Shared/Components/FloatingVariablesSuggestions/types.ts index 1c5230fd3..c33e8ebae 100644 --- a/src/Shared/Components/FloatingVariablesSuggestions/types.ts +++ b/src/Shared/Components/FloatingVariablesSuggestions/types.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { DraggableBounds } from 'react-draggable' - +import { DraggableWrapperProps } from '@Common/DraggableWrapper' import { AppConfigProps } from '@Pages/index' export interface ScopedVariableType { @@ -27,12 +26,12 @@ export interface ScopedVariableType { isRedacted: boolean } -export interface FloatingVariablesSuggestionsProps extends Required> { - zIndex: number +export interface FloatingVariablesSuggestionsProps + extends Required>, + Pick { appId?: string envId?: string clusterId?: string - bounds?: DraggableBounds | string | false /** * This will hide the variables with object/array values if set to true * @default true diff --git a/src/Shared/Components/Header/PageHeader.tsx b/src/Shared/Components/Header/PageHeader.tsx index fd3f84b5d..8a30877f7 100644 --- a/src/Shared/Components/Header/PageHeader.tsx +++ b/src/Shared/Components/Header/PageHeader.tsx @@ -22,9 +22,9 @@ import { handleAnalyticsEvent } from '@Shared/Analytics' import { ComponentSizeType } from '@Shared/constants' import { InstallationType } from '@Shared/types' -import { TippyCustomized, TippyTheme, Tooltip } from '../../../Common' +import { TippyCustomized, TippyTheme } from '../../../Common' import { POSTHOG_EVENT_ONBOARDING } from '../../../Common/Constants' -import { SidePanelTab, useMainContext, useTheme, useUserEmail } from '../../Providers' +import { useMainContext, useTheme, useUserEmail } from '../../Providers' import { Button, ButtonStyleType, ButtonVariantType } from '../Button' import { Icon } from '../Icon' import { ImageWithFallback } from '../ImageWithFallback' @@ -52,8 +52,14 @@ const PageHeader = ({ closeIcon, docPath, }: PageHeaderType) => { - const { setLoginCount, setShowGettingStartedCard, setSidePanelConfig, sidePanelConfig, tempAppWindowConfig } = - useMainContext() + const { + setLoginCount, + setShowGettingStartedCard, + sidePanelConfig, + tempAppWindowConfig, + featureAskDevtronExpert, + AskDevtronButton, + } = useMainContext() const { showSwitchThemeLocationTippy, handleShowSwitchThemeLocationTippyChange } = useTheme() const { @@ -130,30 +136,13 @@ const PageHeader = ({
) - const onAskButtonClick = () => { - handleAnalyticsEvent({ - category: 'AI', - action: `HELP_ASK_DEVTRON_AI`, - }) - setSidePanelConfig((prev) => ({ ...prev, state: SidePanelTab.ASK_DEVTRON })) - } - const renderLogoutHelpSection = () => ( <> - {window._env_?.FEATURE_ASK_DEVTRON_EXPERT && + {AskDevtronButton && + featureAskDevtronExpert && sidePanelConfig.state === 'closed' && - !tempAppWindowConfig.open && ( - - - - )} + !tempAppWindowConfig.open && } + ) => { const { sortBy, @@ -124,6 +127,7 @@ const InternalTable = < rows, filter, filterData, + additionalProps, visibleColumns.find(({ field }) => field === sortBy)?.comparator, ) @@ -138,10 +142,14 @@ const InternalTable = < // useAsync hook for 'rows' scenario const [_areRowsLoading, rowsResult, rowsError, reloadRows] = useAsync( handleFiltering, - [searchKey, sortBy, sortOrder, rows, JSON.stringify(otherFilters), visibleColumns], + [searchKey, filter, sortBy, sortOrder, rows, JSON.stringify(otherFilters), visibleColumns], !!rows, ) + // NOTE: passing getRows to queryKey won't trigger a refetch + // since it is a function + const lastUpdatedGetRowsInstance = useMemo(() => new Date().toISOString(), [getRows]) + // useAsync hook for 'getRows' scenario const { isFetching: _areGetRowsLoading, @@ -158,7 +166,7 @@ const InternalTable = < searchKey, sortBy, sortOrder, - getRows, + lastUpdatedGetRowsInstance, offset, pageSize, JSON.stringify(otherFilters), @@ -188,7 +196,8 @@ const InternalTable = < } if (!areFilteredRowsLoading && !filteredRows?.length && !loading) { - return filtersVariant !== FiltersTypeEnum.NONE && areFiltersApplied ? ( + return filtersVariant !== FiltersTypeEnum.NONE && + (userProvidedAreFiltersApplied !== undefined ? userProvidedAreFiltersApplied : areFiltersApplied) ? ( ) diff --git a/src/Shared/Components/Table/TableContent.tsx b/src/Shared/Components/Table/TableContent.tsx index f7239c885..59c755fe6 100644 --- a/src/Shared/Components/Table/TableContent.tsx +++ b/src/Shared/Components/Table/TableContent.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useEffect, useMemo, useRef, useState } from 'react' +import { MouseEvent, useEffect, useMemo, useRef, useState } from 'react' import { Checkbox } from '@Common/Checkbox' import { DEFAULT_BASE_PAGE_SIZE } from '@Common/Constants' @@ -22,11 +22,23 @@ import { useEffectAfterMount } from '@Common/Helper' import { Pagination } from '@Common/Pagination' import { SortableTableHeaderCell } from '@Common/SortableTableHeaderCell' import { CHECKBOX_VALUE } from '@Common/Types' +import { Button, ButtonStyleType, ButtonVariantType } from '@Shared/Components/Button' +import { Icon } from '@Shared/Components/Icon' +import { ComponentSizeType } from '@Shared/constants' import { BulkSelection } from '../BulkSelection' import BulkSelectionActionWidget from './BulkSelectionActionWidget' -import { BULK_ACTION_GUTTER_LABEL, EVENT_TARGET, SHIMMER_DUMMY_ARRAY } from './constants' -import { BulkActionStateType, FiltersTypeEnum, PaginationEnum, SignalsType, TableContentProps } from './types' +import { ACTION_GUTTER_SIZE, BULK_ACTION_GUTTER_LABEL, EVENT_TARGET, SHIMMER_DUMMY_ARRAY } from './constants' +import { + BulkActionStateType, + ExpandedRowPrefixType, + FiltersTypeEnum, + PaginationEnum, + RowType, + SignalEnum, + SignalsType, + TableContentProps, +} from './types' import useTableWithKeyboardShortcuts from './useTableWithKeyboardShortcuts' import { getStickyColumnConfig, scrollToShowActiveElementIfNeeded } from './utils' @@ -53,14 +65,23 @@ const TableContent = < areFilteredRowsLoading, getRows, totalRows, + rowStartIconConfig, + onRowClick, }: TableContentProps) => { const rowsContainerRef = useRef(null) const parentRef = useRef(null) const bulkSelectionButtonRef = useRef(null) const headerRef = useRef(null) + const skipFocusActiveRowRef = useRef(false) const [bulkActionState, setBulkActionState] = useState(null) const [showBorderRightOnStickyElements, setShowBorderRightOnStickyElements] = useState(false) + const [expandState, _setExpandState] = useState>({}) + + const setExpandState: typeof _setExpandState = (value) => { + skipFocusActiveRowRef.current = true + _setExpandState(value) + } const { width: rowOnHoverComponentWidth, Component: RowOnHoverComponent } = rowActionOnHoverConfig || {} @@ -92,10 +113,51 @@ const TableContent = < .join(' '), } = resizableConfig ?? {} - const gridTemplateColumns = rowOnHoverComponentWidth + const { visibleRows, areAllRowsExpanded, isAnyRowExpandable } = useMemo(() => { + const normalizedFilteredRows = filteredRows ?? [] + + const paginatedRows = + paginationVariant !== PaginationEnum.PAGINATED || + (paginationVariant === PaginationEnum.PAGINATED && getRows) + ? normalizedFilteredRows + : normalizedFilteredRows.slice(offset, offset + pageSize) + + const _isAnyRowExpandable = paginatedRows.some((row) => !!row.expandableRows) + + const _areAllRowsExpanded = + _isAnyRowExpandable && + paginatedRows.reduce((acc, row) => { + if (row.expandableRows) { + return acc && !!expandState[row.id] + } + + return acc + }, true) + + const paginatedRowsWithExpandedRows = paginatedRows.flatMap((row) => { + if (row.expandableRows && expandState[row.id]) { + return [row, ...row.expandableRows] + } + + return [row] + }) + + return { + visibleRows: paginatedRowsWithExpandedRows, + areAllRowsExpanded: _areAllRowsExpanded, + isAnyRowExpandable: _isAnyRowExpandable, + } + }, [paginationVariant, offset, pageSize, filteredRows, expandState]) + + const gridTemplateColumnsWithoutExpandButton = rowOnHoverComponentWidth ? `${initialGridTemplateColumns} ${typeof rowOnHoverComponentWidth === 'number' ? `minmax(${rowOnHoverComponentWidth}px, 1fr)` : rowOnHoverComponentWidth}` : initialGridTemplateColumns + const gridTemplateColumns = + isAnyRowExpandable || rowStartIconConfig + ? `${ACTION_GUTTER_SIZE}px ${gridTemplateColumnsWithoutExpandButton}` + : gridTemplateColumnsWithoutExpandButton + useEffect(() => { const scrollEventHandler = () => { setShowBorderRightOnStickyElements(rowsContainerRef.current?.scrollLeft > 0) @@ -113,18 +175,6 @@ const TableContent = < const bulkSelectionCount = isBulkSelectionApplied ? totalRows : (getSelectedIdentifiersCount?.() ?? 0) - const visibleRows = useMemo(() => { - const normalizedFilteredRows = filteredRows ?? [] - - const paginatedRows = - paginationVariant !== PaginationEnum.PAGINATED || - (paginationVariant === PaginationEnum.PAGINATED && getRows) - ? normalizedFilteredRows - : normalizedFilteredRows.slice(offset, offset + pageSize) - - return paginatedRows - }, [paginationVariant, offset, pageSize, filteredRows]) - const isBEPagination = !!getRows const showPagination = @@ -140,7 +190,7 @@ const TableContent = < useEffectAfterMount(() => { setActiveRowIndex(0) - }, [offset, visibleRows]) + }, [offset]) useEffect(() => { setIdentifiers?.( @@ -155,7 +205,74 @@ const TableContent = < handleSorting(newSortBy) } + useEffect(() => { + if (!isAnyRowExpandable) { + return () => {} + } + + const getExpandCollapseRowHandler = + (state: boolean) => + ({ detail: { activeRowData } }) => { + if ((activeRowData as RowType).expandableRows) { + setExpandState((prev) => ({ + ...prev, + [activeRowData.id]: state, + })) + } + } + + const handleExpandRow = getExpandCollapseRowHandler(true) + const handleCollapseRow = getExpandCollapseRowHandler(false) + + const signals = EVENT_TARGET as SignalsType + + signals.addEventListener(SignalEnum.EXPAND_ROW, handleExpandRow) + signals.addEventListener(SignalEnum.COLLAPSE_ROW, handleCollapseRow) + + return () => { + signals.removeEventListener(SignalEnum.EXPAND_ROW, handleExpandRow) + signals.removeEventListener(SignalEnum.COLLAPSE_ROW, handleCollapseRow) + } + }, [isAnyRowExpandable]) + + useEffect(() => { + if (!onRowClick) { + return () => {} + } + + const handleEnterPress = ({ detail: { activeRowData } }) => { + onRowClick(activeRowData, activeRowData.id.startsWith('expanded-row-' satisfies ExpandedRowPrefixType)) + } + + const signals = EVENT_TARGET as SignalsType + + signals.addEventListener(SignalEnum.ENTER_PRESSED, handleEnterPress) + + return () => { + signals.removeEventListener(SignalEnum.ENTER_PRESSED, handleEnterPress) + } + }, [onRowClick]) + + const toggleExpandAll = (e: MouseEvent) => { + e.stopPropagation() + + setExpandState( + visibleRows.reduce((acc, row) => { + if ((row as RowType).expandableRows) { + acc[row.id] = !areAllRowsExpanded + } + + return acc + }, {}), + ) + } + const focusActiveRow = (node: HTMLDivElement) => { + if (skipFocusActiveRowRef.current) { + skipFocusActiveRowRef.current = false + return + } + if ( node && !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName.toUpperCase()) && @@ -180,14 +297,16 @@ const TableContent = < return Object.values(bulkSelectionState) } + const showIconOrExpandActionGutter = isBulkSelectionConfigured || !!rowStartIconConfig || isAnyRowExpandable + const renderRows = () => { - if (loading) { + if (loading && !visibleColumns.length) { return SHIMMER_DUMMY_ARRAY.map((shimmerRowLabel) => (
- {isBulkSelectionConfigured ?
: null} + {showIconOrExpandActionGutter ?
: null} {SHIMMER_DUMMY_ARRAY.map((shimmerCellLabel) => (
))} @@ -195,7 +314,7 @@ const TableContent = < )) } - if (areFilteredRowsLoading) { + if (areFilteredRowsLoading || (loading && visibleColumns.length)) { return SHIMMER_DUMMY_ARRAY.map((shimmerRowLabel) => (
+ {showIconOrExpandActionGutter ? ( +
+
+
+ ) : null} {visibleColumns.map(({ label }) => (
@@ -216,15 +340,37 @@ const TableContent = < return visibleRows.map((row, visibleRowIndex) => { const isRowActive = activeRowIndex === visibleRowIndex const isRowBulkSelected = !!bulkSelectionState[row.id] || isBulkSelectionApplied + const isExpandedRow = row.id.startsWith('expanded-row-' satisfies ExpandedRowPrefixType) + + const handleChangeActiveRowIndex = (e: MouseEvent) => { + e.stopPropagation() - const handleChangeActiveRowIndex = () => { setActiveRowIndex(visibleRowIndex) + + onRowClick?.(row, isExpandedRow) } const handleToggleBulkSelectionForRow = () => { handleToggleBulkSelectionOnRow(row) } + const toggleExpandRow = (e: MouseEvent) => { + e.stopPropagation() + + if ((row as RowType).expandableRows) { + setExpandState({ + ...expandState, + [row.id]: !expandState[row.id], + }) + } + } + + const hasBulkOrExpandAction = + (isAnyRowExpandable && !isExpandedRow && !!(row as RowType).expandableRows) || + !!bulkSelectionReturnValue + + const expandBtnOrRowStartIconGutterStickyConfig = getStickyColumnConfig(gridTemplateColumns, 0) + return (
+ {rowStartIconConfig && !isExpandedRow && ( +
+ +
+ )} + + {!isExpandedRow && !!(row as RowType).expandableRows ? ( +
+
+ ) : null} + + {/* empty div needed for alignment; therefore hide if rowStartIconConfig (only applies to parent rows) is present */} + {isAnyRowExpandable && + (isExpandedRow || (!(row as RowType).expandableRows && !rowStartIconConfig)) && ( +
+ )} + {visibleColumns.map(({ field, horizontallySticky: isStickyColumn, CellComponent }, index) => { const isBulkActionGutter = field === BULK_ACTION_GUTTER_LABEL const horizontallySticky = isStickyColumn || isBulkActionGutter const { className: stickyClassName = '', left: stickyLeftValue = '' } = horizontallySticky - ? getStickyColumnConfig(gridTemplateColumns, index) + ? getStickyColumnConfig( + gridTemplateColumns, + index + (isAnyRowExpandable || rowStartIconConfig ? 1 : 0), + ) : {} - if (isBulkActionGutter) { + if (isBulkActionGutter && !isExpandedRow) { return (
@@ -282,6 +478,9 @@ const TableContent = < row={row} filterData={filterData as any} isRowActive={isRowActive} + isExpandedRow={isExpandedRow} + isRowInExpandState={expandState[row.id]} + expandRowCallback={toggleExpandRow} {...additionalProps} /> ) : ( @@ -295,7 +494,7 @@ const TableContent = < ) })} - {RowOnHoverComponent && ( + {!isExpandedRow && RowOnHoverComponent && (
@@ -307,6 +506,8 @@ const TableContent = < }) } + const expandAllBtnStickyConfig = getStickyColumnConfig(gridTemplateColumns, 0) + return (
- {loading ? ( + {loading && !visibleColumns.length ? (
- {isBulkSelectionConfigured ?
: null} + {showIconOrExpandActionGutter ?
: null} {SHIMMER_DUMMY_ARRAY.map((label) => (
))} @@ -337,6 +538,32 @@ const TableContent = < gridTemplateColumns, }} > + {isAnyRowExpandable ? ( +
+
+ ) : null} + + {!isAnyRowExpandable && rowStartIconConfig &&
} + {visibleColumns.map( ( { @@ -346,6 +573,7 @@ const TableContent = < size, showTippyOnTruncate, horizontallySticky: isStickyColumn, + infoTooltipText, }, index, ) => { @@ -353,7 +581,12 @@ const TableContent = < const isBulkActionGutter = field === BULK_ACTION_GUTTER_LABEL const horizontallySticky = isStickyColumn || isBulkActionGutter const { className: stickyClassName = '', left: stickyLeftValue = '' } = - horizontallySticky ? getStickyColumnConfig(gridTemplateColumns, index) : {} + horizontallySticky + ? getStickyColumnConfig( + gridTemplateColumns, + index + (isAnyRowExpandable ? 1 : 0), + ) + : {} if (field === BULK_ACTION_GUTTER_LABEL) { return ( @@ -379,7 +612,7 @@ const TableContent = < return (
@@ -392,6 +625,7 @@ const TableContent = < triggerSorting={getTriggerSortingHandler(field)} showTippyOnTruncate={showTippyOnTruncate} disabled={areFilteredRowsLoading} + infoTooltipText={infoTooltipText} {...(isResizable ? { isResizable, handleResize, id: label } : { isResizable: false })} diff --git a/src/Shared/Components/Table/constants.ts b/src/Shared/Components/Table/constants.ts index 1b7ed8b0f..63005f738 100644 --- a/src/Shared/Components/Table/constants.ts +++ b/src/Shared/Components/Table/constants.ts @@ -29,3 +29,5 @@ export const DRAG_SELECTOR_IDENTIFIER = 'table-drag-selector' export const SHIMMER_DUMMY_ARRAY = [1, 2, 3] export const NO_ROWS_OR_GET_ROWS_ERROR = new Error('Neither rows nor getRows function provided') + +export const ACTION_GUTTER_SIZE = 24 diff --git a/src/Shared/Components/Table/styles.scss b/src/Shared/Components/Table/styles.scss index ed2fb7c1e..16bfa0e00 100644 --- a/src/Shared/Components/Table/styles.scss +++ b/src/Shared/Components/Table/styles.scss @@ -40,6 +40,13 @@ left: -20px; width: 20px; } + + &.expand-row-btn::before, + &.row-start-icon::before, + &.expanded-tree-line::before { + left: -24px; + width: 24px; + } } &--scrolled { @@ -69,6 +76,10 @@ outline: none; } + &--expanded-row:has(+ .generic-table__row--expanded-row) { + border-bottom: 0px; + } + &:hover, &:hover > *, &--active, @@ -94,6 +105,33 @@ display: inherit; } } + + &.with-start-icon-and-bulk-or-expand-action { + .bulk-action-checkbox { + display: none; + } + + .expand-row-btn { + display: none; + } + + &:hover, + &.generic-table__row--active, + &.generic-table__row--bulk-selected, + &.generic-table__row--is-expanded { + .row-start-icon { + display: none; + } + + .bulk-action-checkbox { + display: flex; + } + + .expand-row-btn { + display: flex; + } + } + } } .sortable-table-header__resize-btn:hover, @@ -105,4 +143,14 @@ transform: scaleY(var(--resize-btn-scale-multiplier)); } } + + .expanded-tree-line::after { + content: ''; + width: 1px; + height: 100%; + background: var(--N200); + left: calc(50% - 1px); // offset to left by width for perfect centering + top: 0; + position: absolute; + } } diff --git a/src/Shared/Components/Table/types.ts b/src/Shared/Components/Table/types.ts index 0d729b82a..a45d2b7f8 100644 --- a/src/Shared/Components/Table/types.ts +++ b/src/Shared/Components/Table/types.ts @@ -26,12 +26,15 @@ import { import { GenericEmptyStateType } from '@Common/index' import { PageSizeOption } from '@Common/Pagination/types' import { SortableTableHeaderCellProps, useResizableTableConfig } from '@Common/SortableTableHeaderCell' +import { IconsProps } from '@Shared/Components/Icon' import { useBulkSelection, UseBulkSelectionProps } from '../BulkSelection' export interface UseFiltersReturnType extends UseStateFiltersReturnType {} export enum SignalEnum { + COLLAPSE_ROW = 'collapse-row', + EXPAND_ROW = 'expand-row', ENTER_PRESSED = 'enter-pressed', DELETE_PRESSED = 'delete-pressed', ESCAPE_PRESSED = 'escape-pressed', @@ -87,13 +90,23 @@ type BaseColumnType = { size: SizeType horizontallySticky?: boolean -} +} & Pick -export type RowType = { +type CommonRowType = { id: string data: Data } +export type ExpandedRowPrefixType = 'expanded-row-' + +export type ExpandedRowType = CommonRowType & { + id: `${ExpandedRowPrefixType}${string}` +} + +export type RowType = CommonRowType & { + expandableRows?: Array> +} + export type RowsType = RowType[] export enum FiltersTypeEnum { @@ -117,6 +130,10 @@ export type CellComponentProps< ? UseFiltersReturnType : UseUrlFiltersReturnType isRowActive: boolean + isExpandedRow: boolean + isRowInExpandState: boolean + // NOTE: no action if the row is not expandable + expandRowCallback: (e: MouseEvent) => void } export type RowActionsOnHoverComponentProps< @@ -222,7 +239,11 @@ export type ViewWrapperProps< : {}) > -type FilterConfig = { +type FilterConfig< + FilterVariant extends FiltersTypeEnum, + RowData extends unknown, + AdditionalProps extends Record, +> = { filtersVariant: FilterVariant /** * Props for useUrlFilters/useStateFilters hooks @@ -236,12 +257,14 @@ type FilterConfig, filterData: UseFiltersReturnType) => boolean + : (row: RowType, filterData: UseFiltersReturnType, additionalProps: AdditionalProps) => boolean clearFilters?: FilterVariant extends FiltersTypeEnum.URL ? () => void : FilterVariant extends FiltersTypeEnum.STATE ? never : never + + areFiltersApplied?: FilterVariant extends FiltersTypeEnum.NONE ? never : boolean } export type InternalTableProps< @@ -307,6 +330,14 @@ export type InternalTableProps< handleToggleBulkSelectionOnRow: (row: RowType) => void ViewWrapper?: FunctionComponent> + + /** + * An icon as the first element of the row, that hides actions like expand or bulk select icons + * until user hovers over the row or the row has focus from keyboard navigation + */ + rowStartIconConfig?: Omit + + onRowClick?: (row: RowType, isExpandedRow: boolean) => void } & ( | { /** @@ -337,7 +368,7 @@ export type InternalTableProps< pageSizeOptions?: never } ) & - FilterConfig + FilterConfig export type UseResizableTableConfigWrapperProps< RowData extends unknown, @@ -390,6 +421,9 @@ export type TableProps< | 'ViewWrapper' | 'pageSizeOptions' | 'clearFilters' + | 'rowStartIconConfig' + | 'onRowClick' + | 'areFiltersApplied' > export type BulkActionStateType = string | null @@ -440,6 +474,8 @@ export interface TableContentProps< | 'rowActionOnHoverConfig' | 'pageSizeOptions' | 'getRows' + | 'rowStartIconConfig' + | 'onRowClick' >, RowsResultType { areFilteredRowsLoading: boolean diff --git a/src/Shared/Components/Table/useTableWithKeyboardShortcuts.ts b/src/Shared/Components/Table/useTableWithKeyboardShortcuts.ts index a7a67a14b..cb51044c9 100644 --- a/src/Shared/Components/Table/useTableWithKeyboardShortcuts.ts +++ b/src/Shared/Components/Table/useTableWithKeyboardShortcuts.ts @@ -101,6 +101,20 @@ const useTableWithKeyboardShortcuts = < ) useEffect(() => { + registerShortcut({ + keys: ['ArrowLeft'], + callback: () => { + dispatchEvent(SignalEnum.COLLAPSE_ROW) + }, + }) + + registerShortcut({ + keys: ['ArrowRight'], + callback: () => { + dispatchEvent(SignalEnum.EXPAND_ROW) + }, + }) + registerShortcut({ keys: ['ArrowDown'], callback: () => { @@ -142,6 +156,8 @@ const useTableWithKeyboardShortcuts = < unregisterShortcut(['Enter']) unregisterShortcut(['Backspace']) unregisterShortcut(['.']) + unregisterShortcut(['ArrowLeft']) + unregisterShortcut(['ArrowRight']) } }, [getMoveFocusToNextRowHandler, getMoveFocusToPreviousRowHandler, dispatchEvent]) diff --git a/src/Shared/Components/Table/utils.ts b/src/Shared/Components/Table/utils.ts index 8e9ff9580..5d0f4b46a 100644 --- a/src/Shared/Components/Table/utils.ts +++ b/src/Shared/Components/Table/utils.ts @@ -42,11 +42,12 @@ export const searchAndSortRows = < rows: TableProps['rows'], filter: TableProps['filter'], filterData: UseFiltersReturnType, + additionalProps: AdditionalProps, comparator?: Column['comparator'], ): Awaited['getRows']>> => { const { sortBy, sortOrder } = filterData ?? {} - const filteredRows = filter ? rows.filter((row) => filter(row, filterData)) : rows + const filteredRows = filter ? rows.filter((row) => filter(row, filterData, additionalProps)) : rows return { rows: diff --git a/src/Shared/Components/Textarea/Textarea.component.tsx b/src/Shared/Components/Textarea/Textarea.component.tsx index a50374557..8a6080be6 100644 --- a/src/Shared/Components/Textarea/Textarea.component.tsx +++ b/src/Shared/Components/Textarea/Textarea.component.tsx @@ -82,21 +82,22 @@ const Textarea = ({ }, []) const reInitHeight = () => { - const currentHeight = parseInt(textareaRef.current.style.height, 10) - let nextHeight = textareaRef.current.scrollHeight || 0 - - if (nextHeight < currentHeight || currentHeight > AUTO_EXPANSION_MAX_HEIGHT) { + const textarea = textareaRef.current + if (!textarea) { return } - if (nextHeight < MIN_HEIGHT) { - nextHeight = MIN_HEIGHT - } + textarea.style.height = 'auto' + let nextHeight = textarea.scrollHeight if (nextHeight > AUTO_EXPANSION_MAX_HEIGHT) { nextHeight = AUTO_EXPANSION_MAX_HEIGHT + textarea.style.overflowY = 'auto' + } else { + textarea.style.overflowY = 'hidden' } + nextHeight = Math.max(MIN_HEIGHT, nextHeight) updateRefsHeight(nextHeight) } diff --git a/src/Shared/Hooks/useGetResourceKindsOptions/index.ts b/src/Shared/Hooks/useGetResourceKindsOptions/index.ts index 86a41c642..0a1de1da2 100644 --- a/src/Shared/Hooks/useGetResourceKindsOptions/index.ts +++ b/src/Shared/Hooks/useGetResourceKindsOptions/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ +export { getProjectOptions } from './service' export type { UseGetResourceKindOptionsReturnType, UseGetResourceKindsOptionsProps } from './types' export { default as useGetResourceKindsOptions } from './useGetResourceKindsOptions' diff --git a/src/Shared/Providers/MainContextProvider/index.ts b/src/Shared/Providers/MainContextProvider/index.ts index 8c0dd4e1d..332cee39d 100644 --- a/src/Shared/Providers/MainContextProvider/index.ts +++ b/src/Shared/Providers/MainContextProvider/index.ts @@ -16,4 +16,4 @@ export * from './MainContextProvider' export type { MainContext, ReloadVersionConfigTypes, SidePanelConfig, TempAppWindowConfig } from './types' -export { SidePanelTab } from './types' +export { AIAgentContextSourceType, type AIAgentContextType, SidePanelTab } from './types' diff --git a/src/Shared/Providers/MainContextProvider/types.ts b/src/Shared/Providers/MainContextProvider/types.ts index f04949c4d..cff7ce452 100644 --- a/src/Shared/Providers/MainContextProvider/types.ts +++ b/src/Shared/Providers/MainContextProvider/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react' +import { Dispatch, FunctionComponent, MutableRefObject, ReactNode, SetStateAction } from 'react' import { SERVER_MODE } from '../../../Common' import { @@ -48,11 +48,71 @@ export interface SidePanelConfig { /** URL to documentation that should be displayed in the panel */ docLink: string | null aiSessionId?: string + isExpandedView?: boolean } -type AIAgentContextType = { - path: string - context: Record +export enum AIAgentContextSourceType { + APP_DETAILS = 'app-details', + RESOURCE_BROWSER_CLUSTER = 'resource-browser-cluster', +} + +export type AIAgentAppType = + | 'devtronApp' + | 'devtronHelmChart' + | 'externalHelmChart' + | 'externalArgoApp' + | 'externalFluxApp' + +type AIAgentAppDataMasterType = { + appId: number | string + appName: string + envId: number + envName: string + clusterId: number + namespace: string + appType: AIAgentAppType + fluxAppDeploymentType: string +} + +type AIAgentAppDataType = Pick< + AIAgentAppDataMasterType, + TRequiredFields +> & { + [K in Exclude]?: never +} & { + appType: TAppType +} + +type CommonContextDataType = Record & { + uiMarkup?: string +} + +export type AIAgentContextType = + | { + source: AIAgentContextSourceType.APP_DETAILS + data: + | AIAgentAppDataType< + 'devtronApp' | 'devtronHelmChart', + 'appId' | 'appName' | 'envId' | 'envName' | 'clusterId' + > + | AIAgentAppDataType<'externalHelmChart', 'appId' | 'appName' | 'clusterId' | 'namespace'> + | AIAgentAppDataType<'externalArgoApp', 'appName' | 'clusterId' | 'namespace'> + | (AIAgentAppDataType< + 'externalFluxApp', + 'appName' | 'clusterId' | 'namespace' | 'fluxAppDeploymentType' + > & + CommonContextDataType) + } + | { + source: AIAgentContextSourceType.RESOURCE_BROWSER_CLUSTER + data: { + clusterId: number + clusterName: string + } & CommonContextDataType + } + +export type DebugAgentContextType = AIAgentContextType & { + prompt?: string } export interface TempAppWindowConfig { @@ -118,6 +178,8 @@ type CommonMainContextProps = { setLicenseData: Dispatch> canFetchHelmAppStatus: boolean setIntelligenceConfig: Dispatch> + debugAgentContext: DebugAgentContextType | null + setDebugAgentContext: (aiAgentContext: DebugAgentContextType | null) => void setAIAgentContext: (aiAgentContext: AIAgentContextType) => void setSidePanelConfig: Dispatch> } & Pick @@ -152,6 +214,9 @@ export type MainContext = CommonMainContextProps & aiAgentContext: AIAgentContextType tempAppWindowConfig: TempAppWindowConfig setTempAppWindowConfig: Dispatch> + AIRecommendations?: FunctionComponent + featureAskDevtronExpert: EnvironmentDataValuesDTO['featureAskDevtronExpert'] + AskDevtronButton?: FunctionComponent } | { isLicenseDashboard: true @@ -170,6 +235,9 @@ export type MainContext = CommonMainContextProps & aiAgentContext: null tempAppWindowConfig: null setTempAppWindowConfig: null + AIRecommendations?: null + featureAskDevtronExpert?: null + AskDevtronButton?: null } ) diff --git a/src/Shared/Services/types.ts b/src/Shared/Services/types.ts index dd0ba0e8e..7a2fdf857 100644 --- a/src/Shared/Services/types.ts +++ b/src/Shared/Services/types.ts @@ -61,6 +61,7 @@ export interface EnvironmentDataValuesDTO extends Pick + metadata: Record prompt: string analyticsCategory: string } diff --git a/src/Shared/validations.tsx b/src/Shared/validations.tsx index 1f3d936a0..ac5d9cb75 100644 --- a/src/Shared/validations.tsx +++ b/src/Shared/validations.tsx @@ -561,3 +561,31 @@ export const validateCronExpression = (expression: string): ValidationResponseTy } } } + +export const validateAppName = (value: string): Required => { + const re = PATTERNS.APP_NAME + const regExp = new RegExp(re) + const test = regExp.test(value) + + if (value.length === 0) { + return { isValid: false, message: 'Please provide app name' } + } + + if (value.length < 3) { + return { isValid: false, message: MESSAGES.getMinCharMessage(3) } + } + + if (value.length > 30) { + return { isValid: false, message: MESSAGES.getMaxCharMessage(30) } + } + + if (!test) { + return { + isValid: false, + message: + "Min 3 chars; Start with alphabet; End with alphanumeric; Use only lowercase; Allowed:(-); Do not use 'spaces'", + } + } + + return { isValid: true, message: '' } +} diff --git a/src/index.ts b/src/index.ts index afb611a11..6ccacbbaa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -155,7 +155,6 @@ 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. * @@ -188,6 +187,7 @@ export interface customEnv { * @default false */ FEATURE_STORAGE_ENABLE?: boolean + FEATURE_ATHENA_DEBUG_MODE_ENABLE?: boolean /** Org ID for grafana */ GRAFANA_ORG_ID?: number }