(
- (acc, inlineFilter) => `${acc}-${inlineFilter.status}`,
- `${allResourceKindFilter.status}`,
- )
-
return (
diff --git a/src/Shared/Components/CICDHistory/types.tsx b/src/Shared/Components/CICDHistory/types.tsx
index 280b5124e..8fe87ce26 100644
--- a/src/Shared/Components/CICDHistory/types.tsx
+++ b/src/Shared/Components/CICDHistory/types.tsx
@@ -550,8 +550,8 @@ export interface DeploymentTemplateHistoryType {
baseTemplateConfiguration: DeploymentHistoryDetail
previousConfigAvailable: boolean
rootClassName?: string
- codeEditorKey?: string
}
+
export interface DeploymentHistoryDetailRes extends ResponseType {
result?: DeploymentHistoryDetail
}
diff --git a/src/Shared/Components/CodeEditor/CodeEditor.tsx b/src/Shared/Components/CodeEditor/CodeEditor.tsx
index 22838f114..b1cdcbcaa 100644
--- a/src/Shared/Components/CodeEditor/CodeEditor.tsx
+++ b/src/Shared/Components/CodeEditor/CodeEditor.tsx
@@ -33,7 +33,7 @@ import {
} from '@uiw/react-codemirror'
import { DEFAULT_JSON_SCHEMA_URI, MODES } from '@Common/Constants'
-import { cleanKubeManifest } from '@Common/Helper'
+import { cleanKubeManifest, noop } from '@Common/Helper'
import { getUniqueId } from '@Shared/Helpers'
import { AppThemeType, useTheme } from '@Shared/Providers'
@@ -48,7 +48,7 @@ import {
replaceAll,
showReplaceFieldState,
} from './Commands'
-import { codeEditorFindReplace, readOnlyTooltip, yamlHighlight } from './Extensions'
+import { getCodeEditorFindReplace, readOnlyTooltip, yamlHighlight } from './Extensions'
import { CodeEditorContextProps, CodeEditorProps } from './types'
import { getFoldGutterElement, getLanguageExtension, getValidationSchema, parseValueToCode } from './utils'
@@ -77,7 +77,6 @@ const CodeEditor = ({
onChange,
onOriginalValueChange,
onModifiedValueChange,
- readOnly,
placeholder,
diffView,
loading,
@@ -88,8 +87,15 @@ const CodeEditor = ({
onBlur,
onFocus,
autoFocus,
- disableSearch = false,
+ onSearchPanelOpen = noop,
+ onSearchBarAction = noop,
+ collapseUnchangedDiffView = false,
+ ...resProps
}: CodeEditorProps) => {
+ // DERIVED PROPS
+ const disableSearch = (collapseUnchangedDiffView || resProps.disableSearch) ?? false
+ const readOnly = (collapseUnchangedDiffView || resProps.readOnly) ?? false
+
// HOOKS
const { appTheme } = useTheme()
@@ -184,7 +190,7 @@ const CodeEditor = ({
defaultKeymap: false,
searchKeymap: false,
foldGutter: false,
- drawSelection: false,
+ drawSelection: true,
highlightActiveLineGutter: true,
tabSize,
}
@@ -197,23 +203,33 @@ const CodeEditor = ({
setLhsCode(newLhsValue)
}
+ const openSearchPanelWrapper: typeof openSearchPanel = (view) => {
+ onSearchPanelOpen()
+ return openSearchPanel(view)
+ }
+
+ const openSearchPanelWithReplaceWrapper: typeof openSearchPanelWithReplace = (view) => {
+ onSearchPanelOpen()
+ return openSearchPanelWithReplace(view)
+ }
+
// EXTENSIONS
const getBaseExtensions = (): Extension[] => [
basicSetup(basicSetupOptions),
themeExtension,
keymap.of([
...vscodeKeymap.filter(({ key }) => key !== 'Mod-Alt-Enter' && key !== 'Mod-Enter' && key !== 'Mod-f'),
- ...(!disableSearch ? [{ key: 'Mod-f', run: openSearchPanel, scope: 'editor search-panel' }] : []),
+ ...(!disableSearch ? [{ key: 'Mod-f', run: openSearchPanelWrapper, scope: 'editor search-panel' }] : []),
{ key: 'Mod-Enter', run: replaceAll, scope: 'editor search-panel' },
- { key: 'Mod-Alt-f', run: openSearchPanelWithReplace, scope: 'editor search-panel' },
+ { key: 'Mod-Alt-f', run: openSearchPanelWithReplaceWrapper, scope: 'editor search-panel' },
{ key: 'Escape', run: blurOnEscape, stopPropagation: true },
]),
indentationMarkers(),
- getLanguageExtension(mode),
+ getLanguageExtension(mode, collapseUnchangedDiffView),
foldingCompartment.of(foldConfig),
lintGutter(),
search({
- createPanel: codeEditorFindReplace,
+ createPanel: getCodeEditorFindReplace(onSearchBarAction),
}),
showReplaceFieldState,
...(mode === MODES.YAML ? [yamlHighlight] : []),
@@ -244,6 +260,7 @@ const CodeEditor = ({
codeEditorTheme,
basicSetup({
...basicSetupOptions,
+ drawSelection: false,
lineNumbers: false,
highlightActiveLine: false,
highlightActiveLineGutter: false,
@@ -277,6 +294,8 @@ const CodeEditor = ({
modifiedViewExtensions={modifiedViewExtensions}
extensions={extensions}
diffMinimapExtensions={diffMinimapExtensions}
+ collapseUnchanged={collapseUnchangedDiffView}
+ disableMinimap={collapseUnchangedDiffView}
/>
)
diff --git a/src/Shared/Components/CodeEditor/CodeEditorRenderer.tsx b/src/Shared/Components/CodeEditor/CodeEditorRenderer.tsx
index 7ec4f6408..29010db87 100644
--- a/src/Shared/Components/CodeEditor/CodeEditorRenderer.tsx
+++ b/src/Shared/Components/CodeEditor/CodeEditorRenderer.tsx
@@ -46,6 +46,8 @@ export const CodeEditorRenderer = ({
extensions,
autoFocus,
diffMinimapExtensions,
+ collapseUnchanged = false,
+ disableMinimap = false,
}: CodeEditorRendererProps) => {
// CONTEXTS
const { value, lhsValue, diffMode } = useCodeEditorContext()
@@ -59,6 +61,7 @@ export const CodeEditorRenderer = ({
// REFS
const codeMirrorRef = useRef()
const codeMirrorMergeParentRef = useRef()
+ const codeMirrorMergeRef = useRef()
const diffMinimapRef = useRef()
const diffMinimapParentRef = useRef()
@@ -160,9 +163,11 @@ export const CodeEditorRenderer = ({
handleLhsOnChange(val, vu)
}
- // Using `diffMinimapRef` instead of `diffMinimapInstance` since this extension captures the initial reference in a closure.
- // Changes to `diffMinimapInstance` won't be reflected after initialization, so we rely on `diffMinimapRef.current` for updates.
- updateDiffMinimapValues(diffMinimapRef.current, vu.transactions, 'a')
+ if (!disableMinimap) {
+ // Using `diffMinimapRef` instead of `diffMinimapInstance` since this extension captures the initial reference in a closure.
+ // Changes to `diffMinimapInstance` won't be reflected after initialization, so we rely on `diffMinimapRef.current` for updates.
+ updateDiffMinimapValues(diffMinimapRef.current, vu.transactions, 'a')
+ }
})
const modifiedUpdateListener = EditorView.updateListener.of((vu: ViewUpdate) => {
@@ -172,93 +177,125 @@ export const CodeEditorRenderer = ({
handleOnChange(val, vu)
}
- // Using `diffMinimapRef` instead of `diffMinimapInstance` since this extension captures the initial reference in a closure.
- // Changes to `diffMinimapInstance` won't be reflected after initialization, so we rely on `diffMinimapRef.current` for updates.
- updateDiffMinimapValues(diffMinimapRef.current, vu.transactions, 'b')
+ if (!disableMinimap) {
+ // Using `diffMinimapRef` instead of `diffMinimapInstance` since this extension captures the initial reference in a closure.
+ // Changes to `diffMinimapInstance` won't be reflected after initialization, so we rely on `diffMinimapRef.current` for updates.
+ updateDiffMinimapValues(diffMinimapRef.current, vu.transactions, 'b')
+ }
})
- useEffect(() => {
- // DIFF VIEW INITIALIZATION
- if (!loading && codeMirrorMergeParentRef.current) {
- const scanLimit = getScanLimit(lhsValue, value)
+ /**
+ * Initializes or reinitializes the CodeMirror merge view for diff comparison.
+ *
+ * This function:
+ * 1. Destroys any existing merge view instances to prevent memory leaks
+ * 2. Creates a new MergeView instance with the current values and configurations
+ * 3. Initializes the diff minimap if enabled
+ * 4. Updates the component state with the new instances
+ *
+ */
+ const initializeCodeMirrorMergeView = () => {
+ // Destroy existing merge view instance if it exists
+ codeMirrorMergeInstance?.destroy()
+ codeMirrorMergeRef.current?.destroy()
+
+ const codeMirrorMergeView = new MergeView({
+ a: {
+ doc: lhsValue,
+ extensions: [...originalViewExtensions, originalUpdateListener],
+ },
+ b: {
+ doc: value,
+ extensions: [...modifiedViewExtensions, modifiedUpdateListener],
+ },
+ ...(!readOnly ? { revertControls: 'a-to-b', renderRevertControl: getRevertControlButton } : {}),
+ diffConfig: { scanLimit: getScanLimit(lhsValue, value), timeout: 5000 },
+ parent: codeMirrorMergeParentRef.current,
+ collapseUnchanged: collapseUnchanged ? {} : undefined,
+ })
+
+ codeMirrorMergeRef.current = codeMirrorMergeView
+ setCodeMirrorMergeInstance(codeMirrorMergeView)
- codeMirrorMergeInstance?.destroy()
+ // MINIMAP INITIALIZATION
+ if (!disableMinimap && codeMirrorMergeView && diffMinimapParentRef.current) {
+ diffMinimapInstance?.destroy()
+ diffMinimapRef.current?.destroy()
- const codeMirrorMergeView = new MergeView({
+ const diffMinimapMergeView = new MergeView({
a: {
doc: lhsValue,
- extensions: [...originalViewExtensions, originalUpdateListener],
+ extensions: diffMinimapExtensions,
},
b: {
doc: value,
- extensions: [...modifiedViewExtensions, modifiedUpdateListener],
+ extensions: diffMinimapExtensions,
},
- ...(!readOnly ? { revertControls: 'a-to-b', renderRevertControl: getRevertControlButton } : {}),
- diffConfig: { scanLimit, timeout: 5000 },
- parent: codeMirrorMergeParentRef.current,
+ gutter: false,
+ diffConfig: { scanLimit: getScanLimit(lhsValue, value), timeout: 5000 },
+ parent: diffMinimapParentRef.current,
})
- setCodeMirrorMergeInstance(codeMirrorMergeView)
-
- // MINIMAP INITIALIZATION
- if (codeMirrorMergeView && diffMinimapParentRef.current) {
- diffMinimapInstance?.destroy()
- diffMinimapRef.current?.destroy()
-
- const diffMinimapMergeView = new MergeView({
- a: {
- doc: lhsValue,
- extensions: diffMinimapExtensions,
- },
- b: {
- doc: value,
- extensions: diffMinimapExtensions,
- },
- gutter: false,
- diffConfig: { scanLimit, timeout: 5000 },
- parent: diffMinimapParentRef.current,
- })
-
- diffMinimapRef.current = diffMinimapMergeView
- setDiffMinimapInstance(diffMinimapMergeView)
- }
+
+ diffMinimapRef.current = diffMinimapMergeView
+ setDiffMinimapInstance(diffMinimapMergeView)
+ }
+ }
+
+ useEffect(() => {
+ // DIFF VIEW INITIALIZATION
+ if (!loading && codeMirrorMergeParentRef.current) {
+ initializeCodeMirrorMergeView()
}
return () => {
setCodeMirrorMergeInstance(null)
setDiffMinimapInstance(null)
+ codeMirrorMergeRef.current = null
diffMinimapRef.current = null
}
- }, [loading, codemirrorMergeKey, diffMode])
-
- // Sync external changes of `lhsValue` and `value` state to the diff-editor state.
+ }, [loading, codemirrorMergeKey, diffMode, collapseUnchanged, disableMinimap])
+
+ /**
+ * Synchronizes external changes of `lhsValue` and `value` with the diff-editor state.
+ *
+ * When the external state (lhsValue for left-hand side or value for right-hand side) changes,
+ * we need to update the CodeMirror merge view to reflect these changes. This effect detects
+ * discrepancies between the current editor content and the external state.
+ *
+ * Instead of trying to update the existing editors directly (which can be complex and error-prone),
+ * we reinitialize the entire merge view when the external state differs from the editor content.
+ * This ensures a clean, consistent state that properly reflects the external data.
+ *
+ */
useEffect(() => {
- if (codeMirrorMergeInstance) {
- const originalDoc = codeMirrorMergeInstance.a.state.doc.toString()
- if (originalDoc !== lhsValue) {
- codeMirrorMergeInstance.a.dispatch({
- changes: { from: 0, to: originalDoc.length, insert: lhsValue || '' },
- })
- }
-
- const modifiedDoc = codeMirrorMergeInstance.b.state.doc.toString()
- if (modifiedDoc !== value) {
- codeMirrorMergeInstance.b.dispatch({
- changes: { from: 0, to: modifiedDoc.length, insert: value || '' },
- })
+ if (codeMirrorMergeRef.current) {
+ // Get the current content from both editors
+ const originalDoc = codeMirrorMergeRef.current.a.state.doc.toString()
+ const modifiedDoc = codeMirrorMergeRef.current.b.state.doc.toString()
+
+ // If either editor's content doesn't match the external state,
+ // reinitialize the entire merge view with the current values
+ if (originalDoc !== lhsValue || modifiedDoc !== value) {
+ initializeCodeMirrorMergeView()
}
}
- }, [lhsValue, value, codeMirrorMergeInstance])
+ }, [lhsValue, value])
// SCALING FACTOR UPDATER
useEffect(() => {
- setTimeout(() => {
- setScalingFactor(
- codeMirrorMergeInstance
- ? Math.min(codeMirrorMergeInstance.dom.clientHeight / codeMirrorMergeInstance.dom.scrollHeight, 1)
- : 1,
- )
- }, 100)
- }, [lhsValue, value, codeMirrorMergeInstance])
+ if (!disableMinimap) {
+ setTimeout(() => {
+ setScalingFactor(
+ codeMirrorMergeInstance
+ ? Math.min(
+ codeMirrorMergeInstance.dom.clientHeight / codeMirrorMergeInstance.dom.scrollHeight,
+ 1,
+ )
+ : 1,
+ )
+ }, 100)
+ }
+ }, [lhsValue, value, codeMirrorMergeInstance, disableMinimap])
const { codeEditorClassName, codeEditorHeight, codeEditorParentClassName } = getCodeEditorHeight(height)
@@ -286,12 +323,14 @@ export const CodeEditorRenderer = ({
ref={codeMirrorMergeParentRef}
className={`cm-merge-theme flex-grow-1 h-100 dc__overflow-hidden ${readOnly ? 'code-editor__read-only' : ''}`}
/>
-
+ {!disableMinimap && (
+
+ )}
) : (
{
+const FindReplace = ({ view, defaultQuery, defaultShowReplace, onSearchBarAction }: FindReplaceProps) => {
// STATES
const [query, setQuery] = useState(new SearchQuery({ search: '' }))
const [matchesCount, setMatchesCount] = useState({ count: 0, current: 1 })
@@ -103,6 +103,9 @@ const FindReplace = ({ view, defaultQuery, defaultShowReplace }: FindReplaceProp
search = query.search,
wholeWord = query.wholeWord,
}: FindReplaceQuery) => {
+ // Calling this irrespective of whether the query has changed or not
+ onSearchBarAction()
+
const newQuery = new SearchQuery({
caseSensitive,
regexp,
@@ -120,6 +123,7 @@ const FindReplace = ({ view, defaultQuery, defaultShowReplace }: FindReplaceProp
useEffect(() => {
if (!defaultQuery.eq(query)) {
+ onSearchBarAction()
setMatchesCount(getUpdatedSearchMatchesCount(defaultQuery, view))
setQuery(defaultQuery)
}
@@ -144,6 +148,7 @@ const FindReplace = ({ view, defaultQuery, defaultShowReplace }: FindReplaceProp
const onNext = (e?: MouseEvent) => {
e?.preventDefault()
e?.stopPropagation()
+ onSearchBarAction()
findNext(view)
setMatchesCount(getUpdatedSearchMatchesCount(query, view))
}
@@ -151,6 +156,7 @@ const FindReplace = ({ view, defaultQuery, defaultShowReplace }: FindReplaceProp
const onPrevious = (e?: MouseEvent) => {
e?.preventDefault()
e?.stopPropagation()
+ onSearchBarAction()
findPrevious(view)
setMatchesCount(getUpdatedSearchMatchesCount(query, view))
}
@@ -179,6 +185,7 @@ const FindReplace = ({ view, defaultQuery, defaultShowReplace }: FindReplaceProp
e.stopPropagation()
if (e.key === 'Enter') {
e.preventDefault()
+ onSearchBarAction()
replaceNext(view)
}
}
@@ -188,10 +195,12 @@ const FindReplace = ({ view, defaultQuery, defaultShowReplace }: FindReplaceProp
}
const onReplaceTextClick = () => {
+ onSearchBarAction()
replaceNext(view)
}
const onReplaceTextAllClick = () => {
+ onSearchBarAction()
replaceAll(view)
}
@@ -431,62 +440,65 @@ const FindReplace = ({ view, defaultQuery, defaultShowReplace }: FindReplaceProp
)
}
-export const codeEditorFindReplace = (view: EditorView): Panel => {
- const dom = document.createElement('div')
+export const getCodeEditorFindReplace =
+ (onSearchBarAction: CodeEditorProps['onSearchBarAction']) =>
+ (view: EditorView): Panel => {
+ const dom = document.createElement('div')
- const keydown = (e: KeyboardEvent) => {
- if (runScopeHandlers(view, e, 'search-panel')) {
- e.preventDefault()
- e.stopPropagation()
+ const keydown = (e: KeyboardEvent) => {
+ if (runScopeHandlers(view, e, 'search-panel')) {
+ e.preventDefault()
+ e.stopPropagation()
+ }
}
- }
- dom.className =
- 'code-editor__search mt-8 mb-4 mr-8 ml-auto p-5 bg__secondary dc__border br-4 dc__w-fit-content fs-14'
- dom.onkeydown = keydown
-
- const renderFindReplace = () => {
- render(
- ,
- dom,
- )
- }
+ dom.className =
+ 'code-editor__search mt-8 mb-4 mr-8 ml-auto p-5 bg__secondary dc__border br-4 dc__w-fit-content fs-14'
+ dom.onkeydown = keydown
+
+ const renderFindReplace = () => {
+ render(
+ ,
+ dom,
+ )
+ }
- const mount = () => {
- requestAnimationFrame(() => {
- const findField = document.querySelector('[data-code-editor-find]') as HTMLInputElement
- findField?.focus()
- findField?.select()
- })
- }
+ const mount = () => {
+ requestAnimationFrame(() => {
+ const findField = document.querySelector('[data-code-editor-find]') as HTMLInputElement
+ findField?.focus()
+ findField?.select()
+ })
+ }
- const update = ({ transactions, docChanged, state, startState }: ViewUpdate) => {
- transactions.forEach((tr) => {
- tr.effects.forEach((effect) => {
- if (effect.is(setSearchQuery)) {
- renderFindReplace()
- }
- if (effect.is(setShowReplaceField)) {
- renderFindReplace()
- }
+ const update = ({ transactions, docChanged, state, startState }: ViewUpdate) => {
+ transactions.forEach((tr) => {
+ tr.effects.forEach((effect) => {
+ if (effect.is(setSearchQuery)) {
+ renderFindReplace()
+ }
+ if (effect.is(setShowReplaceField)) {
+ renderFindReplace()
+ }
+ })
})
- })
- if (docChanged || state.readOnly !== startState.readOnly) {
- renderFindReplace()
+ if (docChanged || state.readOnly !== startState.readOnly) {
+ renderFindReplace()
+ }
}
- }
- renderFindReplace()
+ renderFindReplace()
- return {
- top: true,
- dom,
- mount,
- update,
+ return {
+ top: true,
+ dom,
+ mount,
+ update,
+ }
}
-}
diff --git a/src/Shared/Components/CodeEditor/codeEditor.scss b/src/Shared/Components/CodeEditor/codeEditor.scss
index cdaac94b2..bd1894d7e 100644
--- a/src/Shared/Components/CodeEditor/codeEditor.scss
+++ b/src/Shared/Components/CodeEditor/codeEditor.scss
@@ -133,7 +133,8 @@
border-bottom: none;
&:has(.code-editor__search) {
- z-index: 0;
+ width: fit-content;
+ margin-left: auto;
}
}
@@ -244,6 +245,20 @@
content: url("data:image/svg+xml,%3Csvg width='14' height='14' viewBox='0 0 14 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.24239 2.18666L1.43119 10.4987C1.3542 10.6317 1.3136 10.7827 1.31348 10.9364C1.31335 11.09 1.3537 11.241 1.43046 11.3742C1.50723 11.5073 1.6177 11.6179 1.75077 11.6947C1.88384 11.7716 2.0348 11.8121 2.18848 11.8121H11.8109C11.9646 11.8121 12.1155 11.7716 12.2486 11.6947C12.3817 11.6179 12.4921 11.5073 12.5689 11.3742C12.6457 11.241 12.686 11.09 12.6859 10.9364C12.6858 10.7827 12.6452 10.6317 12.5682 10.4987L7.75697 2.18666C7.68011 2.05387 7.56968 1.94363 7.43676 1.86699C7.30384 1.79034 7.15311 1.75 6.99968 1.75C6.84625 1.75 6.69551 1.79034 6.5626 1.86699C6.42968 1.94363 6.31925 2.05387 6.24239 2.18666Z' fill='%23F4BA63'/%3E%3Cpath d='M7.58333 5.68758C7.58333 5.36542 7.32217 5.10425 7 5.10425C6.67783 5.10425 6.41667 5.36542 6.41667 5.68758V7.87508C6.41667 8.19725 6.67783 8.45841 7 8.45841C7.32217 8.45841 7.58333 8.19725 7.58333 7.87508V5.68758Z' fill='%23000A14'/%3E%3Cpath d='M7.65625 9.84383C7.65625 10.2063 7.36244 10.5001 7 10.5001C6.63756 10.5001 6.34375 10.2063 6.34375 9.84383C6.34375 9.48139 6.63756 9.18758 7 9.18758C7.36244 9.18758 7.65625 9.48139 7.65625 9.84383Z' fill='%23000A14'/%3E%3C/svg%3E%0A");
}
+ // COLLAPSED
+ .cm-collapsedLines {
+ padding: 6px 12px;
+ background: var(--B50);
+ font-size: 14px;
+ line-height: 20px;
+ color: var(--B500);
+
+ &::before,
+ &::after {
+ content: none;
+ }
+ }
+
// SEARCH
.cm-searchMatch {
background-color: var(--bg-matching-keyword);
diff --git a/src/Shared/Components/CodeEditor/types.ts b/src/Shared/Components/CodeEditor/types.ts
index 1ffb86fd1..edb7a5b92 100644
--- a/src/Shared/Components/CodeEditor/types.ts
+++ b/src/Shared/Components/CodeEditor/types.ts
@@ -51,6 +51,12 @@ type CodeEditorDiffBaseProps = {
originalValue?: ReactCodeMirrorProps['value']
modifiedValue?: ReactCodeMirrorProps['value']
isOriginalModifiable?: boolean
+ /**
+ * When true, renders a diff view in readOnly mode with collapsed unchanged diffs.
+ * This disables the minimap, code-editor search functionality, and language linting.
+ * @default false
+ */
+ collapseUnchangedDiffView?: boolean
}
type CodeEditorPropsBasedOnDiffView = DiffView extends true
@@ -78,6 +84,11 @@ export type CodeEditorProps = {
disableSearch?: boolean
diffView?: DiffView
theme?: AppThemeType
+ onSearchPanelOpen?: () => void
+ /**
+ * This method is triggered when user types something in the search/replace bar or applies a search or replace action.
+ */
+ onSearchBarAction?: () => void
} & CodeEditorPropsBasedOnDiffView
export interface GetCodeEditorHeightReturnType {
@@ -96,7 +107,7 @@ export type FindReplaceQuery = Partial<
Pick
>
-export interface FindReplaceProps {
+export interface FindReplaceProps extends Pick {
view: EditorView
/** Default value for Search Query state. */
defaultQuery: SearchQuery
@@ -146,6 +157,8 @@ export type CodeEditorRendererProps = Required<
modifiedViewExtensions: ReactCodeMirrorProps['extensions']
extensions: ReactCodeMirrorProps['extensions']
diffMinimapExtensions: ReactCodeMirrorProps['extensions']
+ collapseUnchanged?: boolean
+ disableMinimap?: boolean
}
export interface DiffMinimapProps extends Pick {
diff --git a/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx b/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx
index 9a9d6f550..583de9898 100644
--- a/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx
+++ b/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx
@@ -25,6 +25,7 @@ import { Backdrop } from '../Backdrop'
import { Button, ButtonStyleType, ButtonVariantType } from '../Button'
import { Confetti } from '../Confetti'
import { CustomInput } from '../CustomInput'
+import { Icon } from '../Icon'
import { useConfirmationModalContext } from './ConfirmationModalContext'
import { ConfirmationModalBodyProps, ConfirmationModalProps, ConfirmationModalVariantType } from './types'
import { getConfirmationLabel, getIconFromVariant, getPrimaryButtonStyleFromVariant } from './utils'
@@ -34,7 +35,7 @@ import './confirmationModal.scss'
const ConfirmationModalBody = ({
title,
subtitle,
- Icon,
+ Icon: ButtonIcon,
variant,
buttonConfig,
confirmationConfig,
@@ -53,8 +54,8 @@ const ConfirmationModalBody = ({
const { primaryButtonConfig, secondaryButtonConfig } = buttonConfig
- const RenderIcon = Icon ?? getIconFromVariant(variant)
- const hideIcon = variant === ConfirmationModalVariantType.custom && !Icon
+ const RenderIcon = ButtonIcon ?? getIconFromVariant(variant)
+ const hideIcon = variant === ConfirmationModalVariantType.custom && !ButtonIcon
const disablePrimaryButton: boolean =
('disabled' in primaryButtonConfig && primaryButtonConfig.disabled) ||
@@ -159,7 +160,7 @@ const ConfirmationModalBody = ({
text={primaryButtonConfig.text}
onClick={primaryButtonConfig.onClick as ButtonHTMLAttributes['onClick']}
startIcon={primaryButtonConfig.startIcon}
- endIcon={primaryButtonConfig.endIcon}
+ endIcon={primaryButtonConfig.endIcon || }
/>
)}
diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.utils.tsx b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.utils.tsx
index b57c4cbf4..46c27b858 100644
--- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.utils.tsx
+++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiff.utils.tsx
@@ -20,7 +20,7 @@ import { ReactComponent as ICCheck } from '@Icons/ic-check.svg'
import { ReactComponent as ICCheckCircleDots } from '@Icons/ic-check-circle-dots.svg'
import { ReactComponent as ICEditFile } from '@Icons/ic-edit-file.svg'
import { ReactComponent as ICFileCode } from '@Icons/ic-file-code.svg'
-import { deepEqual, noop, YAMLStringify } from '@Common/Helper'
+import { deepEqual, YAMLStringify } from '@Common/Helper'
import {
AppEnvDeploymentConfigListParams,
DeploymentConfigDiffProps,
@@ -28,7 +28,6 @@ import {
DeploymentHistoryDetail,
DeploymentHistorySingleValue,
DiffHeadingDataType,
- GenericSectionErrorState,
prepareHistoryData,
} from '@Shared/Components'
import { DEPLOYMENT_HISTORY_CONFIGURATION_LIST_MAP } from '@Shared/constants'
@@ -47,7 +46,6 @@ import {
TemplateListDTO,
TemplateListType,
} from '../../Services/app.types'
-import { DiffViewerProps } from '../DiffViewer/types'
export const getDeploymentTemplateData = (data: DeploymentTemplateDTO) => {
const parsedDraftData = JSON.parse(data?.deploymentDraftData?.configData[0].draftMetadata.data || null)
@@ -911,21 +909,3 @@ export const getDefaultVersionAndPreviousDeploymentOptions = (data: TemplateList
previousDeployments: [],
},
)
-
-export const renderDiffViewNoDifferenceState = (
- lhsValue: string,
- rhsValue: string,
-): DiffViewerProps['codeFoldMessageRenderer'] =>
- lhsValue === rhsValue
- ? () => (
-
- )
- : null
diff --git a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffMain.tsx b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffMain.tsx
index 595f437a5..db751dd79 100644
--- a/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffMain.tsx
+++ b/src/Shared/Components/DeploymentConfigDiff/DeploymentConfigDiffMain.tsx
@@ -18,14 +18,14 @@ import { Fragment, useEffect, useState } from 'react'
import { ReactComponent as ICSort } from '@Icons/ic-arrow-up-down.svg'
import { ReactComponent as ICSortArrowDown } from '@Icons/ic-sort-arrow-down.svg'
-import { SortingOrder } from '@Common/Constants'
+import { MODES, SortingOrder } from '@Common/Constants'
import ErrorScreenManager from '@Common/ErrorScreenManager'
import { Progressing } from '@Common/Progressing'
-import { DiffViewer } from '@Shared/Components/DiffViewer'
import { ComponentSizeType } from '@Shared/constants'
import { Button, ButtonStyleType, ButtonVariantType } from '../Button'
import { DeploymentHistoryDiffView } from '../CICDHistory'
+import { CodeEditor } from '../CodeEditor'
import { SelectPicker } from '../SelectPicker'
import { ToggleResolveScopedVariables } from '../ToggleResolveScopedVariables'
import {
@@ -34,7 +34,6 @@ import {
DeploymentConfigDiffSelectPickerProps,
DeploymentConfigDiffState,
} from './DeploymentConfigDiff.types'
-import { renderDiffViewNoDifferenceState } from './DeploymentConfigDiff.utils'
import { DeploymentConfigDiffAccordion } from './DeploymentConfigDiffAccordion'
export const DeploymentConfigDiffMain = ({
@@ -191,19 +190,25 @@ export const DeploymentConfigDiffMain = ({
hideDiffState={hideDiffState}
>
{singleView ? (
-
+ <>
+
+
+ >
) : (
{primaryHeading && secondaryHeading && (
@@ -213,7 +218,6 @@ export const DeploymentConfigDiffMain = ({
)}
+
{!!headerText &&
{headerText}
}
diff --git a/src/Shared/Components/DiffViewer/DiffViewer.component.tsx b/src/Shared/Components/DiffViewer/DiffViewer.component.tsx
deleted file mode 100644
index 863d20cca..000000000
--- a/src/Shared/Components/DiffViewer/DiffViewer.component.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (c) 2024. Devtron Inc.
- *
- * 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 ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued'
-
-import { diffViewerStyles } from './constants'
-import { DiffViewerProps, DiffViewTitleWrapperProps } from './types'
-
-const DiffViewTitleWrapper = ({ title }: DiffViewTitleWrapperProps) =>
{title}
-
-/**
- * Component for showing diff between two string or object.
- *
- * Note: Pass down the object as stringified for optimized performance.
- *
- * @example Usage
- *
- * ```tsx
- *
- * ```
- *
- * @example With left/right title for lhs/rhs
- *
- * ```tsx
- *
Title for RHS
- * }
- * />
- * ```
- *
- * @example With custom message for folded code
- * Note: the entire section would be clickable
- *
- * ```tsx
- * Custom text }
- * />
- * ```
- */
-const DiffViewer = ({ oldValue, newValue, leftTitle, rightTitle, ...props }: DiffViewerProps) => (
- : null}
- rightTitle={rightTitle ? : null}
- compareMethod={DiffMethod.WORDS}
- styles={diffViewerStyles}
- />
-)
-
-export default DiffViewer
diff --git a/src/Shared/Components/DiffViewer/constants.ts b/src/Shared/Components/DiffViewer/constants.ts
deleted file mode 100644
index 1c7078dbf..000000000
--- a/src/Shared/Components/DiffViewer/constants.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (c) 2024. Devtron Inc.
- *
- * 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.
- */
-
-// Default variables and style keys
-
-import { ReactDiffViewerProps } from 'react-diff-viewer-continued'
-
-export const diffViewerStyles: ReactDiffViewerProps['styles'] = {
- variables: {
- light: {
- diffViewerBackground: 'var(--bg-primary)',
- diffViewerColor: 'var(--N900)',
- addedBackground: 'var(--G50)',
- addedColor: 'var(--N900)',
- removedBackground: 'var(--R50)',
- removedColor: 'var(--N900)',
- wordAddedBackground: 'var(--G200)',
- wordRemovedBackground: 'var(--R200)',
- addedGutterBackground: 'var(--G100)',
- removedGutterBackground: 'var(--R100)',
- gutterBackground: 'var(--bg-secondary)',
- gutterBackgroundDark: 'var(--bg-secondary)',
- highlightBackground: 'var(--N100)',
- highlightGutterBackground: 'var(--N100)',
- codeFoldGutterBackground: 'var(--B100)',
- codeFoldBackground: 'var(--B50)',
- emptyLineBackground: 'var(--bg-primary)',
- gutterColor: 'var(--N500)',
- addedGutterColor: 'var(--N700)',
- removedGutterColor: 'var(--N700)',
- codeFoldContentColor: 'var(--B600)',
- diffViewerTitleBackground: 'var(--N100)',
- diffViewerTitleColor: 'var(--N700)',
- diffViewerTitleBorderColor: 'var(--N200)',
- },
- },
- diffContainer: {
- fontSize: '14px',
- fontWeight: 400,
- lineHeight: '20px',
-
- pre: {
- fontSize: '14px',
- lineHeight: '20px',
- fontFamily: 'Inconsolata, monospace',
- wordBreak: 'break-word',
- // Reset for styling from patternfly
- padding: 0,
- margin: 0,
- backgroundColor: 'transparent',
- border: 'none',
- borderRadius: 0,
- },
- },
- marker: {
- pre: {
- display: 'none',
- },
- },
- gutter: {
- padding: `0 6px`,
- minWidth: '36px',
- // Cursor would be default for all cases in gutter till we don't support highlighting
- cursor: 'default',
-
- pre: {
- opacity: 1,
- },
- },
- wordDiff: {
- padding: 0,
- },
- wordAdded: {
- paddingInline: '2px',
- lineHeight: '16px',
- },
- wordRemoved: {
- paddingInline: '2px',
- lineHeight: '16px',
- },
- codeFold: {
- fontSize: '14px',
- fontWeight: 400,
- lineHeight: '20px',
- height: '32px',
-
- a: {
- textDecoration: 'none !important',
- },
- },
- codeFoldGutter: {
- '+ td': {
- width: '12px',
- },
- },
- titleBlock: {
- padding: '8px 12px',
- fontSize: '12px',
- lineHeight: '20px',
- fontWeight: 600,
- borderBottom: 'none',
-
- pre: {
- fontFamily: 'Open Sans',
- },
- },
-}
diff --git a/src/Shared/Components/DiffViewer/index.ts b/src/Shared/Components/DiffViewer/index.ts
deleted file mode 100644
index 0abf9bc28..000000000
--- a/src/Shared/Components/DiffViewer/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * Copyright (c) 2024. Devtron Inc.
- *
- * 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.
- */
-
-export { default as DiffViewer } from './DiffViewer.component'
diff --git a/src/Shared/Components/DiffViewer/types.ts b/src/Shared/Components/DiffViewer/types.ts
deleted file mode 100644
index 8b4958c6e..000000000
--- a/src/Shared/Components/DiffViewer/types.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright (c) 2024. Devtron Inc.
- *
- * 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 { ReactNode } from 'react'
-import { ReactDiffViewerProps } from 'react-diff-viewer-continued'
-
-export interface DiffViewerProps
- extends Pick {
- leftTitle?: ReactDiffViewerProps['leftTitle'] | ReactNode
- rightTitle?: ReactDiffViewerProps['rightTitle'] | ReactNode
-}
-
-export interface DiffViewTitleWrapperProps {
- title: DiffViewerProps['leftTitle']
-}
diff --git a/src/Shared/Components/DocLink/DocLink.tsx b/src/Shared/Components/DocLink/DocLink.tsx
new file mode 100644
index 000000000..bbb698abd
--- /dev/null
+++ b/src/Shared/Components/DocLink/DocLink.tsx
@@ -0,0 +1,61 @@
+import { MouseEvent } from 'react'
+
+import { DOCUMENTATION_HOME_PAGE } from '@Common/Constants'
+import { Button, ButtonComponentType, ButtonVariantType, Icon } from '@Shared/Components'
+import { ComponentSizeType } from '@Shared/constants'
+import { useMainContext } from '@Shared/Providers'
+
+import { DocLinkProps } from './types'
+import { getDocumentationUrl } from './utils'
+
+export const DocLink = ({
+ docLinkKey,
+ text = 'Learn more',
+ dataTestId,
+ startIcon,
+ showExternalIcon,
+ onClick,
+ fontWeight,
+ size = ComponentSizeType.medium,
+ variant = ButtonVariantType.text,
+ isExternalLink,
+ openInNewTab = false,
+ fullWidth = false,
+}: DocLinkProps) => {
+ // HOOKS
+ const { isEnterprise, setSidePanelConfig } = useMainContext()
+
+ // CONSTANTS
+ const documentationLink = getDocumentationUrl({
+ docLinkKey,
+ isEnterprise,
+ isExternalLink,
+ })
+
+ // HANDLERS
+ const handleClick = (e: MouseEvent) => {
+ if (!isExternalLink && !openInNewTab && !e.metaKey && documentationLink.startsWith(DOCUMENTATION_HOME_PAGE)) {
+ e.preventDefault()
+ setSidePanelConfig((prev) => ({ ...prev, open: true, docLink: documentationLink, reinitialize: true }))
+ }
+ onClick?.(e)
+ }
+
+ return (
+ }
+ fullWidth={fullWidth}
+ fontWeight={fontWeight}
+ />
+ )
+}
diff --git a/src/Shared/Components/DocLink/constants.ts b/src/Shared/Components/DocLink/constants.ts
new file mode 100644
index 000000000..f1a32de92
--- /dev/null
+++ b/src/Shared/Components/DocLink/constants.ts
@@ -0,0 +1,106 @@
+import { DOCUMENTATION_HOME_PAGE } from '@Common/Constants'
+
+export const DOCUMENTATION = {
+ ADMIN_PASSWORD: 'install/install-devtron#devtron-admin-credentials',
+ APP_CI_CONFIG_BUILD_WITHOUT_DOCKER:
+ 'usage/applications/creating-application/docker-build-configuration#build-docker-image-without-dockerfile',
+ APP_CREATE: 'usage/applications/create-application',
+ APP_CREATE_CI_CONFIG: 'usage/applications/creating-application/docker-build-configuration',
+ APP_CREATE_CONFIG_MAP: 'usage/applications/creating-application/config-maps',
+ APP_CREATE_ENVIRONMENT_OVERRIDE: 'usage/applications/creating-application/environment-overrides',
+ APP_CREATE_MATERIAL: 'usage/applications/creating-application/git-material',
+ APP_CREATE_SECRET: 'usage/applications/creating-application/secrets',
+ APP_CREATE_WORKFLOW: 'usage/applications/creating-application/workflow',
+ APP_DEPLOYMENT_TEMPLATE: 'usage/applications/creating-application/deployment-template',
+ APP_EPHEMERAL_CONTAINER: 'usage/applications/app-details/ephemeral-containers',
+ APP_METRICS: 'usage/applications/app-details/app-metrics',
+ APP_OVERVIEW_TAGS: 'usage/applications/overview#manage-tags',
+ APP_ROLLOUT_DEPLOYMENT_TEMPLATE: 'usage/applications/creating-application/deployment-template/rollout-deployment',
+ BUILD_STAGE: 'usage/applications/creating-application/workflow/ci-pipeline#build-stage',
+ APP_TAGS: 'usage/applications/create-application#tags',
+ BLOB_STORAGE: 'configurations-overview/installation-configuration#configuration-of-blob-storage',
+ BULK_UPDATE: 'usage/bulk-update',
+ CHART_GROUP: 'usage/deploy-chart/chart-group',
+ CHART_LIST: 'usage/deploy-chart/overview-of-charts',
+ CHART_STORE_METRICS_SERVER: 'dashboard//chart-store/discover?appStoreName=metrics-server',
+ CUSTOM_VALUES: 'usage/deploy-chart/overview-of-charts#custom-values',
+ DEPLOYMENT: 'usage/applications/creating-application/deployment-template/deployment',
+ DEPLOYMENT_TEMPLATE: 'usage/applications/creating-application/deployment-template',
+ DEVTRON_UPGRADE: 'getting-started/upgrade',
+ CONFIGURING_WEBHOOK: 'usage/applications/creating-application/workflow/ci-pipeline#configuring-webhook',
+ ENTERPRISE_LICENSE: 'enterprise-license',
+ EXECUTE_CUSTOM_SCRIPT:
+ 'usage/applications/creating-application/workflow/ci-pipeline/ci-build-pre-post-plugins#execute-custom-script',
+ EXTERNAL_LINKS: 'getting-started/global-configurations/external-links',
+ EXTERNAL_SECRET: 'usage/applications/creating-application/secrets#external-secrets',
+ HOME_PAGE: 'https://devtron.ai',
+ DOC_HOME_PAGE: DOCUMENTATION_HOME_PAGE,
+ KUBE_CONFIG: 'usage/resource-browser#running-kubectl-commands-locally',
+ JOBS: 'usage/jobs',
+ TAINT: 'usage/resource-browser#taint-a-node',
+
+ // Global Configurations
+ GLOBAL_CONFIG_API_TOKEN: 'getting-started/global-configurations/authorization/api-tokens',
+ GLOBAL_CONFIG_BUILD_INFRA: 'global-configurations/build-infra',
+ GLOBAL_CONFIG_CHART: 'getting-started/global-configurations/chart-repo',
+ GLOBAL_CONFIG_CLUSTER: 'getting-started/global-configurations/cluster-and-environments',
+ GLOBAL_CONFIG_CUSTOM_CHART: 'getting-started/global-configurations/custom-charts',
+ GLOBAL_CONFIG_CUSTOM_CHART_PRE_REQUISITES: 'global-configurations/deployment-charts#preparing-a-deployment-chart',
+ GLOBAL_CONFIG_DOCKER: 'getting-started/global-configurations/container-registries',
+ GLOBAL_CONFIG_GIT: 'getting-started/global-configurations/git-accounts',
+ GLOBAL_CONFIG_GITOPS: 'global-configurations/gitops',
+ GLOBAL_CONFIG_GITOPS_GITHUB: 'global-configurations/gitops#github',
+ GLOBAL_CONFIG_GITOPS_GITLAB: 'global-configurations/gitops#gitlab',
+ GLOBAL_CONFIG_GITOPS_AZURE: 'global-configurations/gitops#azure',
+ GLOBAL_CONFIG_GITOPS_BITBUCKET: 'global-configurations/gitops#bitbucket',
+ GLOBAL_CONFIG_GROUPS: 'getting-started/global-configurations/authorization/permission-groups',
+ GLOBAL_CONFIG_HOST_URL: 'global-configurations/host-url',
+ GLOBAL_CONFIG_NOTIFICATION: 'getting-started/global-configurations/manage-notification',
+ GLOBAL_CONFIG_PERMISSION: 'global-configurations/authorization/user-access#devtron-apps-permissions',
+ GLOBAL_CONFIG_PROJECT: 'global-configurations/projects',
+ GLOBAL_CONFIG_SSO: 'getting-started/global-configurations/sso-login',
+ GLOBAL_CONFIG_SCOPED_VARIABLES: 'getting-started/global-configurations/scoped-variables',
+ GLOBAL_CONFIG_USER: 'getting-started/global-configurations/authorization/user-access',
+ HYPERION: 'usage/applications#view-external-helm-app-listing',
+ JOB_CRONJOB: 'usage/applications/creating-application/deployment-template/job-and-cronjob',
+ JOB_SOURCE_CODE: 'usage/jobs/configuration-job',
+ JOB_WORKFLOW_EDITOR: 'usage/jobs/workflow-editor-job',
+ K8S_RESOURCES_PERMISSIONS: 'global-configurations/authorization/user-access#kubernetes-resources-permissions',
+ PRE_POST_BUILD_STAGE: 'usage/applications/creating-application/ci-pipeline/ci-build-pre-post-plugins',
+ ROLLOUT: 'usage/applications/creating-application/deployment-template/rollout-deployment',
+ SECURITY: 'usage/security-features',
+ SPECIFY_IMAGE_PULL_SECRET: 'getting-started/global-configurations/container-registries#specify-image-pull-secret',
+ TENANT_INSTALLATION: 'usage/software-distribution-hub/tenants',
+
+ // ENTERPRISE
+ CEL: 'https://github.com/google/cel-spec/blob/master/doc/langdef.md',
+ KUBERNETES_LABELS: 'https://kubernetes.io/docs/concepts/overview/working-with-objects/labels',
+ IMAGE_PROMOTION: 'global-configurations/image-promotion-policy',
+ IMAGE_PROMOTION_ASSIGN_TO: 'global-configurations/image-promotion-policy#applying-an-image-promotion-policy',
+ TAGS: 'usage/applications/create-application#tags',
+ TAGS_POLICY: 'global-configurations/tags-policy',
+ RESOURCE_WATCHER: 'usage/resource-watcher',
+ GITOPS_BITBUCKET: 'global-configurations/gitops#bitbucket',
+ DEPLOYMENT_CONFIGS: 'resources/glossary#base-deployment-template',
+ RJSF_PLAYGROUND: 'https://rjsf-team.github.io/react-jsonschema-form/',
+
+ // GLOBAL CONFIGURATION
+ GLOBAL_CONFIG_DEPLOYMENT_WINDOW: 'global-configurations/deployment-window',
+ GLOBAL_CONFIG_CATALOG_FRAMEWORK: 'global-configurations/catalog-framework',
+ GLOBAL_CONFIG_DEVTRON_APP_TEMPLATES: 'global-configurations',
+ GLOBAL_CONFIG_FILTER_CONDITION: 'global-configurations/filter-condition',
+ GLOBAL_CONFIG_LOCK_DEPLOYMENT_CONFIG: 'global-configurations/lock-deployment-config',
+ GLOBAL_CONFIG_PLUGINS_POLICY: 'global-configurations/plugins-policy',
+ GLOBAL_CONFIG_APPROVAL_POLICY: 'global-configurations/approval-policy',
+ GLOBAL_CONFIG_SSO_LOGIN_LDAP: 'global-configurations/authorization/sso-login/ldap',
+ GLOBAL_CONFIG_SSO_LOGIN_MICROSOFT: 'global-configurations/authorization/sso-login/microsoft',
+ GLOBAL_CONFIG_PULL_IMAGE_DIGEST: 'global-configurations/pull-image-digest',
+ GLOBAL_CONFIG_TAGS: 'getting-started/global-configurations/tags-policy',
+
+ // Software distribution hub
+ SOFTWARE_DISTRIBUTION_HUB: 'usage/software-distribution-hub',
+ RELEASE_TRACKS: 'usage/software-distribution-hub/release-hub#creating-release-tracks-and-versions',
+ RELEASES: 'usage/software-distribution-hub/release-hub#creating-release-tracks-and-versions',
+ TENANTS: 'usage/software-distribution-hub/tenants#adding-installation',
+ TENANTS_INSTALLATION: 'usage/software-distribution-hub/tenants',
+} as const
diff --git a/src/Shared/Components/DocLink/index.ts b/src/Shared/Components/DocLink/index.ts
new file mode 100644
index 000000000..31d1af3cb
--- /dev/null
+++ b/src/Shared/Components/DocLink/index.ts
@@ -0,0 +1,4 @@
+export { DOCUMENTATION } from './constants'
+export { DocLink } from './DocLink'
+export type { DocLinkProps } from './types'
+export { getDocumentationUrl } from './utils'
diff --git a/src/Shared/Components/DocLink/types.ts b/src/Shared/Components/DocLink/types.ts
new file mode 100644
index 000000000..4dfec7b99
--- /dev/null
+++ b/src/Shared/Components/DocLink/types.ts
@@ -0,0 +1,26 @@
+import { MouseEvent } from 'react'
+
+import { ButtonComponentType, ButtonProps } from '@Shared/Components'
+
+import { DOCUMENTATION } from './constants'
+
+export type BaseDocLink = {
+ isExternalLink?: T
+ isEnterprise?: boolean
+ docLinkKey: T extends true ? string : keyof typeof DOCUMENTATION
+}
+
+export type DocLinkProps = Pick<
+ ButtonProps,
+ 'dataTestId' | 'size' | 'variant' | 'fullWidth' | 'fontWeight' | 'startIcon'
+> &
+ Omit, 'isEnterprise'> & {
+ text?: string
+ showExternalIcon?: boolean
+ onClick?: (e: MouseEvent) => void
+ /**
+ * If `true`, the documentation will open in a new browser tab instead of the side panel.
+ * @default false
+ */
+ openInNewTab?: boolean
+ }
diff --git a/src/Shared/Components/DocLink/utils.tsx b/src/Shared/Components/DocLink/utils.tsx
new file mode 100644
index 000000000..c98bd5b35
--- /dev/null
+++ b/src/Shared/Components/DocLink/utils.tsx
@@ -0,0 +1,22 @@
+import { DOCUMENTATION_HOME_PAGE, DOCUMENTATION_VERSION } from '@Common/Constants'
+
+import { DOCUMENTATION } from './constants'
+import { BaseDocLink } from './types'
+
+export const getDocumentationUrl = ({
+ docLinkKey,
+ isEnterprise = false,
+ isExternalLink,
+}: BaseDocLink) => {
+ if (isExternalLink) {
+ return docLinkKey
+ }
+
+ const docPath = DOCUMENTATION[docLinkKey as keyof typeof DOCUMENTATION]
+
+ if (docPath?.startsWith('http')) {
+ return docPath
+ }
+
+ return `${DOCUMENTATION_HOME_PAGE}${DOCUMENTATION_VERSION}/${docPath || ''}?utm-source=product_${isEnterprise ? 'ent' : 'oss'}`
+}
diff --git a/src/Shared/Components/DynamicDataTable/DynamicDataTable.tsx b/src/Shared/Components/DynamicDataTable/DynamicDataTable.tsx
index 95349fc75..dd6580a52 100644
--- a/src/Shared/Components/DynamicDataTable/DynamicDataTable.tsx
+++ b/src/Shared/Components/DynamicDataTable/DynamicDataTable.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { useMemo, useState } from 'react'
+import { useMemo } from 'react'
import { DynamicDataTableHeader } from './DynamicDataTableHeader'
import { DynamicDataTableRow } from './DynamicDataTableRow'
@@ -24,30 +24,15 @@ import './styles.scss'
export const DynamicDataTable = >({
headers,
- onRowAdd,
...props
}: DynamicDataTableProps) => {
- // STATES
- const [isAddRowButtonClicked, setIsAddRowButtonClicked] = useState(false)
-
// CONSTANTS
const filteredHeaders = useMemo(() => headers.filter(({ isHidden }) => !isHidden), [headers])
- // HANDLERS
- const handleRowAdd = () => {
- setIsAddRowButtonClicked(true)
- onRowAdd()
- }
-
return (
-
-
+
+
)
}
diff --git a/src/Shared/Components/DynamicDataTable/DynamicDataTableRow.tsx b/src/Shared/Components/DynamicDataTable/DynamicDataTableRow.tsx
index 45b141d80..3b1c438e4 100644
--- a/src/Shared/Components/DynamicDataTable/DynamicDataTableRow.tsx
+++ b/src/Shared/Components/DynamicDataTable/DynamicDataTableRow.tsx
@@ -14,21 +14,10 @@
* limitations under the License.
*/
-import {
- createElement,
- createRef,
- Fragment,
- ReactElement,
- RefObject,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from 'react'
+import { createElement, createRef, Fragment, ReactElement, RefObject, useEffect, useMemo, useRef } from 'react'
// eslint-disable-next-line import/no-extraneous-dependencies
import { followCursor } from 'tippy.js'
-import { ReactComponent as ICClose } from '@Icons/ic-close.svg'
import { ResizableTagTextArea } from '@Common/CustomTagSelector'
import { ConditionalWrap } from '@Common/Helper'
import { Tooltip } from '@Common/Tooltip'
@@ -36,6 +25,7 @@ import { ComponentSizeType } from '@Shared/constants'
import { Button, ButtonStyleType, ButtonVariantType } from '../Button'
import { FileUpload } from '../FileUpload'
+import { Icon } from '../Icon'
import {
getSelectPickerOptionByValue,
SelectPicker,
@@ -73,8 +63,7 @@ export const DynamicDataTableRow = ) => {
// CONSTANTS
const isFirstRowEmpty = headers.every(({ key }) => !rows[0]?.data[key].value)
@@ -88,25 +77,18 @@ export const DynamicDataTableRow = >>>()
+ const shouldAutoFocusNewRowRef = useRef(shouldAutoFocusOnMount)
+ const cellRef = useRef>>>(null)
if (!cellRef.current) {
- cellRef.current = rows.reduce(
- (acc, curr) => ({
- ...acc,
- [curr.id]: headers.reduce((headerAcc, { key }) => ({ ...headerAcc, [key]: createRef() }), {}),
- }),
- {},
- )
+ cellRef.current = rows.reduce((acc, curr) => {
+ acc[curr.id] = headers.reduce((headerAcc, { key }) => ({ ...headerAcc, [key]: createRef() }), {})
+ return acc
+ }, {})
}
const rowIds = useMemo(() => rows.map(({ id }) => id), [rows])
useEffect(() => {
- setIsRowAdded(rows.length > 0 && Object.keys(cellRef.current).length < rows.length)
-
// When a new row is added, we create references for its cells.
// This logic ensures that references are created only for the newly added row, while retaining the existing references.
const updatedCellRef = rowIds.reduce((acc, curr) => {
@@ -121,18 +103,6 @@ export const DynamicDataTableRow = {
- if (isAddRowButtonClicked && isRowAdded) {
- // Using the below logic to ensure the cell is focused after row addition.
- const cell = cellRef.current[rows[0].id][focusableFieldKey || headers[0].key].current
- if (cell) {
- cell.focus()
- setIsRowAdded(false)
- setIsAddRowButtonClicked(false)
- }
- }
- }, [isRowAdded, isAddRowButtonClicked])
-
// METHODS
const onChange =
(row: DynamicDataTableRowType, key: K) =>
@@ -163,14 +133,30 @@ export const DynamicDataTableRow = , key: K) => {
+ const renderCellContent = (row: DynamicDataTableRowType, key: K, index: number) => {
const isDisabled = readOnly || row.data[key].disabled
+ const autoFocus =
+ shouldAutoFocusNewRowRef.current && key === (focusableFieldKey ?? headers[0].key) && index === 0
+
+ // This logic ensures only newly added rows get autofocus.
+ // On the initial mount, all rows are treated as new, so autofocus is enabled.
+ // After the first render, when cellRef is set (DOM rendered), we set shouldAutoFocusNewRowRef to true,
+ // so autofocus is applied only to the correct cell in any subsequently added row.
+ if (
+ !shouldAutoFocusOnMount &&
+ !shouldAutoFocusNewRowRef.current &&
+ index === 0 &&
+ cellRef?.current?.[row.id]?.[key].current
+ ) {
+ shouldAutoFocusNewRowRef.current = true
+ }
switch (row.data[key].type) {
case DynamicDataTableRowDataType.DROPDOWN:
return (
+ autoFocus={autoFocus}
{...row.data[key].props}
inputId={`data-table-${row.id}-${key}-cell`}
classNamePrefix="dynamic-data-table__cell__select-picker"
@@ -193,6 +179,7 @@ export const DynamicDataTableRow =
(
-
-
+
)
@@ -329,7 +317,7 @@ export const DynamicDataTableRow =
{renderCellIcon(row, key, true)}
- {renderCellContent(row, key)}
+ {renderCellContent(row, key, index)}
{renderAsterisk(row, key)}
{renderCellIcon(row, key)}
{renderErrorMessages(row, key)}
@@ -383,7 +371,7 @@ export const DynamicDataTableRow = }
+ icon={ }
disabled={disableDeleteRow || row.disableDelete}
onClick={onDelete(row)}
variant={ButtonVariantType.borderLess}
diff --git a/src/Shared/Components/DynamicDataTable/styles.scss b/src/Shared/Components/DynamicDataTable/styles.scss
index bea6f38b1..015d955f1 100644
--- a/src/Shared/Components/DynamicDataTable/styles.scss
+++ b/src/Shared/Components/DynamicDataTable/styles.scss
@@ -69,21 +69,21 @@
height: 36px;
width: 100%;
background: inherit;
-
- &--add {
- resize: none;
- border-radius: 4px;
- outline: none;
- }
}
&__cell {
min-width: 0;
- &__select-picker__control {
- gap: 6px !important;
- padding: 8px !important;
- max-height: 160px !important;
+ &__select-picker {
+ &__control {
+ gap: 6px !important;
+ padding: 8px !important;
+ max-height: 160px !important;
+ }
+
+ &__single-value {
+ font-weight: 400 !important;
+ }
}
&__select-picker-text-area {
diff --git a/src/Shared/Components/DynamicDataTable/types.ts b/src/Shared/Components/DynamicDataTable/types.ts
index 3e9574cb1..a7b8b3981 100644
--- a/src/Shared/Components/DynamicDataTable/types.ts
+++ b/src/Shared/Components/DynamicDataTable/types.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { DetailedHTMLProps, Dispatch, ReactElement, ReactNode, SetStateAction } from 'react'
+import { DetailedHTMLProps, ReactElement, ReactNode } from 'react'
import { ResizableTagTextAreaProps } from '@Common/CustomTagSelector'
import { UseStateFiltersReturnType } from '@Common/Hooks'
@@ -228,6 +228,12 @@ export type DynamicDataTableProps>
@@ -261,7 +267,5 @@ export interface DynamicDataTableRowProps {
- isAddRowButtonClicked: boolean
- setIsAddRowButtonClicked: Dispatch>
-}
+ | 'shouldAutoFocusOnMount'
+ > {}
diff --git a/src/Shared/Components/Error/ErrorBar.tsx b/src/Shared/Components/Error/ErrorBar.tsx
index 1cf0b7bc1..ee5c2781f 100644
--- a/src/Shared/Components/Error/ErrorBar.tsx
+++ b/src/Shared/Components/Error/ErrorBar.tsx
@@ -17,7 +17,7 @@
import { NavLink } from 'react-router-dom'
import { ReactComponent as ErrorInfo } from '../../../Assets/Icon/ic-errorInfo.svg'
-import { URLS } from '../../../Common'
+import { DISCORD_LINK, URLS } from '../../../Common'
import { AppType } from '../../types'
import { ErrorBarType } from './types'
import { getIsImagePullBackOff, renderErrorHeaderMessage } from './utils'
@@ -90,7 +90,7 @@ const ErrorBar = ({ appDetails, useParentMargin = true }: ErrorBarType) => {
Facing issues?
{docLink.length > 0 && (
-
- {BUTTON_TEXT.VIEW_DOCUMENTATION}
-
-
+
)}
ReactNode
- docLink?: string
+ docLink?: DocLinkProps['docLinkKey']
imageVariant?: ImageType
SVGImage?: React.FunctionComponent>
imageStyles?: React.CSSProperties
diff --git a/src/Shared/Components/FramerComponents/MotionDiv.tsx b/src/Shared/Components/FramerComponents/MotionDiv.tsx
deleted file mode 100644
index ad3f7f245..000000000
--- a/src/Shared/Components/FramerComponents/MotionDiv.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-import { HTMLMotionProps, motion } from 'framer-motion'
-
-export const MotionDiv = (props: HTMLMotionProps<'div'>) =>
diff --git a/src/Shared/Components/FramerComponents/index.ts b/src/Shared/Components/FramerComponents/index.ts
index d316ca871..fe454ac71 100644
--- a/src/Shared/Components/FramerComponents/index.ts
+++ b/src/Shared/Components/FramerComponents/index.ts
@@ -1,4 +1,3 @@
-import { AnimatePresence, motion } from 'framer-motion'
+import { animate, AnimatePresence, motion, useMotionTemplate, useMotionValue } from 'framer-motion'
-export * from './MotionDiv'
-export { AnimatePresence, motion }
+export { animate, AnimatePresence, motion, useMotionTemplate, useMotionValue }
diff --git a/src/Shared/Components/GenericInfoCard/GenericInfoCardListing.tsx b/src/Shared/Components/GenericInfoCard/GenericInfoCardListing.tsx
new file mode 100644
index 000000000..c8dd2704e
--- /dev/null
+++ b/src/Shared/Components/GenericInfoCard/GenericInfoCardListing.tsx
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2024. Devtron Inc.
+ */
+
+import { useMemo } from 'react'
+
+import emptyList from '@Images/empty-create.png'
+import ErrorScreenManager from '@Common/ErrorScreenManager'
+import { GenericEmptyState, GenericFilterEmptyState } from '@Common/index'
+
+import GenericInfoCard from './GenericInfoCard.component'
+import { GenericInfoListSkeleton } from './GenericInfoListSkeleton'
+import { GenericInfoCardListingProps } from './types'
+
+export const GenericInfoCardListing = ({
+ isLoading,
+ error,
+ list,
+ searchKey,
+ reloadList,
+ borderVariant,
+ handleClearFilters,
+ emptyStateConfig,
+}: GenericInfoCardListingProps) => {
+ const filteredList = useMemo(() => {
+ const sanitizedList = list || []
+ if (!searchKey || error) {
+ return sanitizedList
+ }
+
+ const loweredSearchKey = searchKey.toLowerCase()
+ return sanitizedList.filter(({ title }) => title.toLowerCase().includes(loweredSearchKey))
+ }, [searchKey, list, error])
+
+ if (isLoading) {
+ return
+ }
+
+ if (error) {
+ return
+ }
+
+ if (filteredList.length === 0) {
+ if (searchKey) {
+ return
+ }
+
+ return (
+
+ )
+ }
+
+ return (
+ <>
+ {filteredList.map(({ id, title, description, author, Icon, onClick, linkProps }) => (
+
+ ))}
+ >
+ )
+}
diff --git a/src/Shared/Components/GenericInfoCard/GenericInfoListSkeleton.tsx b/src/Shared/Components/GenericInfoCard/GenericInfoListSkeleton.tsx
new file mode 100644
index 000000000..f78f83668
--- /dev/null
+++ b/src/Shared/Components/GenericInfoCard/GenericInfoListSkeleton.tsx
@@ -0,0 +1,10 @@
+import GenericInfoCard from './GenericInfoCard.component'
+import { GenericInfoListSkeletonProps } from './types'
+
+export const GenericInfoListSkeleton = ({ borderVariant }: GenericInfoListSkeletonProps) => (
+ <>
+
+
+
+ >
+)
diff --git a/src/Shared/Components/GenericInfoCard/index.ts b/src/Shared/Components/GenericInfoCard/index.ts
index 763c46d7d..bca53f3fd 100644
--- a/src/Shared/Components/GenericInfoCard/index.ts
+++ b/src/Shared/Components/GenericInfoCard/index.ts
@@ -15,4 +15,6 @@
*/
export { default as GenericInfoCard } from './GenericInfoCard.component'
-export { GenericInfoCardBorderVariant, type GenericInfoCardProps } from './types'
+export * from './GenericInfoCardListing'
+export { GenericInfoListSkeleton } from './GenericInfoListSkeleton'
+export { GenericInfoCardBorderVariant, type GenericInfoCardListingProps, type GenericInfoCardProps } from './types'
diff --git a/src/Shared/Components/GenericInfoCard/types.ts b/src/Shared/Components/GenericInfoCard/types.ts
index 3050292f6..6c90f881f 100644
--- a/src/Shared/Components/GenericInfoCard/types.ts
+++ b/src/Shared/Components/GenericInfoCard/types.ts
@@ -17,6 +17,11 @@
import { MouseEventHandler, ReactElement } from 'react'
import { LinkProps } from 'react-router-dom'
+import { GenericFilterEmptyStateProps } from '@Common/EmptyState/types'
+import { GenericEmptyStateType } from '@Common/Types'
+
+import { APIResponseHandlerProps } from '../APIResponseHandler'
+
type BaseGenericInfoCardProps = {
title: string
description: string
@@ -46,3 +51,17 @@ export type GenericInfoCardProps = { borderVariant: GenericInfoCardBorderVariant
isLoading?: boolean
} & BaseGenericInfoCardProps)
)
+
+export interface GenericInfoCardListingProps
+ extends Pick,
+ Pick {
+ list: (Pick &
+ Record<'id', string>)[]
+ emptyStateConfig: Pick
+ searchKey?: string
+ reloadList: () => void
+ error?: APIResponseHandlerProps['error']
+ isLoading?: boolean
+}
+
+export interface GenericInfoListSkeletonProps extends Partial> {}
diff --git a/src/Shared/Components/Header/HelpButton.tsx b/src/Shared/Components/Header/HelpButton.tsx
index 6851c9e37..2815642b6 100644
--- a/src/Shared/Components/Header/HelpButton.tsx
+++ b/src/Shared/Components/Header/HelpButton.tsx
@@ -2,21 +2,22 @@ import { useRef, useState } from 'react'
import ReactGA from 'react-ga4'
import { SliderButton } from '@typeform/embed-react'
-import { URLS } from '@Common/Constants'
+import { DOCUMENTATION_HOME_PAGE, URLS } from '@Common/Constants'
import { ComponentSizeType } from '@Shared/constants'
import { useMainContext } from '@Shared/Providers'
+import { InstallationType } from '@Shared/types'
-import { ActionMenu, ActionMenuItemType } from '../ActionMenu'
+import { ActionMenu } from '../ActionMenu'
import { Button, ButtonComponentType, ButtonVariantType } from '../Button'
import { Icon } from '../Icon'
-import { HelpButtonActionMenuProps, HelpButtonProps, HelpMenuItems, InstallationType } from './types'
+import { HelpButtonActionMenuProps, HelpButtonProps, HelpMenuItems } from './types'
import { getHelpActionMenuOptions } from './utils'
const CheckForUpdates = ({
serverInfo,
fetchingServerInfo,
}: Pick) => (
-
+
{fetchingServerInfo ? (
Checking version
) : (
@@ -40,20 +41,20 @@ export const HelpButton = ({ serverInfo, fetchingServerInfo, onClick }: HelpButt
const [isActionMenuOpen, setIsActionMenuOpen] = useState(false)
// HOOKS
- const { currentServerInfo, handleOpenLicenseInfoDialog, licenseData, setGettingStartedClicked } = useMainContext()
+ const { handleOpenLicenseInfoDialog, licenseData, setGettingStartedClicked, isEnterprise, setSidePanelConfig } =
+ useMainContext()
// REFS
const typeFormSliderButtonRef = useRef(null)
// CONSTANTS
const FEEDBACK_FORM_ID = `UheGN3KJ#source=${window.location.hostname}`
- const isEnterprise = currentServerInfo?.serverInfo?.installationType === InstallationType.ENTERPRISE
// HANDLERS
- const handleAnalytics = (option: ActionMenuItemType) => {
+ const handleAnalytics: HelpButtonActionMenuProps['onClick'] = (item) => {
ReactGA.event({
category: 'Help Nav',
- action: `${option.label} Clicked`,
+ action: `${item.label} Clicked`,
})
}
@@ -73,7 +74,21 @@ export const HelpButton = ({ serverInfo, fetchingServerInfo, onClick }: HelpButt
setGettingStartedClicked(true)
}
- const handleActionMenuClick: HelpButtonActionMenuProps['onClick'] = (item) => {
+ const handleViewDocumentationClick: HelpButtonActionMenuProps['onClick'] = (item, e) => {
+ handleAnalytics(item, e)
+ // Opens documentation in side panel when clicked normally, or in a new tab when clicked with the meta/command key
+ if (!e.metaKey) {
+ e.preventDefault()
+ setSidePanelConfig((prev) => ({
+ ...prev,
+ open: true,
+ docLink: DOCUMENTATION_HOME_PAGE,
+ reinitialize: true,
+ }))
+ }
+ }
+
+ const handleActionMenuClick: HelpButtonActionMenuProps['onClick'] = (item, e) => {
switch (item.id) {
case HelpMenuItems.GETTING_STARTED:
handleGettingStartedClick()
@@ -85,12 +100,14 @@ export const HelpButton = ({ serverInfo, fetchingServerInfo, onClick }: HelpButt
handleFeedbackClick()
break
case HelpMenuItems.JOIN_DISCORD_COMMUNITY:
- case HelpMenuItems.VIEW_DOCUMENTATION:
case HelpMenuItems.OPEN_NEW_TICKET:
case HelpMenuItems.VIEW_ALL_TICKETS:
case HelpMenuItems.CHAT_WITH_SUPPORT:
case HelpMenuItems.RAISE_ISSUE_REQUEST:
- handleAnalytics(item)
+ handleAnalytics(item, e)
+ break
+ case HelpMenuItems.VIEW_DOCUMENTATION:
+ handleViewDocumentationClick(item, e)
break
default:
}
@@ -110,7 +127,7 @@ export const HelpButton = ({ serverInfo, fetchingServerInfo, onClick }: HelpButt
onOpen={setIsActionMenuOpen}
{...(serverInfo?.installationType === InstallationType.OSS_HELM
? {
- menuListFooterConfig: {
+ footerConfig: {
type: 'customNode',
value: (
diff --git a/src/Shared/Components/Header/PageHeader.tsx b/src/Shared/Components/Header/PageHeader.tsx
index e30dfdedb..28ef087ce 100644
--- a/src/Shared/Components/Header/PageHeader.tsx
+++ b/src/Shared/Components/Header/PageHeader.tsx
@@ -18,21 +18,21 @@ import { useEffect, useState } from 'react'
import ReactGA from 'react-ga4'
import Tippy from '@tippyjs/react'
-import { ReactComponent as ICCaretDownSmall } from '@Icons/ic-caret-down-small.svg'
import { ReactComponent as Close } from '@Icons/ic-close.svg'
import { ReactComponent as ICMediumPaintBucket } from '@IconsV2/ic-medium-paintbucket.svg'
+import { InstallationType } from '@Shared/types'
-import { getAlphabetIcon, TippyCustomized, TippyTheme } from '../../../Common'
+import { TippyCustomized, TippyTheme } from '../../../Common'
import { MAX_LOGIN_COUNT, POSTHOG_EVENT_ONBOARDING } from '../../../Common/Constants'
import { useMainContext, useTheme, useUserEmail } from '../../Providers'
import GettingStartedCard from '../GettingStartedCard/GettingStarted'
import { InfoIconTippy } from '../InfoIconTippy'
-import LogoutCard from '../LogoutCard'
import { HelpButton } from './HelpButton'
import { IframePromoButton } from './IframePromoButton'
+import { ProfileMenu } from './ProfileMenu'
import { getServerInfo } from './service'
-import { InstallationType, PageHeaderType, ServerInfo } from './types'
-import { getIsShowingLicenseData, handlePostHogEventUpdate, setActionWithExpiry } from './utils'
+import { PageHeaderType, ServerInfo } from './types'
+import { handlePostHogEventUpdate, setActionWithExpiry } from './utils'
import './pageHeader.scss'
@@ -49,13 +49,11 @@ const PageHeader = ({
markAsBeta,
tippyProps,
}: PageHeaderType) => {
- const { loginCount, setLoginCount, showGettingStartedCard, setShowGettingStartedCard, licenseData } =
- useMainContext()
+ const { loginCount, setLoginCount, showGettingStartedCard, setShowGettingStartedCard } = useMainContext()
const { showSwitchThemeLocationTippy, handleShowSwitchThemeLocationTippyChange } = useTheme()
const { isTippyCustomized, tippyRedirectLink, TippyIcon, tippyMessage, onClickTippyButton, additionalContent } =
tippyProps || {}
- const [showLogOutCard, setShowLogOutCard] = useState(false)
const { email } = useUserEmail()
const [currentServerInfo, setCurrentServerInfo] = useState<{ serverInfo: ServerInfo; fetchingServerInfo: boolean }>(
{
@@ -97,25 +95,19 @@ const PageHeader = ({
handleShowSwitchThemeLocationTippyChange(false)
}
- const onClickLogoutButton = () => {
+ const handleProfileMenuButtonClick = () => {
handleCloseSwitchThemeLocationTippyChange()
- setShowLogOutCard(!showLogOutCard)
setActionWithExpiry('clickedOkay', 1)
hideGettingStartedCard()
}
- const onHelpButtonClick = async () => {
+ const handleHelpButtonClick = async () => {
if (
!window._env_.K8S_CLIENT &&
currentServerInfo.serverInfo?.installationType !== InstallationType.ENTERPRISE
) {
await getCurrentServerInfo()
}
-
- if (showLogOutCard) {
- setShowLogOutCard(false)
- }
-
setActionWithExpiry('clickedOkay', 1)
hideGettingStartedCard()
await handlePostHogEventUpdate(POSTHOG_EVENT_ONBOARDING.HELP)
@@ -137,7 +129,7 @@ const PageHeader = ({
{!window._env_.K8S_CLIENT && (
-
- {getAlphabetIcon(email, 'm-0-imp h-24 w-24-imp')}
-
-
+
)}
>
@@ -181,8 +166,6 @@ const PageHeader = ({
Beta
)
- const showingLicenseBar = getIsShowingLicenseData(licenseData)
-
const renderIframeButton = () =>
return (
@@ -215,7 +198,7 @@ const PageHeader = ({
heading={headerName}
iconClassName="icon-dim-20 ml-8 fcn-5"
documentationLink={tippyRedirectLink}
- documentationLinkText="Learn More"
+ documentationLinkText="View Documentation"
additionalContent={additionalContent}
>
{TippyIcon && (
@@ -249,7 +232,7 @@ const PageHeader = ({
{markAsBeta && renderBetaTag()}
{showTabs && (
-
+
{renderIframeButton()}
{typeof renderActionButtons === 'function' && renderActionButtons()}
{renderLogoutHelpSection()}
@@ -269,16 +252,8 @@ const PageHeader = ({
loginCount={loginCount}
/>
)}
- {showLogOutCard && (
-
- )}
{!showTabs && (
-
+
{typeof renderActionButtons === 'function' && renderActionButtons()}
{renderIframeButton()}
{renderLogoutHelpSection()}
diff --git a/src/Shared/Components/Header/ProfileMenu.tsx b/src/Shared/Components/Header/ProfileMenu.tsx
new file mode 100644
index 000000000..1b84c759e
--- /dev/null
+++ b/src/Shared/Components/Header/ProfileMenu.tsx
@@ -0,0 +1,78 @@
+import { useMemo } from 'react'
+import { Link } from 'react-router-dom'
+
+import { URLS } from '@Common/Constants'
+import { getAlphabetIcon } from '@Common/Helper'
+import { clearCookieOnLogout } from '@Shared/Helpers'
+import { useMainContext } from '@Shared/Providers'
+
+import { Icon } from '../Icon'
+import { Popover, usePopover } from '../Popover'
+import { ThemeSwitcher } from '../ThemeSwitcher'
+import { ProfileMenuProps } from './types'
+
+export const ProfileMenu = ({ user, onClick }: ProfileMenuProps) => {
+ // HOOKS
+ const { viewIsPipelineRBACConfiguredNode } = useMainContext()
+
+ const { open, overlayProps, popoverProps, triggerProps, scrollableRef, closePopover } = usePopover({
+ id: 'profile-menu',
+ alignment: 'end',
+ width: 250,
+ })
+
+ // ELEMENTS
+ const triggerElement = useMemo(
+ () => (
+
+ {getAlphabetIcon(user, 'dc__no-shrink m-0-imp icon-dim-24')}
+
+
+ ),
+ [open],
+ )
+
+ // HANDLERS
+ const onLogout = () => {
+ closePopover()
+ clearCookieOnLogout()
+ }
+
+ return (
+
+
+
+
+
+ {getAlphabetIcon(user, 'dc__no-shrink m-0-imp fs-16 icon-dim-36')}
+
+
+
+
+ {viewIsPipelineRBACConfiguredNode}
+
+
+
+ Logout
+
+
+
+
+
+ )
+}
diff --git a/src/Shared/Components/Header/types.ts b/src/Shared/Components/Header/types.ts
index 0dc15f387..925965001 100644
--- a/src/Shared/Components/Header/types.ts
+++ b/src/Shared/Components/Header/types.ts
@@ -14,16 +14,11 @@
* limitations under the License.
*/
-import { ModuleStatus } from '@Shared/types'
+import { InstallationType, ModuleStatus } from '@Shared/types'
import { ResponseType, TippyCustomizedProps } from '../../../Common'
import { ActionMenuProps } from '../ActionMenu'
-
-export enum InstallationType {
- OSS_KUBECTL = 'oss_kubectl',
- OSS_HELM = 'oss_helm',
- ENTERPRISE = 'enterprise',
-}
+import { DOCUMENTATION } from '../DocLink'
export interface PageHeaderType {
headerName?: string
@@ -36,9 +31,9 @@ export interface PageHeaderType {
showCloseButton?: boolean
onClose?: () => void
markAsBeta?: boolean
- tippyProps?: Pick
& {
+ tippyProps?: Pick, 'additionalContent'> & {
isTippyCustomized?: boolean
- tippyRedirectLink?: string
+ tippyRedirectLink?: keyof typeof DOCUMENTATION
TippyIcon?: React.FunctionComponent
tippyMessage?: string
onClickTippyButton?: () => void
@@ -76,3 +71,8 @@ export enum HelpMenuItems {
}
export type HelpButtonActionMenuProps = ActionMenuProps
+
+export interface ProfileMenuProps {
+ user: string
+ onClick?: () => void
+}
diff --git a/src/Shared/Components/Header/utils.ts b/src/Shared/Components/Header/utils.ts
index 769041b14..1896ea901 100644
--- a/src/Shared/Components/Header/utils.ts
+++ b/src/Shared/Components/Header/utils.ts
@@ -16,7 +16,6 @@
import { LOGIN_COUNT } from '@Common/Constants'
-import { DevtronLicenseInfo, LicenseStatus } from '../License'
import {
COMMON_HELP_ACTION_MENU_ITEMS,
ENTERPRISE_HELP_ACTION_MENU_ITEMS,
@@ -43,9 +42,6 @@ export const setActionWithExpiry = (key: string, days: number): void => {
localStorage.setItem(key, `${getDateInMilliseconds(days)}`)
}
-export const getIsShowingLicenseData = (licenseData: DevtronLicenseInfo) =>
- licenseData && (licenseData.licenseStatus !== LicenseStatus.ACTIVE || licenseData.isTrial)
-
export const getHelpActionMenuOptions = ({
isEnterprise,
isTrial,
diff --git a/src/Shared/Components/Icon/Icon.tsx b/src/Shared/Components/Icon/Icon.tsx
index c1b87b4d1..302c24049 100644
--- a/src/Shared/Components/Icon/Icon.tsx
+++ b/src/Shared/Components/Icon/Icon.tsx
@@ -6,6 +6,7 @@ import { ReactComponent as ICAdd } from '@IconsV2/ic-add.svg'
import { ReactComponent as ICAmazonEks } from '@IconsV2/ic-amazon-eks.svg'
import { ReactComponent as ICApica } from '@IconsV2/ic-apica.svg'
import { ReactComponent as ICAppGroup } from '@IconsV2/ic-app-group.svg'
+import { ReactComponent as ICAppTemplate } from '@IconsV2/ic-app-template.svg'
import { ReactComponent as ICArrowClockwise } from '@IconsV2/ic-arrow-clockwise.svg'
import { ReactComponent as ICArrowRight } from '@IconsV2/ic-arrow-right.svg'
import { ReactComponent as ICArrowSquareOut } from '@IconsV2/ic-arrow-square-out.svg'
@@ -51,12 +52,15 @@ import { ReactComponent as ICDeleteDots } from '@IconsV2/ic-delete-dots.svg'
import { ReactComponent as ICDeleteLightning } from '@IconsV2/ic-delete-lightning.svg'
import { ReactComponent as ICDelhivery } from '@IconsV2/ic-delhivery.svg'
import { ReactComponent as ICDevtron } from '@IconsV2/ic-devtron.svg'
+import { ReactComponent as ICDevtronApp } from '@IconsV2/ic-devtron-app.svg'
import { ReactComponent as ICDevtronHeaderLogo } from '@IconsV2/ic-devtron-header-logo.svg'
+import { ReactComponent as ICDevtronJob } from '@IconsV2/ic-devtron-job.svg'
import { ReactComponent as ICDisconnect } from '@IconsV2/ic-disconnect.svg'
import { ReactComponent as ICDiscordFill } from '@IconsV2/ic-discord-fill.svg'
import { ReactComponent as ICDockerhub } from '@IconsV2/ic-dockerhub.svg'
import { ReactComponent as ICEcr } from '@IconsV2/ic-ecr.svg'
import { ReactComponent as ICEdit } from '@IconsV2/ic-edit.svg'
+import { ReactComponent as ICEmail } from '@IconsV2/ic-email.svg'
import { ReactComponent as ICEnterpriseFeat } from '@IconsV2/ic-enterprise-feat.svg'
import { ReactComponent as ICEnterpriseTag } from '@IconsV2/ic-enterprise-tag.svg'
import { ReactComponent as ICEnv } from '@IconsV2/ic-env.svg'
@@ -101,6 +105,7 @@ import { ReactComponent as ICJobColor } from '@IconsV2/ic-job-color.svg'
import { ReactComponent as ICK3s } from '@IconsV2/ic-k3s.svg'
import { ReactComponent as ICK8sJob } from '@IconsV2/ic-k8s-job.svg'
import { ReactComponent as ICKey } from '@IconsV2/ic-key.svg'
+import { ReactComponent as ICKeyEnter } from '@IconsV2/ic-key-enter.svg'
import { ReactComponent as ICKind } from '@IconsV2/ic-kind.svg'
import { ReactComponent as ICLaptop } from '@IconsV2/ic-laptop.svg'
import { ReactComponent as ICLdap } from '@IconsV2/ic-ldap.svg'
@@ -144,6 +149,7 @@ import { ReactComponent as ICSortDescending } from '@IconsV2/ic-sort-descending.
import { ReactComponent as ICSortable } from '@IconsV2/ic-sortable.svg'
import { ReactComponent as ICSparkleColor } from '@IconsV2/ic-sparkle-color.svg'
import { ReactComponent as ICSpinny } from '@IconsV2/ic-spinny.svg'
+import { ReactComponent as ICSprayCan } from '@IconsV2/ic-spray-can.svg'
import { ReactComponent as ICStack } from '@IconsV2/ic-stack.svg'
import { ReactComponent as ICStamp } from '@IconsV2/ic-stamp.svg'
import { ReactComponent as ICStrategyBlueGreen } from '@IconsV2/ic-strategy-blue-green.svg'
@@ -186,6 +192,7 @@ export const iconMap = {
'ic-amazon-eks': ICAmazonEks,
'ic-apica': ICApica,
'ic-app-group': ICAppGroup,
+ 'ic-app-template': ICAppTemplate,
'ic-arrow-clockwise': ICArrowClockwise,
'ic-arrow-right': ICArrowRight,
'ic-arrow-square-out': ICArrowSquareOut,
@@ -230,13 +237,16 @@ export const iconMap = {
'ic-delete-lightning': ICDeleteLightning,
'ic-delete': ICDelete,
'ic-delhivery': ICDelhivery,
+ 'ic-devtron-app': ICDevtronApp,
'ic-devtron-header-logo': ICDevtronHeaderLogo,
+ 'ic-devtron-job': ICDevtronJob,
'ic-devtron': ICDevtron,
'ic-disconnect': ICDisconnect,
'ic-discord-fill': ICDiscordFill,
'ic-dockerhub': ICDockerhub,
'ic-ecr': ICEcr,
'ic-edit': ICEdit,
+ 'ic-email': ICEmail,
'ic-enterprise-feat': ICEnterpriseFeat,
'ic-enterprise-tag': ICEnterpriseTag,
'ic-env': ICEnv,
@@ -280,6 +290,7 @@ export const iconMap = {
'ic-job-color': ICJobColor,
'ic-k3s': ICK3s,
'ic-k8s-job': ICK8sJob,
+ 'ic-key-enter': ICKeyEnter,
'ic-key': ICKey,
'ic-kind': ICKind,
'ic-laptop': ICLaptop,
@@ -324,6 +335,7 @@ export const iconMap = {
'ic-sortable': ICSortable,
'ic-sparkle-color': ICSparkleColor,
'ic-spinny': ICSpinny,
+ 'ic-spray-can': ICSprayCan,
'ic-stack': ICStack,
'ic-stamp': ICStamp,
'ic-strategy-blue-green-color': ICStrategyBlueGreenColor,
diff --git a/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx b/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx
index eb79c7339..563df99a4 100644
--- a/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx
+++ b/src/Shared/Components/InfoIconTippy/InfoIconTippy.tsx
@@ -19,7 +19,7 @@ import { ReactComponent as ICHelpOutline } from '../../../Assets/Icon/ic-help-ou
import { TippyCustomized } from '../../../Common/TippyCustomized'
import { InfoIconTippyProps, TippyTheme } from '../../../Common/Types'
-const InfoIconTippy = ({
+const InfoIconTippy = ({
heading,
infoText,
iconClass = 'fcv-5',
@@ -32,7 +32,9 @@ const InfoIconTippy = ({
children,
headingInfo,
buttonPadding = 'p-0',
-}: InfoIconTippyProps) => (
+ isExternalLink,
+ openInNewTab,
+}: InfoIconTippyProps) => (
{children || (
{
*/
export type KeyValueTableProps = Pick<
DynamicDataTableProps,
- 'isAdditionNotAllowed' | 'readOnly' | 'headerComponent'
+ 'isAdditionNotAllowed' | 'readOnly' | 'headerComponent' | 'shouldAutoFocusOnMount'
> & {
/**
* The label for the table header.
diff --git a/src/Shared/Components/License/DevtronLicenseCard.tsx b/src/Shared/Components/License/DevtronLicenseCard.tsx
index 25395dcc6..1b068b4f9 100644
--- a/src/Shared/Components/License/DevtronLicenseCard.tsx
+++ b/src/Shared/Components/License/DevtronLicenseCard.tsx
@@ -5,11 +5,10 @@ import { ReactComponent as ICChatSupport } from '@IconsV2/ic-chat-circle-dots.sv
import { ReactComponent as TexturedBG } from '@Images/licenseCardBG.svg'
import { ClipboardButton, getTTLInHumanReadableFormat } from '@Common/index'
import { CONTACT_SUPPORT_LINK, ENTERPRISE_SUPPORT_LINK } from '@Shared/constants'
-import { getHandleOpenURL } from '@Shared/Helpers'
import { AppThemeType } from '@Shared/Providers'
import { getThemeOppositeThemeClass } from '@Shared/Providers/ThemeProvider/utils'
-import { Button, ButtonVariantType } from '../Button'
+import { Button, ButtonComponentType, ButtonVariantType } from '../Button'
import { Icon } from '../Icon'
import { DevtronLicenseCardProps, LicenseStatus } from './types'
import { getLicenseColorsAccordingToStatus } from './utils'
@@ -147,7 +146,8 @@ export const DevtronLicenseCard = ({
startIcon={ }
text="Contact support"
variant={ButtonVariantType.text}
- onClick={getHandleOpenURL(CONTACT_SUPPORT_LINK)}
+ component={ButtonComponentType.anchor}
+ anchorProps={{ href: CONTACT_SUPPORT_LINK }}
/>
)}
diff --git a/src/Shared/Components/License/License.components.tsx b/src/Shared/Components/License/License.components.tsx
index df879988a..585e43cdf 100644
--- a/src/Shared/Components/License/License.components.tsx
+++ b/src/Shared/Components/License/License.components.tsx
@@ -2,7 +2,6 @@ import { useEffect, useRef, useState } from 'react'
import { ReactComponent as ICCheck } from '@Icons/ic-check.svg'
import { ReactComponent as ICClipboard } from '@Icons/ic-copy.svg'
-import { DOCUMENTATION } from '@Common/Constants'
import { ClipboardButton, copyToClipboard, showError } from '@Common/index'
import { Backdrop, Button, ButtonStyleType, ButtonVariantType, Icon, InfoIconTippy, QRCode } from '..'
@@ -90,7 +89,8 @@ const InstallationFingerprintInfo = ({ fingerprint, showHelpTooltip = false }: I
documentationLinkText="Documentation"
iconClassName="icon-dim-20 fcn-6"
placement="right"
- documentationLink={DOCUMENTATION.ENTERPRISE_LICENSE}
+ documentationLink="ENTERPRISE_LICENSE"
+ openInNewTab
/>
)}
diff --git a/src/Shared/Components/LogoutCard.tsx b/src/Shared/Components/LogoutCard.tsx
deleted file mode 100644
index 6f2e4cece..000000000
--- a/src/Shared/Components/LogoutCard.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (c) 2024. Devtron Inc.
- *
- * 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 React from 'react'
-import { useHistory } from 'react-router-dom'
-
-import { clearCookieOnLogout } from '@Shared/Helpers'
-import { useMainContext } from '@Shared/Providers'
-
-import { getRandomColor, stopPropagation } from '../../Common'
-import { Icon } from './Icon'
-import { ThemeSwitcher } from './ThemeSwitcher'
-
-interface LogoutCardType {
- className: string
- userFirstLetter: string
- setShowLogOutCard: React.Dispatch
>
- showLogOutCard: boolean
-}
-
-export const LOGOUT_CARD_BASE_BUTTON_CLASS =
- 'dc__unset-button-styles dc__content-space px-12 py-8 fs-13 fw-4 lh-20 cursor w-100 flex left br-4'
-
-const LogoutCard = ({ className, userFirstLetter, setShowLogOutCard, showLogOutCard }: LogoutCardType) => {
- const history = useHistory()
- const { viewIsPipelineRBACConfiguredNode } = useMainContext()
-
- const onLogout = () => {
- clearCookieOnLogout()
- history.push('/login')
- }
-
- const toggleLogoutCard = () => {
- setShowLogOutCard(!showLogOutCard)
- }
-
- return (
-
-
-
-
-
{userFirstLetter}
-
{userFirstLetter}
-
-
- {userFirstLetter[0]}
-
-
-
-
-
- {viewIsPipelineRBACConfiguredNode}
-
-
- Logout
-
-
-
-
-
- )
-}
-
-export default LogoutCard
diff --git a/src/Shared/Components/ModalSidebarPanel/ModalSidebarPanel.component.tsx b/src/Shared/Components/ModalSidebarPanel/ModalSidebarPanel.component.tsx
index 13d863b8e..eb8315a25 100644
--- a/src/Shared/Components/ModalSidebarPanel/ModalSidebarPanel.component.tsx
+++ b/src/Shared/Components/ModalSidebarPanel/ModalSidebarPanel.component.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { ReactComponent as ArrowOut } from '../../../Assets/Icon/ic-arrow-square-out.svg'
+import { DocLink } from '../DocLink'
import { ModalSidebarPanelProps } from './types'
const ModalSidebarPanel = ({
@@ -34,17 +34,15 @@ const ModalSidebarPanel = ({
)}
{children && {children}
}
-
)
diff --git a/src/Shared/Components/ModalSidebarPanel/types.ts b/src/Shared/Components/ModalSidebarPanel/types.ts
index 26787ea38..dbee95e50 100644
--- a/src/Shared/Components/ModalSidebarPanel/types.ts
+++ b/src/Shared/Components/ModalSidebarPanel/types.ts
@@ -16,10 +16,12 @@
import { ReactNode } from 'react'
+import { DocLinkProps } from '../DocLink'
+
export interface ModalSidebarPanelProps {
rootClassName?: string
heading: string | null
icon?: JSX.Element
children?: ReactNode
- documentationLink: string
+ documentationLink: DocLinkProps['docLinkKey']
}
diff --git a/src/Shared/Components/NumbersCount/index.ts b/src/Shared/Components/NumbersCount/index.ts
index b3b6ec1db..cdf8ca7ba 100644
--- a/src/Shared/Components/NumbersCount/index.ts
+++ b/src/Shared/Components/NumbersCount/index.ts
@@ -15,3 +15,4 @@
*/
export { default as NumbersCount } from './NumbersCount.component'
+export * from './types'
diff --git a/src/Shared/Components/Popover/Popover.component.tsx b/src/Shared/Components/Popover/Popover.component.tsx
index 68640c840..d332af886 100644
--- a/src/Shared/Components/Popover/Popover.component.tsx
+++ b/src/Shared/Components/Popover/Popover.component.tsx
@@ -15,24 +15,25 @@ export const Popover = ({
open,
popoverProps,
overlayProps,
- triggerProps,
+ triggerProps: { bounds, ...triggerProps },
buttonProps,
triggerElement,
children,
}: PopoverProps) => (
-
+ <>
{triggerElement || }
{open && (
- <>
- {/* Overlay to block interactions with the background */}
-
-
- {children}
-
- >
+
)}
-
+ >
)
diff --git a/src/Shared/Components/Popover/popover.scss b/src/Shared/Components/Popover/popover.scss
index f241ad180..26caeb6e0 100644
--- a/src/Shared/Components/Popover/popover.scss
+++ b/src/Shared/Components/Popover/popover.scss
@@ -6,7 +6,3 @@
left:0;
z-index: var(--modal-index);
}
-
-.popover-content {
- z-index: var(--modal-index);
-}
diff --git a/src/Shared/Components/Popover/types.ts b/src/Shared/Components/Popover/types.ts
index 5b1fdaac8..40ea7c343 100644
--- a/src/Shared/Components/Popover/types.ts
+++ b/src/Shared/Components/Popover/types.ts
@@ -1,4 +1,4 @@
-import { DetailedHTMLProps, KeyboardEvent, MutableRefObject, ReactElement } from 'react'
+import { DetailedHTMLProps, KeyboardEvent, LegacyRef, MutableRefObject, ReactElement } from 'react'
import { HTMLMotionProps } from 'framer-motion'
import { ButtonProps } from '../Button'
@@ -67,7 +67,9 @@ export interface UsePopoverReturnType {
* Props to be spread onto the trigger element that opens the popover.
* These props include standard HTML attributes for a `div` element.
*/
- triggerProps: DetailedHTMLProps
, HTMLDivElement>
+ triggerProps: DetailedHTMLProps, HTMLDivElement> & {
+ bounds: Pick
+ }
/**
* Props to be spread onto the overlay element of the popover.
* These props include standard HTML attributes for a `div` element.
@@ -82,7 +84,7 @@ export interface UsePopoverReturnType {
* A mutable reference to the scrollable element inside the popover. \
* This reference should be assigned to the element that is scrollable.
*/
- scrollableRef: MutableRefObject
+ scrollableRef: MutableRefObject | LegacyRef
/**
* A function to close the popover.
*/
diff --git a/src/Shared/Components/Popover/usePopover.hook.ts b/src/Shared/Components/Popover/usePopover.hook.ts
index d95ff93b2..26a0b7a0e 100644
--- a/src/Shared/Components/Popover/usePopover.hook.ts
+++ b/src/Shared/Components/Popover/usePopover.hook.ts
@@ -1,4 +1,4 @@
-import { useLayoutEffect, useRef, useState } from 'react'
+import { MouseEvent, useLayoutEffect, useRef, useState } from 'react'
import { UsePopoverProps, UsePopoverReturnType } from './types'
import {
@@ -21,6 +21,7 @@ export const usePopover = ({
const [open, setOpen] = useState(false)
const [actualPosition, setActualPosition] = useState(position)
const [actualAlignment, setActualAlignment] = useState(alignment)
+ const [triggerBounds, setTriggerBounds] = useState(null)
// CONSTANTS
const isAutoWidth = width === 'auto'
@@ -49,25 +50,42 @@ export const usePopover = ({
onTriggerKeyDown?.(e, open, closePopover)
}
- const handlePopoverKeyDown = (e: React.KeyboardEvent) => onPopoverKeyDown(e, open, closePopover)
+ const handlePopoverKeyDown = (e: React.KeyboardEvent) => onPopoverKeyDown?.(e, open, closePopover)
+
+ const handleOverlayClick = (e: MouseEvent) => {
+ if (!popover.current?.contains(e.target as Node)) {
+ closePopover()
+ }
+ }
useLayoutEffect(() => {
if (!open || !triggerRef.current || !popover.current || !scrollableRef.current) {
return
}
- const triggerRect = triggerRef.current.getBoundingClientRect()
- const popoverRect = popover.current.getBoundingClientRect()
-
- const { fallbackPosition, fallbackAlignment } = getPopoverActualPositionAlignment({
- position,
- alignment,
- triggerRect,
- popoverRect,
- })
+ const updatePopoverPosition = () => {
+ const triggerRect = triggerRef.current.getBoundingClientRect()
+ const popoverRect = popover.current.getBoundingClientRect()
+
+ const { fallbackPosition, fallbackAlignment } = getPopoverActualPositionAlignment({
+ position,
+ alignment,
+ triggerRect,
+ popoverRect,
+ })
+
+ setActualPosition(fallbackPosition)
+ setActualAlignment(fallbackAlignment)
+ setTriggerBounds({
+ left: triggerRect.left,
+ top: triggerRect.top,
+ height: triggerRect.height,
+ width: triggerRect.width,
+ })
+ }
- setActualPosition(fallbackPosition)
- setActualAlignment(fallbackAlignment)
+ // update position on open
+ updatePopoverPosition()
// prevent scroll propagation unless scrollable
const handleWheel = (e: WheelEvent) => {
@@ -84,9 +102,12 @@ export const usePopover = ({
}
scrollableRef.current.addEventListener('wheel', handleWheel, { passive: false })
+ window.addEventListener('resize', updatePopoverPosition)
+
// eslint-disable-next-line consistent-return
return () => {
scrollableRef.current.removeEventListener('wheel', handleWheel)
+ window.removeEventListener('resize', updatePopoverPosition)
}
}, [open, position, alignment])
@@ -100,17 +121,18 @@ export const usePopover = ({
'aria-haspopup': 'listbox',
'aria-expanded': open,
tabIndex: 0,
+ bounds: triggerBounds ?? { left: 0, top: 0, height: 0, width: 0 },
},
overlayProps: {
role: 'dialog',
- onClick: closePopover,
+ onClick: handleOverlayClick,
className: 'popover-overlay',
},
popoverProps: {
id,
ref: popover,
role: 'listbox',
- className: `popover-content dc__position-abs bg__menu--primary shadow__menu border__primary br-6 dc__overflow-hidden ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''}`,
+ className: `dc__position-abs bg__menu--primary shadow__menu border__primary br-6 dc__overflow-hidden ${isAutoWidth ? 'dc_width-max-content dc__mxw-250' : ''}`,
onKeyDown: handlePopoverKeyDown,
style: {
width: !isAutoWidth ? `${width}px` : undefined,
diff --git a/src/Shared/Components/Switch/Switch.component.tsx b/src/Shared/Components/Switch/Switch.component.tsx
new file mode 100644
index 000000000..9e32a0feb
--- /dev/null
+++ b/src/Shared/Components/Switch/Switch.component.tsx
@@ -0,0 +1,150 @@
+import { AriaAttributes, useRef } from 'react'
+import { AnimatePresence, motion } from 'framer-motion'
+
+import { Tooltip } from '@Common/Tooltip'
+import { ComponentSizeType } from '@Shared/constants'
+import { getUniqueId } from '@Shared/Helpers'
+
+import { Icon } from '../Icon'
+import { INDETERMINATE_ICON_WIDTH_MAP, LOADING_COLOR_MAP } from './constants'
+import { DTSwitchProps } from './types'
+import {
+ getSwitchContainerClass,
+ getSwitchIconColor,
+ getSwitchThumbClass,
+ getSwitchTrackColor,
+ getSwitchTrackHoverColor,
+ getThumbPadding,
+ getThumbPosition,
+} from './utils'
+
+import './switch.scss'
+
+const Switch = ({
+ ariaLabel,
+ isDisabled,
+ isLoading,
+ isChecked,
+ tooltipContent,
+ shape = 'rounded',
+ variant = 'positive',
+ iconColor,
+ iconName,
+ indeterminate = false,
+ size = ComponentSizeType.medium,
+ name,
+ onChange,
+}: DTSwitchProps) => {
+ const inputId = useRef(getUniqueId())
+
+ const getAriaCheckedValue = (): AriaAttributes['aria-checked'] => {
+ if (!isChecked) {
+ return false
+ }
+
+ return indeterminate ? 'mixed' : true
+ }
+
+ const ariaCheckedValue = getAriaCheckedValue()
+
+ const showIndeterminateIcon = ariaCheckedValue === 'mixed'
+
+ const renderContent = () => (
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+
+ {showIndeterminateIcon ? (
+
+ ) : (
+ iconName && (
+
+
+
+ )
+ )}
+
+
+ )}
+
+ )
+
+ return (
+
+
+
+
+
+ {renderContent()}
+
+
+
+ )
+}
+
+export default Switch
diff --git a/src/Shared/Components/Switch/constants.ts b/src/Shared/Components/Switch/constants.ts
new file mode 100644
index 000000000..6d1c97cbc
--- /dev/null
+++ b/src/Shared/Components/Switch/constants.ts
@@ -0,0 +1,64 @@
+import { ComponentSizeType } from '@Shared/constants'
+import { IconBaseColorType } from '@Shared/types'
+
+import { DTSwitchProps } from './types'
+
+export const ROUNDED_SWITCH_SIZE_MAP: Readonly> = {
+ [ComponentSizeType.medium]: 'w-32',
+ [ComponentSizeType.small]: 'w-24',
+}
+
+export const SQUARE_SWITCH_SIZE_MAP: typeof ROUNDED_SWITCH_SIZE_MAP = {
+ [ComponentSizeType.medium]: 'w-28',
+ [ComponentSizeType.small]: 'w-24',
+}
+
+export const SWITCH_HEIGHT_MAP: Readonly> = {
+ [ComponentSizeType.medium]: 'h-24',
+ [ComponentSizeType.small]: 'h-20',
+}
+
+export const LOADING_COLOR_MAP: Record = {
+ theme: 'B500',
+ positive: 'G500',
+}
+
+export const ROUNDED_SWITCH_TRACK_COLOR_MAP: Record = {
+ theme: 'bcb-5',
+ positive: 'bcg-5',
+}
+
+export const ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP: Record = {
+ theme: 'var(--B600)',
+ positive: 'var(--G600)',
+}
+
+export const SQUARE_SWITCH_TRACK_COLOR_MAP: typeof ROUNDED_SWITCH_TRACK_COLOR_MAP = {
+ theme: 'bcb-3',
+ positive: 'bcg-3',
+}
+
+export const SQUARE_SWITCH_TRACK_HOVER_COLOR_MAP: typeof ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP = {
+ theme: 'var(--B400)',
+ positive: 'var(--G400)',
+}
+
+export const ROUNDED_SWITCH_THUMB_SIZE_MAP: Record = {
+ [ComponentSizeType.medium]: 'icon-dim-16',
+ [ComponentSizeType.small]: 'icon-dim-12',
+}
+
+export const INDETERMINATE_ICON_WIDTH_MAP: Record = {
+ [ComponentSizeType.medium]: 'w-12',
+ [ComponentSizeType.small]: 'w-10',
+}
+
+export const SWITCH_THUMB_PADDING_MAP: Record = {
+ [ComponentSizeType.medium]: 'p-3',
+ [ComponentSizeType.small]: 'p-1',
+}
+
+export const THUMB_OUTER_PADDING_MAP: Record = {
+ rounded: 'p-2',
+ square: 'p-1',
+}
diff --git a/src/Shared/Components/Switch/index.ts b/src/Shared/Components/Switch/index.ts
new file mode 100644
index 000000000..a5ff57e39
--- /dev/null
+++ b/src/Shared/Components/Switch/index.ts
@@ -0,0 +1,2 @@
+export { default as DTSwitch } from './Switch.component'
+export type { DTSwitchProps } from './types'
diff --git a/src/Shared/Components/Switch/switch.scss b/src/Shared/Components/Switch/switch.scss
new file mode 100644
index 000000000..522b011c0
--- /dev/null
+++ b/src/Shared/Components/Switch/switch.scss
@@ -0,0 +1,9 @@
+.dt-switch {
+ &__track {
+ --switch-track-hover-color: 'transparent';
+
+ &:hover {
+ background-color: var(--switch-track-hover-color);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Shared/Components/Switch/types.ts b/src/Shared/Components/Switch/types.ts
new file mode 100644
index 000000000..fa4c6e42c
--- /dev/null
+++ b/src/Shared/Components/Switch/types.ts
@@ -0,0 +1,119 @@
+import { ButtonHTMLAttributes } from 'react'
+
+import { ComponentSizeType } from '@Shared/constants'
+import { IconBaseColorType } from '@Shared/types'
+
+import { IconName } from '../Icon'
+
+/**
+ * Represents the properties for configuring the shape and behavior of a switch component.
+ *
+ * - When `shape` is `rounded`:
+ * - The switch will have a rounded appearance.
+ * - `iconName`, `iconColor`, and `indeterminate` are not applicable.
+ *
+ * - When `shape` is `square`:
+ * - The switch will have a square appearance.
+ * - `iconName` specifies the name of the icon to display.
+ * - `iconColor` allows customization of the icon's color in the active state.
+ * - `indeterminate` indicates whether the switch is in an indeterminate state, typically used for checkboxes to represent a mixed state.
+ * If `indeterminate` is true, the switch will not be fully checked or unchecked.
+ */
+type SwitchShapeProps =
+ | {
+ /**
+ * The shape of the switch. Defaults to `rounded` if not specified.
+ */
+ shape?: 'rounded'
+
+ /**
+ * Icon name is not applicable for the `rounded` shape.
+ */
+ iconName?: never
+
+ /**
+ * Icon color is not applicable for the `rounded` shape.
+ */
+ iconColor?: never
+ /**
+ * Indicates whether the switch is in an indeterminate state.
+ * This state is typically used for checkboxes to indicate a mixed state.
+ * If true, the switch will not be fully checked or unchecked. Due this state alone we are keeping role as `checkbox` instead of `switch`.
+ * This property is not applicable for the `square` shape.
+ * @default false
+ */
+ indeterminate?: boolean
+ }
+ | {
+ /**
+ * The shape of the switch. Must be `square` to enable icon-related properties.
+ */
+ shape: 'square'
+
+ /**
+ * The name of the icon to display when the shape is `square`.
+ */
+ iconName: IconName
+
+ /**
+ * The color of the icon. If provided, this will override the default color in the active state.
+ */
+ iconColor?: IconBaseColorType
+ indeterminate?: never
+ }
+
+/**
+ * Represents the properties for the `Switch` component.
+ */
+export type DTSwitchProps = {
+ /**
+ * The ARIA label for the switch, used for accessibility purposes.
+ */
+ ariaLabel: string
+
+ /**
+ * Used in forms to identify the switch.
+ */
+ name: string
+
+ /**
+ * The visual variant of the switch.
+ *
+ * @default `positive`
+ */
+ variant?: 'theme' | 'positive'
+
+ /**
+ * The size of the switch.
+ * @default `ComponentSizeType.medium`
+ */
+ size?: Extract
+
+ /**
+ * Callback function that is called when the switch state changes.
+ * This function should handle the logic for toggling the switch.
+ */
+ onChange: ButtonHTMLAttributes['onClick']
+
+ /**
+ * Indicates whether the switch is disabled.
+ */
+ isDisabled?: boolean
+
+ /**
+ * Indicates whether the switch is in a loading state.
+ */
+ isLoading?: boolean
+
+ /**
+ * Indicates whether the switch is currently checked (on).
+ */
+ isChecked: boolean
+
+ /**
+ * Optional tooltip content to display when hovering over the switch.
+ *
+ * @default undefined
+ */
+ tooltipContent?: string
+} & SwitchShapeProps
diff --git a/src/Shared/Components/Switch/utils.ts b/src/Shared/Components/Switch/utils.ts
new file mode 100644
index 000000000..514697ff6
--- /dev/null
+++ b/src/Shared/Components/Switch/utils.ts
@@ -0,0 +1,94 @@
+import { IconBaseColorType } from '@Shared/types'
+
+import {
+ ROUNDED_SWITCH_SIZE_MAP,
+ ROUNDED_SWITCH_THUMB_SIZE_MAP,
+ ROUNDED_SWITCH_TRACK_COLOR_MAP,
+ ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP,
+ SQUARE_SWITCH_SIZE_MAP,
+ SQUARE_SWITCH_TRACK_COLOR_MAP,
+ SQUARE_SWITCH_TRACK_HOVER_COLOR_MAP,
+ SWITCH_HEIGHT_MAP,
+ SWITCH_THUMB_PADDING_MAP,
+ THUMB_OUTER_PADDING_MAP,
+} from './constants'
+import { DTSwitchProps } from './types'
+
+export const getSwitchContainerClass = ({ shape, size }: Required>): string =>
+ `${SWITCH_HEIGHT_MAP[size]} ${shape === 'rounded' ? ROUNDED_SWITCH_SIZE_MAP[size] : SQUARE_SWITCH_SIZE_MAP[size]}`
+
+export const getSwitchTrackColor = ({
+ shape,
+ variant,
+ isChecked,
+ isLoading,
+}: Required>): string => {
+ if (isLoading) {
+ return 'dc__transparent--unstyled'
+ }
+
+ if (!isChecked) {
+ return 'bcn-2'
+ }
+
+ return shape === 'rounded' ? ROUNDED_SWITCH_TRACK_COLOR_MAP[variant] : SQUARE_SWITCH_TRACK_COLOR_MAP[variant]
+}
+
+export const getSwitchTrackHoverColor = ({
+ shape,
+ variant,
+ isChecked,
+}: Required<
+ Pick
+>): (typeof ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP)[DTSwitchProps['variant']] => {
+ if (!isChecked) {
+ return 'var(--N300)'
+ }
+
+ return shape === 'rounded'
+ ? ROUNDED_SWITCH_TRACK_HOVER_COLOR_MAP[variant]
+ : SQUARE_SWITCH_TRACK_HOVER_COLOR_MAP[variant]
+}
+
+export const getSwitchThumbClass = ({
+ shape,
+ size,
+ showIndeterminateIcon,
+}: Pick & { showIndeterminateIcon: boolean }) => {
+ if (showIndeterminateIcon) {
+ return 'w-100 h-100 flex'
+ }
+
+ return `flex ${SWITCH_THUMB_PADDING_MAP[size]} ${shape === 'rounded' ? `dc__border-radius-50-per ${ROUNDED_SWITCH_THUMB_SIZE_MAP[size]}` : 'br-3'} bg__white`
+}
+
+export const getSwitchIconColor = ({
+ iconColor,
+ isChecked,
+ variant,
+}: Pick): IconBaseColorType => {
+ if (!isChecked) {
+ return 'N500'
+ }
+
+ return iconColor || (variant === 'theme' ? 'B500' : 'G500')
+}
+
+export const getThumbPosition = ({
+ isLoading,
+ isChecked,
+}: Pick): 'left' | 'right' | 'center' => {
+ if (isLoading) {
+ return 'center'
+ }
+
+ return isChecked ? 'right' : 'left'
+}
+
+export const getThumbPadding = ({ shape, isLoading }: Pick): string => {
+ if (isLoading) {
+ return ''
+ }
+
+ return THUMB_OUTER_PADDING_MAP[shape]
+}
diff --git a/src/Shared/Components/ThemeSwitcher/ThemeSwitcher.component.tsx b/src/Shared/Components/ThemeSwitcher/ThemeSwitcher.component.tsx
index 93f23dcf1..587de3b63 100644
--- a/src/Shared/Components/ThemeSwitcher/ThemeSwitcher.component.tsx
+++ b/src/Shared/Components/ThemeSwitcher/ThemeSwitcher.component.tsx
@@ -14,32 +14,31 @@
* limitations under the License.
*/
-import { ReactComponent as ICCaretLeftSmall } from '@Icons/ic-caret-left-small.svg'
import { getThemePreferenceText, useTheme } from '@Shared/Providers'
-import { LOGOUT_CARD_BASE_BUTTON_CLASS } from '../LogoutCard'
+import { Icon } from '../Icon'
import { ThemeSwitcherProps } from './types'
-const ThemeSwitcher = ({ onChange }: ThemeSwitcherProps) => {
+const ThemeSwitcher = ({ onClick }: ThemeSwitcherProps) => {
const { handleThemeSwitcherDialogVisibilityChange, themePreference } = useTheme()
const handleShowThemeSwitcherDialog = () => {
handleThemeSwitcherDialogVisibilityChange(true)
- onChange()
+ onClick?.()
}
return (
- Theme
-
- {getThemePreferenceText(themePreference)}
-
-
+ Theme
+
+ {getThemePreferenceText(themePreference)}
+
+
)
}
diff --git a/src/Shared/Components/ThemeSwitcher/index.ts b/src/Shared/Components/ThemeSwitcher/index.ts
index 5913dfa3f..9dba93c23 100644
--- a/src/Shared/Components/ThemeSwitcher/index.ts
+++ b/src/Shared/Components/ThemeSwitcher/index.ts
@@ -15,4 +15,3 @@
*/
export { default as ThemeSwitcher } from './ThemeSwitcher.component'
-export type { ThemeSwitcherProps } from './types'
diff --git a/src/Shared/Components/ThemeSwitcher/types.ts b/src/Shared/Components/ThemeSwitcher/types.ts
index a75045c9b..f86536088 100644
--- a/src/Shared/Components/ThemeSwitcher/types.ts
+++ b/src/Shared/Components/ThemeSwitcher/types.ts
@@ -15,5 +15,5 @@
*/
export interface ThemeSwitcherProps {
- onChange: () => void
+ onClick?: () => void
}
diff --git a/src/Shared/Components/index.ts b/src/Shared/Components/index.ts
index 89dcbeb34..a6252dbd6 100644
--- a/src/Shared/Components/index.ts
+++ b/src/Shared/Components/index.ts
@@ -42,7 +42,7 @@ export * from './DatePicker'
export * from './DeploymentConfigDiff'
export * from './DeploymentStatusBreakdown'
export * from './DetectBottom'
-export * from './DiffViewer'
+export * from './DocLink'
export * from './DynamicDataTable'
export * from './EditableTextArea'
export * from './EditImageFormField'
@@ -88,6 +88,7 @@ export * from './SelectPicker'
export * from './ShowMoreText'
export * from './SSOProviderIcon'
export * from './StatusComponent'
+export * from './Switch'
export * from './TabGroup'
export * from './Table'
export * from './TagsKeyValueTable'
diff --git a/src/Shared/Helpers.tsx b/src/Shared/Helpers.tsx
index 2120400ef..b11bccb57 100644
--- a/src/Shared/Helpers.tsx
+++ b/src/Shared/Helpers.tsx
@@ -19,6 +19,7 @@ import { ReactElement, useEffect, useRef, useState } from 'react'
import { PromptProps } from 'react-router-dom'
import { StrictRJSFSchema } from '@rjsf/utils'
import Tippy from '@tippyjs/react'
+import { animate } from 'framer-motion'
import moment from 'moment'
import { nanoid } from 'nanoid'
import { Pair } from 'yaml'
@@ -52,7 +53,7 @@ import {
} from '../Common'
import { getAggregator } from '../Pages'
import { AggregatedNodes, PodMetadatum } from './Components'
-import { UNSAVED_CHANGES_PROMPT_MESSAGE } from './constants'
+import { CUBIC_BEZIER_CURVE, UNSAVED_CHANGES_PROMPT_MESSAGE } from './constants'
import {
AggregationKeys,
BorderConfigType,
@@ -700,3 +701,16 @@ export const getAppDetailsURL = (appId: number | string, envId?: number | string
}
return baseURL
}
+
+export const smoothScrollToTop = (scrollContainer: HTMLElement, targetPosition: number) => {
+ const start = scrollContainer.scrollTop
+
+ const controls = animate(start, targetPosition, {
+ ease: CUBIC_BEZIER_CURVE,
+ onUpdate: (value) => {
+ scrollContainer.scrollTop = value
+ },
+ })
+
+ return controls
+}
diff --git a/src/Shared/Providers/index.ts b/src/Shared/Providers/index.ts
index 1ba004223..a1dcc842c 100644
--- a/src/Shared/Providers/index.ts
+++ b/src/Shared/Providers/index.ts
@@ -17,5 +17,5 @@
export * from './ImageSelectionUtility'
export * from './MainContextProvider'
export * from './ThemeProvider'
-export type { MainContext, ReloadVersionConfigTypes } from './types'
+export type { MainContext, ReloadVersionConfigTypes, SidePanelConfig } from './types'
export * from './UserEmailProvider'
diff --git a/src/Shared/Providers/types.ts b/src/Shared/Providers/types.ts
index 82f1630cb..4911a42b9 100644
--- a/src/Shared/Providers/types.ts
+++ b/src/Shared/Providers/types.ts
@@ -29,6 +29,16 @@ export interface ReloadVersionConfigTypes {
updateToastRef: MutableRefObject> | null
isRefreshing: boolean
}
+
+export interface SidePanelConfig {
+ /** Determines whether the side panel is visible */
+ open: boolean
+ /** Optional flag to reset/reinitialize the side panel state */
+ reinitialize?: boolean
+ /** URL to documentation that should be displayed in the panel */
+ docLink: string | null
+}
+
export interface MainContext {
serverMode: SERVER_MODE
setServerMode: (serverMode: SERVER_MODE) => void
@@ -78,6 +88,20 @@ export interface MainContext {
reloadVersionConfig: ReloadVersionConfigTypes
intelligenceConfig: IntelligenceConfig
setIntelligenceConfig: Dispatch>
+
+ sidePanelConfig: SidePanelConfig
+ setSidePanelConfig: Dispatch>
+
+ /**
+ * Indicates whether the current Devtron instance is running as an Enterprise edition. \
+ * This flag is determined based on server-side configuration.
+ */
+ isEnterprise: boolean
+ /**
+ * Indicates whether the fe-lib modules are available in the current instance. \
+ * Used to conditionally render or enable features that depend on fe-lib
+ */
+ isFELibAvailable: boolean
}
export interface MainContextProviderProps {
diff --git a/src/Shared/constants.tsx b/src/Shared/constants.tsx
index 501c21c70..948a007d9 100644
--- a/src/Shared/constants.tsx
+++ b/src/Shared/constants.tsx
@@ -580,3 +580,4 @@ export const DEPLOYMENT_STAGE_TO_NODE_MAP: Readonly = {
[K in keyof T]?: never
}
+/**
+ * A utility type that filters out properties from type `T` that are of type `never`. \
+ * This is useful when you want to remove properties that have been marked as `never` from a type,
+ * effectively creating a new type without those properties.
+ *
+ * @template T - The input type from which to filter out `never` properties.
+ * @example
+ * ```typescript
+ * type User = {
+ * id: number;
+ * name: string;
+ * deleted: never;
+ * }
+ *
+ * type ActiveUser = OmitNever; // { id: number; name: string; }
+ * ```
+ */
+export type OmitNever = {
+ [K in keyof T as T[K] extends never ? never : K]: T[K]
+}
+
export interface TargetPlatformItemDTO {
name: string
}
diff --git a/src/Shared/validations.tsx b/src/Shared/validations.tsx
index 7f2c3cf06..c55244015 100644
--- a/src/Shared/validations.tsx
+++ b/src/Shared/validations.tsx
@@ -497,3 +497,25 @@ export const validateYAML = (yamlString: string, isRequired?: boolean): Validati
}
}
}
+
+export const validateEmail = (email: string): ValidationResponseType => {
+ if (!email) {
+ return {
+ isValid: false,
+ message: 'Email is required',
+ }
+ }
+
+ const result = PATTERNS.EMAIL.test(String(email).toLowerCase())
+
+ if (result) {
+ return {
+ isValid: true,
+ }
+ }
+
+ return {
+ isValid: false,
+ message: 'Please provide a valid email address',
+ }
+}