-
- {updatedBy && _date && (
-
- Last updated by
- {updatedBy} on {_date}
-
- )}
-
- Edit
-
+
+ {!isEditView ? (
+
+
+
-
- Promise.resolve()
- }
- />
-
- ) : (
-
-
getEditorCustomIcon(commandName)}
- toolbarCommands={MARKDOWN_EDITOR_COMMANDS}
- value={modifiedDescriptionText}
- onChange={setModifiedDescriptionText}
- minEditorHeight={minEditorHeight}
- minPreviewHeight={150}
- selectedTab={editorView}
- onTabChange={handleTabChange}
- generateMarkdownPreview={(markdown: string) =>
- Promise.resolve(
- ,
- )
- }
- childProps={{
- writeButton: {
- className: `tab-list__tab pointer fs-13 ${
- editorView === MDEditorSelectedTabType.WRITE && 'cb-5 fw-6 active active-tab'
- }`,
- },
- previewButton: {
- className: `tab-list__tab pointer fs-13 ${
- editorView === MDEditorSelectedTabType.PREVIEW && 'cb-5 fw-6 active active-tab'
- }`,
- },
- textArea: {
- tabIndex,
- },
- }}
- />
- {editorView === MDEditorSelectedTabType.WRITE && (
-
-
-
-
-
+ {updatedBy && _date && (
+
+ Last updated by
+ {updatedBy} on {_date}
)}
+
- )}
-
+
+ {renderMarkdown(parsedText)}
+
+ ) : (
+ <>
+
+
+
+ >
+ )}
)
}
diff --git a/src/Common/GenericDescription/constant.ts b/src/Common/GenericDescription/constant.ts
deleted file mode 100644
index 7cb7e7a03..000000000
--- a/src/Common/GenericDescription/constant.ts
+++ /dev/null
@@ -1,18 +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 const DESCRIPTION_EMPTY_ERROR_MSG = 'Readme cannot be empty. Please add some information or cancel the changes.'
-export const DESCRIPTION_UNSAVED_CHANGES_MSG = 'Are you sure you want to discard your changes?'
diff --git a/src/Common/GenericDescription/constant.tsx b/src/Common/GenericDescription/constant.tsx
new file mode 100644
index 000000000..e58edda61
--- /dev/null
+++ b/src/Common/GenericDescription/constant.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 { commands } from '@uiw/react-md-editor'
+
+import { ReactComponent as BoldIcon } from '@Icons/ic-bold.svg'
+import { ReactComponent as CheckedListIcon } from '@Icons/ic-checked-list.svg'
+import { ReactComponent as CodeIcon } from '@Icons/ic-code.svg'
+import { ReactComponent as HeaderIcon } from '@Icons/ic-header.svg'
+import { ReactComponent as ImageIcon } from '@Icons/ic-image.svg'
+import { ReactComponent as ItalicIcon } from '@Icons/ic-italic.svg'
+import { ReactComponent as LinkIcon } from '@Icons/ic-link.svg'
+import { ReactComponent as OrderedListIcon } from '@Icons/ic-ordered-list.svg'
+import { ReactComponent as QuoteIcon } from '@Icons/ic-quote.svg'
+import { ReactComponent as StrikethroughIcon } from '@Icons/ic-strikethrough.svg'
+import { ReactComponent as UnorderedListIcon } from '@Icons/ic-unordered-list.svg'
+
+export const DESCRIPTION_EMPTY_ERROR_MSG = 'Readme cannot be empty. Please add some information or cancel the changes.'
+export const DESCRIPTION_UNSAVED_CHANGES_MSG = 'Are you sure you want to discard your changes?'
+
+export const TOOLBAR_SECONDARY_COMMANDS = [
+ {
+ ...commands.bold,
+ icon:
,
+ },
+ {
+ ...commands.italic,
+ icon:
,
+ },
+ {
+ ...commands.strikethrough,
+ icon:
,
+ },
+ {
+ ...commands.heading,
+ icon:
,
+ },
+ {
+ ...commands.quote,
+ icon:
,
+ },
+ {
+ ...commands.code,
+ icon:
,
+ },
+ {
+ ...commands.link,
+ icon:
,
+ },
+ {
+ ...commands.image,
+ icon:
,
+ },
+ {
+ ...commands.orderedListCommand,
+ icon:
,
+ },
+ {
+ ...commands.unorderedListCommand,
+ icon:
,
+ },
+ {
+ ...commands.checkedListCommand,
+ icon:
,
+ },
+]
diff --git a/src/Common/GenericDescription/genericDescription.scss b/src/Common/GenericDescription/genericDescription.scss
index ca49f8123..3a27c9951 100644
--- a/src/Common/GenericDescription/genericDescription.scss
+++ b/src/Common/GenericDescription/genericDescription.scss
@@ -14,165 +14,95 @@
* limitations under the License.
*/
-.tab-description {
- .tab-list__tab {
- .tab-hover {
- &:hover,
- &.active {
- color: var(--B500);
- .edit-yaml-icon {
- path {
- stroke: var(--B500);
- }
- }
- .terminal-icon {
- path {
- fill: var(--B500);
- }
- }
- }
- }
- .node-details__active-tab {
- height: 2px;
- background-color: var(--B500);
- border-radius: 2px 2px 0px 0px;
- margin-top: -1px;
- }
- }
-}
-
-.cluster__body-details {
+.markdown-editor__wrapper {
flex-basis: 100%;
.pencil-icon:hover {
color: var(--B500);
text-decoration: none;
+
svg path {
stroke: var(--B500);
}
}
- .mark-down-editor-container {
- border: 1px solid var(--N200);
- border-radius: 3px 3px 0 0;
- font-family: 'Open Sans', Arial, sans-serif !important;
- font-size: 13px !important;
- line-height: 1.67;
-
- .mark-down-editor-preview {
- background-color: var(--bg-primary) !important;
- }
-
- .mark-down-editor-textarea-wrapper {
- border-radius: 3px !important;
- border: 1px solid var(--N200) !important;
- background-color: var(--bg-secondary) !important;
- }
+ .mark-down-editor__no-border {
+ border: 0px;
+ }
- .mde-textarea-wrapper {
- background-color: var(--bg-primary) !important;
- padding: 10px !important;
- }
+ .w-md-editor {
+ background-color: transparent;
+ box-shadow: none;
+
+ &-toolbar {
+ border-bottom: 1px solid var(--border-primary);
+ background-color: transparent;
+ padding-inline: 16px;
+ padding-block: 0px;
+ height: auto;
+
+ li {
+ .markdown-editor__tab-button {
+ margin-block: 8px;
+ background-color: var(--bg-primary);
+ font-size: 13px;
+ line-height: 20px;
+ text-align: center;
+ color: var(--N700);
+ margin-right: 8px;
+ }
- .mark-down-editor-toolbar {
- justify-content: space-between !important;
- padding-left: 16px !important;
- padding-right: 16px !important;
- background-color: var(--bg-primary) !important;
- border-bottom: 1px solid var(--N200);
- }
+ &.active {
+ position: relative;
- .mde-header {
- height: 36px !important;
- .mde-tabs {
- height: inherit !important;
- button {
- padding-left: 0px !important;
- padding-right: 0px !important;
- margin: 0 16px 0 0px !important;
- padding-top: 4px !important;
- }
- .active-tab {
- border-bottom: 2px solid var(--B500) !important;
- border-radius: 0px !important;
+ .markdown-editor__tab-button {
+ font-size: 13px;
+ line-height: 20px;
+ text-align: center;
+ margin-right: 8px;
+ color: var(--B500);
+ }
}
- }
- ul.mde-header-group {
- height: inherit !important;
- display: flex !important;
- justify-content: center !important;
- align-items: center !important;
- padding-top: 0px !important;
- padding-bottom: 0px !important;
- padding-right: 0px !important;
- .mde-header-item button {
- padding: 10px 4px !important;
- height: inherit !important;
+
+ &.active::after {
+ content: '';
+ position: absolute;
+ display: block;
+ width: 100%;
+ height: 2px;
+ background-color: var(--B500);
+ border-radius: 4px;
+ bottom: 0px;
}
}
}
- .mde-preview {
- .mde-preview-content {
- padding: 0px !important;
-
- a {
- color: var(--B500);
-
- &.anchor {
- color: var(--N900);
+ .w-md-editor-content {
+ background: var(--bg-secondary);
+ border-radius: 3px;
+ margin: 12px;
+ border: 1px solid var(--border-primary);
+
+ &:has(.w-md-editor-preview) {
+ background: var(--bg-primary);
+ border-radius: 0px;
+ margin: 0px;
+ border: none;
+ }
- h1,
- h2,
- h3 {
- border-bottom: 1px solid var(--N200);
- }
- }
- }
+ .w-md-editor-preview {
+ margin: 0px;
+ padding: 8px 0px;
+ }
- pre {
- background-color: var(--bg-secondary);
+ .w-md-editor-input {
+ display: flex;
+ flex-direction: column;
- // Would need to add theming to fix this altogether
- > code {
- color: var(--B700);
- background-color: var(--transparent);
- border-radius: 1px;
- display: inline;
- margin: 0;
- overflow: visible;
- line-height: inherit;
- word-wrap: normal;
- white-space: pre;
- border: 0;
- padding: 0;
- }
+ .w-md-editor-text {
+ flex-grow: 1;
}
}
}
-
- .mark-down-editor__hidden {
- display: none !important;
- }
- }
-
- .mark-down-editor__no-border {
- border: 0px;
- }
-
- .cluster__description-footer {
- border-radius: 0 0 3px 3px;
- border: 1px solid var(--N200);
- border-top: 0px;
}
-}
-
-.cluster__description-footer {
- border-radius: 0 0 3px 3px;
- border: 1px solid var(--N200);
-}
-
-.create-app-description {
- border-bottom: 1px solid var(--N200) !important;
- border-radius: 3px !important;
-}
+}
\ No newline at end of file
diff --git a/src/Common/GenericDescription/types.ts b/src/Common/GenericDescription/types.ts
index 802b5c9f7..7a7584d78 100644
--- a/src/Common/GenericDescription/types.ts
+++ b/src/Common/GenericDescription/types.ts
@@ -20,15 +20,8 @@ export interface GenericDescriptionProps {
updatedOn?: string
updateDescription: (string) => Promise
title: string
- tabIndex?: number
- minEditorHeight?: number
emptyStateConfig?: {
img: string
subtitle: JSX.Element
}
}
-
-export enum MDEditorSelectedTabType {
- WRITE = 'write',
- PREVIEW = 'preview',
-}
diff --git a/src/Common/GenericDescription/utils.tsx b/src/Common/GenericDescription/utils.tsx
index b4e7f8c7e..0f0ccd4ee 100644
--- a/src/Common/GenericDescription/utils.tsx
+++ b/src/Common/GenericDescription/utils.tsx
@@ -14,23 +14,9 @@
* limitations under the License.
*/
-import Tippy from '@tippyjs/react'
import moment from 'moment'
-import { ReactComponent as BoldIcon } from '@Icons/ic-bold.svg'
-import { ReactComponent as CheckedListIcon } from '@Icons/ic-checked-list.svg'
-import { ReactComponent as CodeIcon } from '@Icons/ic-code.svg'
-import { ReactComponent as HeaderIcon } from '@Icons/ic-header.svg'
-import { ReactComponent as ImageIcon } from '@Icons/ic-image.svg'
-import { ReactComponent as ItalicIcon } from '@Icons/ic-italic.svg'
-import { ReactComponent as LinkIcon } from '@Icons/ic-link.svg'
-import { ReactComponent as OrderedListIcon } from '@Icons/ic-ordered-list.svg'
-import { ReactComponent as QuoteIcon } from '@Icons/ic-quote.svg'
-import { ReactComponent as StrikethroughIcon } from '@Icons/ic-strikethrough.svg'
-import { ReactComponent as UnorderedListIcon } from '@Icons/ic-unordered-list.svg'
import { DATE_TIME_FORMATS, ZERO_TIME_STRING } from '@Common/Constants'
-import { logExceptionToSentry } from '@Common/Helper'
-import { MARKDOWN_EDITOR_COMMAND_ICON_TIPPY_CONTENT, MARKDOWN_EDITOR_COMMAND_TITLE } from '@Common/Markdown/constant'
export const getParsedUpdatedOnDate = (updatedOn: string) => {
if (!updatedOn || updatedOn === ZERO_TIME_STRING) {
@@ -41,154 +27,3 @@ export const getParsedUpdatedOnDate = (updatedOn: string) => {
return _moment.isValid() ? _moment.format(DATE_TIME_FORMATS.TWELVE_HOURS_FORMAT) : updatedOn
}
-
-export const getEditorCustomIcon = (commandName: string): JSX.Element => {
- switch (commandName) {
- case MARKDOWN_EDITOR_COMMAND_TITLE.HEADER:
- return (
-
-
-
-
-
- )
- case MARKDOWN_EDITOR_COMMAND_TITLE.BOLD:
- return (
-
-
-
-
-
- )
- case MARKDOWN_EDITOR_COMMAND_TITLE.ITALIC:
- return (
-
-
-
-
-
- )
- case MARKDOWN_EDITOR_COMMAND_TITLE.STRIKETHROUGH:
- return (
-
-
-
-
-
- )
- case MARKDOWN_EDITOR_COMMAND_TITLE.LINK:
- return (
-
-
-
-
-
- )
- case MARKDOWN_EDITOR_COMMAND_TITLE.QUOTE:
- return (
-
-
-
-
-
- )
- case MARKDOWN_EDITOR_COMMAND_TITLE.CODE:
- return (
-
-
-
-
-
- )
- case MARKDOWN_EDITOR_COMMAND_TITLE.IMAGE:
- return (
-
-
-
-
-
- )
- case MARKDOWN_EDITOR_COMMAND_TITLE.UNORDERED_LIST:
- return (
-
-
-
-
-
- )
- case MARKDOWN_EDITOR_COMMAND_TITLE.ORDERED_LIST:
- return (
-
-
-
-
-
- )
- case MARKDOWN_EDITOR_COMMAND_TITLE.CHECKED_LIST:
- return (
-
-
-
-
-
- )
- default:
- logExceptionToSentry('Invalid command for MDE')
- return null
- }
-}
diff --git a/src/Common/Markdown/constant.ts b/src/Common/Markdown/constant.ts
deleted file mode 100644
index 1c2ce8a38..000000000
--- a/src/Common/Markdown/constant.ts
+++ /dev/null
@@ -1,63 +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 const DEFAULT_MARKDOWN_EDITOR_PREVIEW_MESSAGE = `
-
-Nothing to preview
-`
-export const MARKDOWN_EDITOR_COMMANDS = [
- [
- 'header',
- 'bold',
- 'italic',
- 'strikethrough',
- 'link',
- 'quote',
- 'code',
- 'image',
- 'unordered-list',
- 'ordered-list',
- 'checked-list',
- ],
-]
-
-export enum MARKDOWN_EDITOR_COMMAND_TITLE {
- HEADER = 'header',
- BOLD = 'bold',
- ITALIC = 'italic',
- STRIKETHROUGH = 'strikethrough',
- LINK = 'link',
- QUOTE = 'quote',
- CODE = 'code',
- IMAGE = 'image',
- UNORDERED_LIST = 'unordered-list',
- ORDERED_LIST = 'ordered-list',
- CHECKED_LIST = 'checked-list',
-}
-
-export const MARKDOWN_EDITOR_COMMAND_ICON_TIPPY_CONTENT = {
- [MARKDOWN_EDITOR_COMMAND_TITLE.HEADER]: 'Add heading text',
- [MARKDOWN_EDITOR_COMMAND_TITLE.BOLD]: 'Add bold text',
- [MARKDOWN_EDITOR_COMMAND_TITLE.ITALIC]: 'Add italic text',
- [MARKDOWN_EDITOR_COMMAND_TITLE.STRIKETHROUGH]: 'Add strikethrough text',
- [MARKDOWN_EDITOR_COMMAND_TITLE.LINK]: 'Add a link',
- [MARKDOWN_EDITOR_COMMAND_TITLE.QUOTE]: 'Add a quote',
- [MARKDOWN_EDITOR_COMMAND_TITLE.CODE]: 'Add code',
- [MARKDOWN_EDITOR_COMMAND_TITLE.IMAGE]: 'Add image via link',
- [MARKDOWN_EDITOR_COMMAND_TITLE.UNORDERED_LIST]: 'Add a bulleted list',
- [MARKDOWN_EDITOR_COMMAND_TITLE.ORDERED_LIST]: 'Add a numbered list',
- [MARKDOWN_EDITOR_COMMAND_TITLE.CHECKED_LIST]: 'Add a task list',
-}
diff --git a/src/Common/Modals/VisibleModal.tsx b/src/Common/Modals/VisibleModal.tsx
index e4a6f5c18..e3bff4150 100644
--- a/src/Common/Modals/VisibleModal.tsx
+++ b/src/Common/Modals/VisibleModal.tsx
@@ -27,6 +27,7 @@ export class VisibleModal extends React.Component<{
close?: (e?) => void
onEscape?: (e?) => void
initialFocus?: DTFocusTrapType['initialFocus']
+ avoidFocusTrap?: boolean
}> {
constructor(props) {
super(props)
@@ -56,6 +57,7 @@ export class VisibleModal extends React.Component<{
onEscape={this.escFunction}
onClick={this.handleBodyClick}
initialFocus={this.props.initialFocus ?? undefined}
+ avoidFocusTrap={this.props.avoidFocusTrap}
>
{this.props.children}
diff --git a/src/Common/SegmentedBarChart/SegmentedBarChart.tsx b/src/Common/SegmentedBarChart/SegmentedBarChart.tsx
index 3d598fba0..24b5096c9 100644
--- a/src/Common/SegmentedBarChart/SegmentedBarChart.tsx
+++ b/src/Common/SegmentedBarChart/SegmentedBarChart.tsx
@@ -59,7 +59,9 @@ const SegmentedBarChart: React.FC
= ({
) : (
- {isProportional && !hideTotal ? `${value}/${total}` : value}
+ {isProportional && !hideTotal
+ ? `${value.toLocaleString()}/${total.toLocaleString()}`
+ : value.toLocaleString()}
)
diff --git a/src/Pages-Devtron-2.0/Navigation/types.ts b/src/Pages-Devtron-2.0/Navigation/types.ts
index fb8fb1f65..c0dcc41cf 100644
--- a/src/Pages-Devtron-2.0/Navigation/types.ts
+++ b/src/Pages-Devtron-2.0/Navigation/types.ts
@@ -8,7 +8,6 @@ export type NavigationItemID =
| 'application-management-application-groups'
| 'application-management-bulk-edit'
| 'application-management-application-templates'
- | 'application-management-projects'
| 'application-management-configurations'
| 'application-management-policies'
| 'infrastructure-management-overview'
@@ -39,6 +38,7 @@ export type NavigationItemID =
| 'global-configuration-cluster-and-environments'
| 'global-configuration-container-oci-registry'
| 'global-configuration-authorization'
+ | 'global-configuration-projects'
| 'data-protection-overview'
| 'data-protection-backup-and-schedule'
| 'data-protection-restores'
diff --git a/src/Shared/Components/Backdrop/Backdrop.tsx b/src/Shared/Components/Backdrop/Backdrop.tsx
index 323424d69..8865837de 100644
--- a/src/Shared/Components/Backdrop/Backdrop.tsx
+++ b/src/Shared/Components/Backdrop/Backdrop.tsx
@@ -33,6 +33,7 @@ const Backdrop = ({
deactivateFocusOnEscape = true,
initialFocus,
returnFocusOnDeactivate,
+ avoidFocusTrap,
}: BackdropProps) => {
// STATES
const [portalContainer, setPortalContainer] = useState(null)
@@ -92,6 +93,7 @@ const Backdrop = ({
deactivateFocusOnEscape={deactivateFocusOnEscape}
initialFocus={initialFocus ?? undefined}
returnFocusOnDeactivate={returnFocusOnDeactivate}
+ avoidFocusTrap={avoidFocusTrap}
>
{
+ extends Pick<
+ DTFocusTrapType,
+ 'deactivateFocusOnEscape' | 'initialFocus' | 'onEscape' | 'returnFocusOnDeactivate' | 'avoidFocusTrap'
+ > {
/**
* The content to be rendered within the backdrop component.
*/
diff --git a/src/Shared/Components/CICDHistory/types.tsx b/src/Shared/Components/CICDHistory/types.tsx
index 58a299b95..a2ab75708 100644
--- a/src/Shared/Components/CICDHistory/types.tsx
+++ b/src/Shared/Components/CICDHistory/types.tsx
@@ -642,13 +642,6 @@ export interface AggregatedNodes {
}
}
-export interface PodMetadatum {
- name: string
- uid: string
- containers: string[]
- isNew: boolean
-}
-
export const STATUS_SORTING_ORDER = {
[NodeStatus.Missing]: 1,
[NodeStatus.Degraded]: 2,
diff --git a/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx b/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx
index 9ab7d5cd8..004cbe2b9 100644
--- a/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx
+++ b/src/Shared/Components/ConfirmationModal/ConfirmationModal.tsx
@@ -46,6 +46,7 @@ const ConfirmationModalBody = ({
shouldCloseOnEscape = true,
isLandscapeView = false,
showConfetti = false,
+ avoidFocusTrap = false,
}: ConfirmationModalBodyProps) => {
const { registerShortcut, unregisterShortcut } = useRegisterShortcut()
@@ -92,6 +93,7 @@ const ConfirmationModalBody = ({
deactivateFocusOnEscape={shouldCloseOnEscape}
// Since when custom input is present, we auto focus on input, else focus on primary button
initialFocus={confirmationConfig ? false : `#${PRIMARY_BUTTON_ID}`}
+ avoidFocusTrap={avoidFocusTrap}
>
= PropsWith
* @default false
*/
showConfetti?: boolean
+ avoidFocusTrap?: boolean
}> &
ButtonConfigAndVariantType &
(isConfig extends false
diff --git a/src/Shared/Components/DTFocusTrap/DTFocusTrap.tsx b/src/Shared/Components/DTFocusTrap/DTFocusTrap.tsx
index 3bb280cd2..01d68273c 100644
--- a/src/Shared/Components/DTFocusTrap/DTFocusTrap.tsx
+++ b/src/Shared/Components/DTFocusTrap/DTFocusTrap.tsx
@@ -14,40 +14,22 @@
* limitations under the License.
*/
-import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
+import { useCallback, useEffect } from 'react'
import FocusTrap from 'focus-trap-react'
-import { noop } from '@Common/Helper'
import { ALLOW_ACTION_OUTSIDE_FOCUS_TRAP } from '@Shared/constants'
import { preventBodyScroll } from '@Shared/Helpers'
import { DTFocusTrapType } from './types'
-const FocusTrapControlContext = createContext<{
- disableFocusTrap: () => void
- resumeFocusTrap: () => void
-}>(null)
-
-export const useFocusTrapControl = () => {
- const context = useContext(FocusTrapControlContext)
- if (!context) {
- return {
- disableFocusTrap: noop,
- resumeFocusTrap: noop,
- }
- }
- return context
-}
-
const DTFocusTrap = ({
onEscape,
deactivateFocusOnEscape = true,
children,
initialFocus = undefined,
returnFocusOnDeactivate = true,
+ avoidFocusTrap = false,
}: DTFocusTrapType) => {
- const [isFocusEnabled, setIsFocusEnabled] = useState(true)
-
const handleEscape = useCallback(
(e?: KeyboardEvent | MouseEvent) => {
onEscape(e)
@@ -56,48 +38,51 @@ const DTFocusTrap = ({
[onEscape, deactivateFocusOnEscape],
)
+ // Focus escape key bind when focus trap is avoided
+ const handleEscapeKeyBind = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ onEscape(e)
+ }
+ }
+
useEffect(() => {
+ if (avoidFocusTrap) {
+ document.addEventListener('keydown', handleEscapeKeyBind)
+ }
preventBodyScroll(true)
return () => {
preventBodyScroll(false)
+ if (avoidFocusTrap) {
+ document.removeEventListener('keydown', handleEscapeKeyBind)
+ }
}
}, [])
- const focusContextValue = useMemo(
- () => ({
- disableFocusTrap: () => setIsFocusEnabled(false),
- resumeFocusTrap: () => setIsFocusEnabled(true),
- }),
- [],
- )
-
return (
-
- {
- // Allow up to 3 parent levels to check for the allowed class
- let el = event.target as Element | null
- let depth = 0
- while (el && depth < 4) {
- if (el.classList && el.classList.contains(ALLOW_ACTION_OUTSIDE_FOCUS_TRAP)) {
- return true
- }
- el = el.parentElement
- depth += 1
+ {
+ // Allow up to 3 parent levels to check for the allowed class
+ let el = event.target as Element | null
+ let depth = 0
+ while (el && depth < 4) {
+ if (el.classList && el.classList.contains(ALLOW_ACTION_OUTSIDE_FOCUS_TRAP)) {
+ return true
}
- return false
- },
- returnFocusOnDeactivate,
- }}
- >
- {children}
-
-
+ el = el.parentElement
+ depth += 1
+ }
+ return false
+ },
+ returnFocusOnDeactivate,
+ }}
+ >
+ {children}
+
)
}
diff --git a/src/Shared/Components/DTFocusTrap/index.tsx b/src/Shared/Components/DTFocusTrap/index.tsx
index d0506f829..488c307f6 100644
--- a/src/Shared/Components/DTFocusTrap/index.tsx
+++ b/src/Shared/Components/DTFocusTrap/index.tsx
@@ -14,5 +14,5 @@
* limitations under the License.
*/
-export { default as DTFocusTrap, useFocusTrapControl } from './DTFocusTrap'
+export { default as DTFocusTrap } from './DTFocusTrap'
export type { DTFocusTrapType } from './types'
diff --git a/src/Shared/Components/DTFocusTrap/types.ts b/src/Shared/Components/DTFocusTrap/types.ts
index b2ddd5d6b..4da661e49 100644
--- a/src/Shared/Components/DTFocusTrap/types.ts
+++ b/src/Shared/Components/DTFocusTrap/types.ts
@@ -50,4 +50,5 @@ export interface DTFocusTrapType extends Pick void)
+ avoidFocusTrap?: boolean
}
diff --git a/src/Shared/Components/DatePicker/DateTimePicker.scss b/src/Shared/Components/DatePicker/DateTimePicker.scss
new file mode 100644
index 000000000..5456d0ed9
--- /dev/null
+++ b/src/Shared/Components/DatePicker/DateTimePicker.scss
@@ -0,0 +1,78 @@
+.date-time-picker {
+ --rdp-accent-color: var(--B500);
+ --rdp-accent-background-color: var(--N700);
+ --rdp-day_button-border-radius: 0;
+ --rdp-day_button-border: 2px solid transparent;
+ --rdp-selected-border: none;
+ --rdp-today-color: var(--rdp-accent-color);
+ --rdp-range_middle-background-color: var(--O500);
+ --rdp-range_middle-color: inherit;
+ --rdp-range_start-color: var(--N100);
+ --rdp-range_start-background: linear-gradient(var(--O200), transparent 50%, var(--rdp-range_middle-background-color) 50%);
+ --rdp-range_start-date-background-color: var(--rdp-accent-color);
+ --rdp-range_end-background: linear-gradient(var(--O200), var(--rdp-range_middle-background-color) 50%, transparent 50%);
+ --rdp-range_end-color: var(--N100);
+ --rdp-range_end-date-background-color: var(--rdp-accent-color);
+ --rdp-week_number-border-radius: 100%;
+ --rdp-week_number-border: 2px solid var(--R100);
+
+ .rdp-day {
+ border: 1px solid var(--border-primary);
+
+ &_button {
+ color: var(--N900);
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 20px;
+ border-radius: 0;
+ width: 100%;
+ height: 100%;
+
+ &:hover {
+ background: var(--bg-hover);
+ }
+ }
+ }
+
+ .rdp-day.rdp-hidden {
+ border: none;
+ }
+
+ .rdp-day.rdp-selected {
+ .rdp-day_button {
+ background: var(--B100);
+ color: var(--B500);
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 20px;
+ border: none;
+ border-radius: 0;
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ .rdp-day.rdp-disabled {
+ .rdp-day_button {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+ }
+
+ .rdp-caption_label {
+ color: var(--N900);
+ font-size: 16px;
+ font-weight: 600;
+ }
+
+ .rdp-month_grid {
+ margin-top: 12px;
+ }
+
+ .rdp-weekday {
+ color: var(--N700);
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 20px;
+ }
+}
\ No newline at end of file
diff --git a/src/Shared/Components/DatePicker/DateTimePicker.tsx b/src/Shared/Components/DatePicker/DateTimePicker.tsx
index 5b4c22dc6..233663eee 100644
--- a/src/Shared/Components/DatePicker/DateTimePicker.tsx
+++ b/src/Shared/Components/DatePicker/DateTimePicker.tsx
@@ -14,31 +14,43 @@
* limitations under the License.
*/
-import { useEffect, useState } from 'react'
-import { SingleDatePicker } from 'react-dates'
-import CustomizableCalendarDay from 'react-dates/esm/components/CustomizableCalendarDay'
+import { useMemo, useRef } from 'react'
+import { DateRange, DayPicker, OnSelectHandler } from 'react-day-picker'
import { SelectInstance } from 'react-select'
-import moment from 'moment'
+import dayjs from 'dayjs'
-import { ReactComponent as CalendarIcon } from '@Icons/ic-calendar.svg'
import { ReactComponent as ClockIcon } from '@Icons/ic-clock.svg'
import { ReactComponent as ICWarning } from '@Icons/ic-warning.svg'
+import { DATE_TIME_FORMATS } from '@Common/Constants'
import { ComponentSizeType } from '@Shared/constants'
+import { getUniqueId } from '@Shared/Helpers'
-import 'react-dates/initialize'
-
-import { DATE_TIME_FORMATS } from '../../../Common'
-import { useFocusTrapControl } from '../DTFocusTrap'
-import { SelectPicker } from '../SelectPicker'
-import { customDayStyles, DATE_PICKER_IDS, DATE_PICKER_PLACEHOLDER } from './constants'
-import { DateTimePickerProps } from './types'
+import { Icon } from '../Icon'
+import { Popover, usePopover } from '../Popover'
+import { SelectPicker, SelectPickerOptionType } from '../SelectPicker'
+import { DATE_PICKER_CUSTOM_COMPONENTS, DATE_PICKER_IDS, DATE_PICKER_PLACEHOLDER } from './constants'
+import { DateTimePickerProps, UpdateDateRangeType } from './types'
import { DEFAULT_TIME_OPTIONS, getTimeValue, updateDate, updateTime } from './utils'
-import './datePicker.scss'
-import 'react-dates/lib/css/_datepicker.css'
+import 'react-day-picker/style.css'
+import './DateTimePicker.scss'
+
+const isDateUpdateRange = (
+ isRange: boolean,
+ handler: DateTimePickerProps['onChange'],
+): handler is UpdateDateRangeType => isRange
+
+const getTodayDate = (): Date => {
+ const today = dayjs()
+ return today.toDate()
+}
const DateTimePicker = ({
- date: dateObject = new Date(),
+ date: dateObject = getTodayDate(),
+ dateRange = {
+ from: getTodayDate(),
+ to: getTodayDate(),
+ },
onChange,
timePickerProps = {} as SelectInstance,
disabled,
@@ -47,40 +59,170 @@ const DateTimePicker = ({
required,
hideTimeSelect = false,
readOnly = false,
- isTodayBlocked = false,
dataTestIdForTime = DATE_PICKER_IDS.TIME,
- dataTestidForDate = DATE_PICKER_IDS.DATE,
- openDirection = 'down',
error = '',
+ isRangePicker = false,
+ isTodayBlocked = false,
+ blockPreviousDates = true,
+ isOutsideRange,
+ rangeShortcutOptions,
}: DateTimePickerProps) => {
+ const calendarPopoverId = useRef(getUniqueId())
+
+ const { open, overlayProps, popoverProps, triggerProps, scrollableRef } = usePopover({
+ id: `date-time-picker-popover-${calendarPopoverId.current}`,
+ alignment: 'end',
+ variant: 'overlay',
+ })
+
+ const parsedPopoverProps = useMemo(
+ () => ({
+ ...popoverProps,
+ className: `${popoverProps.className} w-100 p-12 date-time-picker`,
+ style: {
+ ...popoverProps.style,
+ maxWidth: 'none',
+ },
+ }),
+ [popoverProps],
+ )
+
+ const parsedOverlayProps = useMemo(
+ () => ({
+ ...overlayProps,
+ initialFocus: false,
+ }),
+ [overlayProps],
+ )
+
const time = getTimeValue(dateObject)
const selectedTimeOption = DEFAULT_TIME_OPTIONS.find((option) => option.value === time) ?? DEFAULT_TIME_OPTIONS[0]
- const [isFocused, setFocused] = useState(false)
+ const handleTimeChange = (option: SelectPickerOptionType) => {
+ if (isDateUpdateRange(isRangePicker, onChange)) {
+ return
+ }
+ onChange(updateTime(dateObject, option.value).value)
+ }
- const { disableFocusTrap, resumeFocusTrap } = useFocusTrapControl()
+ const handleDateRangeChange = (range: DateRange) => {
+ if (isDateUpdateRange(isRangePicker, onChange)) {
+ const fromDate = range.from ? range.from : new Date()
+ const toDate = range.to ? range.to : undefined
- const handleFocusChange = ({ focused }) => {
- setFocused(focused)
+ onChange({
+ from: fromDate,
+ to: toDate,
+ })
+ }
}
- const handleDateChange = (event) => {
- onChange(updateDate(dateObject, event?.toDate()))
+
+ const handleRangeSelect: OnSelectHandler = (range) => {
+ handleDateRangeChange(range)
}
- const handleTimeChange = (option) => {
- onChange(updateTime(dateObject, option.value).value)
+ const getRangeUpdateHandler = (range: DateRange) => () => {
+ handleDateRangeChange(range)
}
- const today = moment()
- // Function to disable dates including today and all past dates
- const isDayBlocked = (day) => isTodayBlocked && !day.isAfter(today)
+ const handleSingleDateSelect: OnSelectHandler = (date) => {
+ if (!isDateUpdateRange(isRangePicker, onChange)) {
+ const updatedDate = date ? updateDate(dateObject, date) : null
+ onChange(updatedDate)
+ }
+ }
- useEffect(() => {
- if (isFocused) {
- disableFocusTrap()
- return
+ const getDisabledState = () => {
+ if (readOnly) {
+ return true
}
- resumeFocusTrap()
- }, [isFocused])
+
+ const today = getTodayDate()
+ today.setHours(0, 0, 0, 0)
+
+ const isOutsideRangeFn = isOutsideRange || (() => false)
+
+ if (isTodayBlocked) {
+ return (date: Date) => date <= today || isOutsideRangeFn(date)
+ }
+
+ if (blockPreviousDates) {
+ return (date: Date) => date < today || isOutsideRangeFn(date)
+ }
+
+ return isOutsideRangeFn
+ }
+
+ const renderDatePicker = () => {
+ if (isRangePicker) {
+ return (
+
+ {!!rangeShortcutOptions?.length && (
+
+ {rangeShortcutOptions.map(({ label: optionLabel, value, onClick }) => (
+
+ ))}
+
+ )}
+
+
+
+ )
+ }
+
+ return (
+
+ )
+ }
+
+ const renderInputLabel = () => {
+ if (isRangePicker) {
+ const fromDate = dateRange.from ? dayjs(dateRange.from).format(DATE_TIME_FORMATS.DD_MMM_YYYY) : ''
+ const toDate = dateRange.to ? dayjs(dateRange.to).format(DATE_TIME_FORMATS.DD_MMM_YYYY) : '...'
+
+ return `${fromDate} - ${toDate}`
+ }
+
+ return dayjs(dateObject).format(DATE_TIME_FORMATS.DD_MMM_YYYY)
+ }
+
+ const triggerElement = (
+ ,
+ }}
+ size={ComponentSizeType.large}
+ />
+ )
return (
@@ -90,28 +232,19 @@ const DateTimePicker = ({
)}
-
}
- hideKeyboardShortcutsPanel
- withFullScreenPortal={false}
- orientation="horizontal"
- readOnly={readOnly || false}
- customInputIcon={}
- inputIconPosition="after"
- displayFormat={DATE_TIME_FORMATS.DD_MMM_YYYY}
- data-testid={dataTestidForDate}
- isDayBlocked={isDayBlocked}
- disabled={disabled}
- appendToBody
- />
+
+
+ {renderDatePicker()}
+
+
+
{!hideTimeSelect && (
{
- const [showCalendar, setShowCalender] = useState(false)
- const onClickApplyTimeChange = () => {
- setShowCalender(false)
- handleApply()
- }
-
- const onClickPredefinedTimeRange = (startDate: Moment, endDate: Moment, endStr: string) => () => {
- handlePredefinedRange(startDate, endDate, endStr)
- setShowCalender(false)
- }
-
- const renderDatePresets = () => (
-
-
-
Pick time range
-
-
- From
- {
- handleDateInput('startDate', event.target.value)
- }}
- />
-
-
- To
- {
- handleDateInput('endDate', event.target.value)
- }}
- />
-
-
-
-
-
- {DayPickerRangeControllerPresets.map(({ text, startDate, endDate, endStr }) => {
- const isSelected =
- startDate.isSame(calendar.startDate, 'minute') &&
- startDate.isSame(calendar.startDate, 'hour') &&
- startDate.isSame(calendar.startDate, 'day') &&
- endDate.isSame(calendar.endDate, 'day')
- let buttonStyles = {
- ...styles.PresetDateRangePicker_button,
- }
- if (isSelected) {
- buttonStyles = {
- ...buttonStyles,
- ...styles.PresetDateRangePicker_button__selected,
- }
- }
- return (
-
- )
- })}
-
-
- )
-
- const toggleCalender = () => {
- setShowCalender(!showCalendar)
- }
-
- const hideCalender = () => setShowCalender(false)
-
- return (
- <>
-
- {showCalendar && (
- !isInclusivelyBeforeDay(day, moment())} // enable past dates
- renderCalendarDay={(props) => }
- onOutsideClick={hideCalender}
- initialVisibleMonth={() => moment().subtract(2, 'd')} //
- />
- )}
- >
- )
-}
diff --git a/src/Shared/Components/DatePicker/SingleDatePickerComponent.tsx b/src/Shared/Components/DatePicker/SingleDatePickerComponent.tsx
deleted file mode 100644
index e5ea513e1..000000000
--- a/src/Shared/Components/DatePicker/SingleDatePickerComponent.tsx
+++ /dev/null
@@ -1,80 +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 { useState } from 'react'
-import { SingleDatePicker } from 'react-dates'
-import CustomizableCalendarDay from 'react-dates/lib/components/CustomizableCalendarDay'
-import moment, { Moment } from 'moment'
-
-import 'react-dates/initialize'
-
-import CalenderIcon from '../../../Assets/Icon/ic-calender-blank.svg'
-import { customDayStyles } from './constants'
-import { SingleDatePickerProps } from './types'
-
-import 'react-dates/lib/css/_datepicker.css'
-
-const blockToday = (day: Moment): boolean => day.isSame(moment(), 'day')
-
-/**
- *
- * @param date Date value to be displayed
- * @param handleDatesChange Function to handle date change
- * @param readOnly Value to make date picker read only
- * @param isTodayBlocked Value to block today's date
- * @returns
- */
-
-const SingleDatePickerComponent = ({
- date,
- handleDatesChange,
- readOnly,
- isTodayBlocked,
- displayFormat,
- dataTestId,
-}: SingleDatePickerProps) => {
- const [focused, setFocused] = useState(false)
-
- const handleFocusChange = (props) => {
- setFocused(props.focused)
- }
-
- const renderCustomDay = (props) =>
-
- return (
- renderCustomDay(props)}
- hideKeyboardShortcutsPanel
- withFullScreenPortal={false}
- orientation="horizontal"
- readOnly={readOnly || false}
- isDayBlocked={isTodayBlocked ? blockToday : undefined}
- customInputIcon={}
- inputIconPosition="after"
- displayFormat={displayFormat}
- data-testid={dataTestId}
- />
- )
-}
-export default SingleDatePickerComponent
diff --git a/src/Shared/Components/DatePicker/constants.ts b/src/Shared/Components/DatePicker/constants.ts
deleted file mode 100644
index 08e27ce3f..000000000
--- a/src/Shared/Components/DatePicker/constants.ts
+++ /dev/null
@@ -1,220 +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 moment from 'moment'
-
-export const selectedStyles = {
- background: 'var(--B100)',
- color: 'var(--B500)',
-
- hover: {
- background: 'var(--B500)',
- color: 'var(--N0)',
- },
-}
-
-export const selectedSpanStyles = {
- background: 'var(--B100)',
- color: 'var(--B500)',
- hover: {
- background: 'var(--B500)',
- color: 'var(--N0)',
- },
-}
-
-export const hoveredSpanStyles = {
- background: 'var(--B100)',
- color: 'var(--B500)',
-}
-
-export const customDayStyles = {
- selectedStartStyles: selectedStyles,
- selectedEndStyles: selectedStyles,
- hoveredSpanStyles,
- selectedSpanStyles,
- selectedStyles,
- border: 'none',
-}
-
-export const MONTHLY_DATES_CONFIG = {
- 'Day 1': '1',
- 'Day 2': '2',
- 'Day 3': '3',
- 'Day 4': '4',
- 'Day 5': '5',
- 'Day 6': '6',
- 'Day 7': '7',
- 'Day 8': '8',
- 'Day 9': '9',
- 'Day 10': '10',
- 'Day 11': '11',
- 'Day 12': '12',
- 'Day 13': '13',
- 'Day 14': '14',
- 'Day 15': '15',
- 'Day 16': '16',
- 'Day 17': '17',
- 'Day 18': '18',
- 'Day 19': '19',
- 'Day 20': '20',
- 'Day 21': '21',
- 'Day 22': '22',
- 'Day 23': '23',
- 'Day 24': '24',
- 'Day 25': '25',
- 'Day 26': '26',
- 'Day 27': '27',
- 'Day 28': '28',
- 'Third Last Day': '-3',
- 'Second Last Day': '-2',
- 'Last Day': '-1',
-}
-
-export const TIME_OPTIONS_CONFIG = {
- '12:00 AM': '00:00:00',
- '12:30 AM': '00:30:00',
- '01:00 AM': '01:00:00',
- '01:30 AM': '01:30:00',
- '02:00 AM': '02:00:00',
- '02:30 AM': '02:30:00',
- '03:00 AM': '03:00:00',
- '03:30 AM': '03:30:00',
- '04:00 AM': '04:00:00',
- '04:30 AM': '04:30:00',
- '05:00 AM': '05:00:00',
- '05:30 AM': '05:30:00',
- '06:00 AM': '06:00:00',
- '06:30 AM': '06:30:00',
- '07:00 AM': '07:00:00',
- '07:30 AM': '07:30:00',
- '08:00 AM': '08:00:00',
- '08:30 AM': '08:30:00',
- '09:00 AM': '09:00:00',
- '09:30 AM': '09:30:00',
- '10:00 AM': '10:00:00',
- '10:30 AM': '10:30:00',
- '11:00 AM': '11:00:00',
- '11:30 AM': '11:30:00',
- '12:00 PM': '12:00:00',
- '12:30 PM': '12:30:00',
- '01:00 PM': '13:00:00',
- '01:30 PM': '13:30:00',
- '02:00 PM': '14:00:00',
- '02:30 PM': '14:30:00',
- '03:00 PM': '15:00:00',
- '03:30 PM': '15:30:00',
- '04:00 PM': '16:00:00',
- '04:30 PM': '16:30:00',
- '05:00 PM': '17:00:00',
- '05:30 PM': '17:30:00',
- '06:00 PM': '18:00:00',
- '06:30 PM': '18:30:00',
- '07:00 PM': '19:00:00',
- '07:30 PM': '19:30:00',
- '08:00 PM': '20:00:00',
- '08:30 PM': '20:30:00',
- '09:00 PM': '21:00:00',
- '09:30 PM': '21:30:00',
- '10:00 PM': '22:00:00',
- '10:30 PM': '22:30:00',
- '11:00 PM': '23:00:00',
- '11:30 PM': '23:30:00',
-}
-
-export const DATE_PICKER_PLACEHOLDER = {
- DATE: 'Select date',
- TIME: 'Select time',
- MONTH: 'Select month',
- DEFAULT_TIME: '12:00 AM',
- DEFAULT_MONTHLY_DATE: 'Day 1',
-}
-
-export const DATE_PICKER_IDS = {
- DATE: 'date_picker',
- MONTH: 'month_picker',
- TIME: 'time_picker',
-}
-
-export const styles = {
- PresetDateRangePicker_panel: {
- padding: '0px',
- width: '200px',
- height: '100%',
- },
- PresetDateRangePicker_button: {
- width: '188px',
- background: 'var(--transparent)',
- border: 'none',
- color: 'var(--N900)',
- padding: '8px',
- font: 'inherit',
- fontWeight: 500,
- lineHeight: 'normal',
- overflow: 'visible',
- cursor: 'pointer',
- ':active': {
- outline: 0,
- },
- },
- DayPicker__horizontal: {
- borderRadius: '4px',
- },
- PresetDateRangePicker_button__selected: {
- color: 'var(--B500)',
- fontWeight: 600,
- background: 'var(--B100)',
- outline: 'none',
- },
-}
-
-export const DayPickerCalendarInfoHorizontal = {
- width: '532px',
- boxShadow: 'none',
-}
-
-export const DayPickerRangeControllerPresets = [
- { text: 'Last 5 minutes', endDate: moment(), startDate: moment().subtract(5, 'minutes'), endStr: 'now-5m' },
- { text: 'Last 30 minutes', endDate: moment(), startDate: moment().subtract(30, 'minutes'), endStr: 'now-30m' },
- { text: 'Last 1 hour', endDate: moment(), startDate: moment().subtract(1, 'hours'), endStr: 'now-1h' },
- { text: 'Last 24 hours', endDate: moment(), startDate: moment().subtract(24, 'hours'), endStr: 'now-24h' },
- { text: 'Last 7 days', endDate: moment(), startDate: moment().subtract(7, 'days'), endStr: 'now-7d' },
- { text: 'Last 1 month', endDate: moment(), startDate: moment().subtract(1, 'months'), endStr: 'now-1M' },
- { text: 'Last 6 months', endDate: moment(), startDate: moment().subtract(6, 'months'), endStr: 'now-6M' },
-]
-
-/**
- * Returns a string representing the range of dates
- * given by the start and end dates. If the end date
- * is 'now' and the start date includes 'now',
- * it will return the corresponding range from the
- * DayPickerRangeControllerPresets array.
- * @param startDateStr - the start date string
- * @param endDateStr - the end date string
- * @returns - a string representing the range of dates
- */
-
-export function getCalendarValue(startDateStr: string, endDateStr: string): string {
- let str: string = `${startDateStr} - ${endDateStr}`
- if (endDateStr === 'now' && startDateStr.includes('now')) {
- const range = DayPickerRangeControllerPresets.find((d) => d.endStr === startDateStr)
- if (range) {
- str = range.text
- } else {
- str = `${startDateStr} - ${endDateStr}`
- }
- }
- return str
-}
diff --git a/src/Shared/Components/DatePicker/constants.tsx b/src/Shared/Components/DatePicker/constants.tsx
new file mode 100644
index 000000000..dd2277789
--- /dev/null
+++ b/src/Shared/Components/DatePicker/constants.tsx
@@ -0,0 +1,154 @@
+/*
+ * 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 { ButtonHTMLAttributes } from 'react'
+import { CustomComponents } from 'react-day-picker'
+
+import { noop } from '@Common/Helper'
+import { ComponentSizeType } from '@Shared/constants'
+
+import { Button, ButtonStyleType, ButtonVariantType } from '../Button'
+import { Icon } from '../Icon'
+
+export const MONTHLY_DATES_CONFIG = {
+ 'Day 1': '1',
+ 'Day 2': '2',
+ 'Day 3': '3',
+ 'Day 4': '4',
+ 'Day 5': '5',
+ 'Day 6': '6',
+ 'Day 7': '7',
+ 'Day 8': '8',
+ 'Day 9': '9',
+ 'Day 10': '10',
+ 'Day 11': '11',
+ 'Day 12': '12',
+ 'Day 13': '13',
+ 'Day 14': '14',
+ 'Day 15': '15',
+ 'Day 16': '16',
+ 'Day 17': '17',
+ 'Day 18': '18',
+ 'Day 19': '19',
+ 'Day 20': '20',
+ 'Day 21': '21',
+ 'Day 22': '22',
+ 'Day 23': '23',
+ 'Day 24': '24',
+ 'Day 25': '25',
+ 'Day 26': '26',
+ 'Day 27': '27',
+ 'Day 28': '28',
+ 'Third Last Day': '-3',
+ 'Second Last Day': '-2',
+ 'Last Day': '-1',
+}
+
+export const TIME_OPTIONS_CONFIG = {
+ '12:00 AM': '00:00:00',
+ '12:30 AM': '00:30:00',
+ '01:00 AM': '01:00:00',
+ '01:30 AM': '01:30:00',
+ '02:00 AM': '02:00:00',
+ '02:30 AM': '02:30:00',
+ '03:00 AM': '03:00:00',
+ '03:30 AM': '03:30:00',
+ '04:00 AM': '04:00:00',
+ '04:30 AM': '04:30:00',
+ '05:00 AM': '05:00:00',
+ '05:30 AM': '05:30:00',
+ '06:00 AM': '06:00:00',
+ '06:30 AM': '06:30:00',
+ '07:00 AM': '07:00:00',
+ '07:30 AM': '07:30:00',
+ '08:00 AM': '08:00:00',
+ '08:30 AM': '08:30:00',
+ '09:00 AM': '09:00:00',
+ '09:30 AM': '09:30:00',
+ '10:00 AM': '10:00:00',
+ '10:30 AM': '10:30:00',
+ '11:00 AM': '11:00:00',
+ '11:30 AM': '11:30:00',
+ '12:00 PM': '12:00:00',
+ '12:30 PM': '12:30:00',
+ '01:00 PM': '13:00:00',
+ '01:30 PM': '13:30:00',
+ '02:00 PM': '14:00:00',
+ '02:30 PM': '14:30:00',
+ '03:00 PM': '15:00:00',
+ '03:30 PM': '15:30:00',
+ '04:00 PM': '16:00:00',
+ '04:30 PM': '16:30:00',
+ '05:00 PM': '17:00:00',
+ '05:30 PM': '17:30:00',
+ '06:00 PM': '18:00:00',
+ '06:30 PM': '18:30:00',
+ '07:00 PM': '19:00:00',
+ '07:30 PM': '19:30:00',
+ '08:00 PM': '20:00:00',
+ '08:30 PM': '20:30:00',
+ '09:00 PM': '21:00:00',
+ '09:30 PM': '21:30:00',
+ '10:00 PM': '22:00:00',
+ '10:30 PM': '22:30:00',
+ '11:00 PM': '23:00:00',
+ '11:30 PM': '23:30:00',
+}
+
+export const DATE_PICKER_PLACEHOLDER = {
+ DATE: 'Select date',
+ TIME: 'Select time',
+ MONTH: 'Select month',
+ DEFAULT_TIME: '12:00 AM',
+ DEFAULT_MONTHLY_DATE: 'Day 1',
+}
+
+export const DATE_PICKER_IDS = {
+ DATE: 'date_picker',
+ MONTH: 'month_picker',
+ TIME: 'time_picker',
+}
+
+export const DATE_PICKER_CUSTOM_COMPONENTS: Partial = {
+ NextMonthButton: ({ onClick, className }: ButtonHTMLAttributes) => (
+
+ }
+ size={ComponentSizeType.small}
+ variant={ButtonVariantType.secondary}
+ style={ButtonStyleType.neutral}
+ ariaLabel="Next Month"
+ showAriaLabelInTippy={false}
+ onClick={onClick || noop}
+ />
+
+ ),
+ PreviousMonthButton: ({ onClick, className }: ButtonHTMLAttributes) => (
+
+ }
+ size={ComponentSizeType.small}
+ variant={ButtonVariantType.secondary}
+ style={ButtonStyleType.neutral}
+ ariaLabel="Previous Month"
+ showAriaLabelInTippy={false}
+ onClick={onClick || noop}
+ />
+
+ ),
+}
diff --git a/src/Shared/Components/DatePicker/datePicker.scss b/src/Shared/Components/DatePicker/datePicker.scss
deleted file mode 100644
index 49f3b1949..000000000
--- a/src/Shared/Components/DatePicker/datePicker.scss
+++ /dev/null
@@ -1,103 +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.
- */
-
-.SingleDatePicker_picker {
- z-index: calc(var(--modal-index) + 1) !important;
-}
-
-.date-time-picker {
- .SingleDatePicker {
- width: 100%;
- }
-
- .SingleDatePickerInput {
- display: flex !important;
- width: 100%;
- background-color: var(--bg-secondary);
- margin-right: 0 !important;
- padding: 0 !important;
- height: 36px !important;
- overflow: hidden;
- border: 1px solid var(--N200);
- border-radius: 4px;
-
- &:hover {
- border: 1px solid var(--N400);
- }
-
- &:focus,
- &:focus-visible,
- &:focus-within {
- border: 1px solid var(--B500);
- }
-
- &__disabled {
- background-color: var(--N100);
- border-color: var(--N200);
- }
- }
-
- .SingleDatePickerInput_calendarIcon {
- padding: 0 !important;
- margin: 0 !important;
- padding-right: 8px !important;
- background-color: transparent !important;
- height: 34px !important;
- display: flex;
- align-items: center;
-
- &:focus-visible {
- outline: none;
- }
- }
-
- .SingleDatePickerInput__withBorder {
- .DateInput {
- line-height: 20px !important;
- height: 34px;
- background-color: transparent;
- width: 100%;
-
- &:focus-visible {
- outline: none;
- }
- }
-
- .DateInput_input {
- font-size: 13px;
- border-bottom: none;
- cursor: pointer;
- background-color: transparent;
- color: var(--N900);
-
- &__focused + .DateInput_fang {
- display: none;
- }
-
- &__disabled {
- cursor: not-allowed;
- }
- }
- }
-
- .DateInput_input {
- padding: 6px 8px !important;
-
- &__disabled {
- font-style: normal;
- }
- }
-}
diff --git a/src/Shared/Components/DatePicker/index.ts b/src/Shared/Components/DatePicker/index.ts
index f0cc559fa..88b5a9d0f 100644
--- a/src/Shared/Components/DatePicker/index.ts
+++ b/src/Shared/Components/DatePicker/index.ts
@@ -16,9 +16,7 @@
export * from './constants'
export { default as DateTimePicker } from './DateTimePicker'
-export { DatePickerRangeController } from './DayPickerRangeController'
export * from './MonthlySelect'
-export { default as SingleDatePickerComponent } from './SingleDatePickerComponent'
export * from './TimeSelect'
export * from './types'
export * from './utils'
diff --git a/src/Shared/Components/DatePicker/types.ts b/src/Shared/Components/DatePicker/types.ts
index d8844e7d4..ba6183046 100644
--- a/src/Shared/Components/DatePicker/types.ts
+++ b/src/Shared/Components/DatePicker/types.ts
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-import { SingleDatePickerShape } from 'react-dates'
import { SelectInstance } from 'react-select'
import { Moment } from 'moment'
@@ -103,13 +102,18 @@ export interface TimeSelectProps {
selectedTimeOption: DateSelectPickerType
}
-export interface DateTimePickerProps
- extends Pick,
- Pick {
- /**
- * Props for the date picker
- */
- datePickerProps?: any
+interface DateRangeType {
+ from: Date
+ to?: Date
+}
+
+export type UpdateDateRangeType = (dateRange: DateRangeType) => void
+export type UpdateSingleDateType = (date: Date) => void
+
+export type DateTimePickerProps = Pick<
+ TimeSelectProps,
+ 'timePickerProps' | 'error' | 'disabled' | 'dataTestIdForTime'
+> & {
/**
* Id for the component
*/
@@ -122,10 +126,6 @@ export interface DateTimePickerProps
* If true, the field is required and asterisk is shown
*/
required?: boolean
- /**
- * To hide time selector
- */
- hideTimeSelect?: boolean
/**
* To make the field read only
*/
@@ -134,25 +134,37 @@ export interface DateTimePickerProps
* To block today's date
*/
isTodayBlocked?: boolean
- /**
- * Data test id for date picker
- */
- dataTestidForDate?: string
- /**
- * Function to handle date change
- */
- onChange: (date: Date) => void
-}
-
-export interface DatePickerRangeControllerProps {
- calendar
- calendarInputs
- focusedInput
- handleFocusChange
- handleDatesChange
- handleCalendarInputs?
- calendarValue: string
- handlePredefinedRange: (start: Moment, end: Moment, endStr: string) => void
- handleDateInput: (key: 'startDate' | 'endDate', value: string) => void
- handleApply: (...args) => void
-}
+ blockPreviousDates?: boolean
+ isOutsideRange?: (day: Date) => boolean
+} & (
+ | {
+ isRangePicker: true
+ hideTimeSelect: true
+ dateRange: DateRangeType
+ onChange: UpdateDateRangeType
+ rangeShortcutOptions?: (
+ | {
+ label: string
+ onClick: () => void
+ value?: never
+ }
+ | {
+ label: string
+ onClick?: never
+ value: DateRangeType
+ }
+ )[]
+ date?: never
+ }
+ | {
+ isRangePicker?: false
+ date: TimeSelectProps['date']
+ onChange: UpdateSingleDateType
+ /**
+ * To hide time selector
+ */
+ hideTimeSelect?: boolean
+ dateRange?: never
+ rangeShortcutOptions?: never
+ }
+ )
diff --git a/src/Shared/Components/DatePicker/utils.tsx b/src/Shared/Components/DatePicker/utils.tsx
index 5ef41b568..33166f8bb 100644
--- a/src/Shared/Components/DatePicker/utils.tsx
+++ b/src/Shared/Components/DatePicker/utils.tsx
@@ -29,8 +29,7 @@ export const MONTHLY_DATE_OPTIONS = Object.entries(MONTHLY_DATES_CONFIG).map(([l
* Return the options for the time in label and value format
* @type {SelectPickerOptionType[]}
*/
-// eslint-disable-next-line import/prefer-default-export
-export const DEFAULT_TIME_OPTIONS: SelectPickerOptionType[] = Object.entries(TIME_OPTIONS_CONFIG).map(
+export const DEFAULT_TIME_OPTIONS: SelectPickerOptionType[] = Object.entries(TIME_OPTIONS_CONFIG).map(
([label, value]) => ({
label,
value,
diff --git a/src/Shared/Components/GenericModal/GenericModal.component.tsx b/src/Shared/Components/GenericModal/GenericModal.component.tsx
index 3be60dab8..2f52309f5 100644
--- a/src/Shared/Components/GenericModal/GenericModal.component.tsx
+++ b/src/Shared/Components/GenericModal/GenericModal.component.tsx
@@ -21,7 +21,7 @@ import { noop, stopPropagation } from '@Common/Helper'
import { Backdrop, Button, ButtonStyleType, ButtonVariantType, Icon } from '@Shared/Components'
import { ComponentSizeType } from '@Shared/constants'
-import { MODAL_WIDTH_TO_CLASS_NAME_MAP } from './constants'
+import { BORDER_VARIANT_TO_CLASS_NAME_MAP, MODAL_WIDTH_TO_CLASS_NAME_MAP } from './constants'
import { GenericModalProvider, useGenericModalContext } from './GenericModal.context'
import { GenericModalFooterProps, GenericModalHeaderProps, GenericModalProps } from './types'
@@ -87,13 +87,20 @@ const GenericModal = ({
onEscape = noop,
closeOnBackdropClick = false,
children,
+ avoidFocusTrap = false,
+ borderVariant = 'secondary',
+ alignCenter = false,
}: PropsWithChildren) => (
{open && (
-
+
= {
450: 'w-450',
@@ -22,3 +22,9 @@ export const MODAL_WIDTH_TO_CLASS_NAME_MAP: Record = {
+ secondary: 'border__secondary',
+ none: '',
+ 'secondary-translucent': 'border__secondary-translucent',
+}
diff --git a/src/Shared/Components/GenericModal/types.ts b/src/Shared/Components/GenericModal/types.ts
index e5993b388..a71884e9e 100644
--- a/src/Shared/Components/GenericModal/types.ts
+++ b/src/Shared/Components/GenericModal/types.ts
@@ -17,7 +17,9 @@
import { BackdropProps } from '../Backdrop'
import { ButtonProps } from '../Button'
-export interface GenericModalProps extends Partial> {
+export type BorderVariantType = 'secondary' | 'none' | 'secondary-translucent'
+
+export interface GenericModalProps extends Partial> {
/** Unique identifier for the modal */
name: string
/** Controls whether the modal is visible or hidden */
@@ -35,6 +37,8 @@ export interface GenericModalProps extends Partial {}
diff --git a/src/Shared/Components/Icon/Icon.tsx b/src/Shared/Components/Icon/Icon.tsx
index f9517d50e..17bf08959 100644
--- a/src/Shared/Components/Icon/Icon.tsx
+++ b/src/Shared/Components/Icon/Icon.tsx
@@ -96,6 +96,7 @@ import { ReactComponent as ICCostVisibility } from '@IconsV2/ic-cost-visibility.
import { ReactComponent as ICCpu } from '@IconsV2/ic-cpu.svg'
import { ReactComponent as ICCrown } from '@IconsV2/ic-crown.svg'
import { ReactComponent as ICCube } from '@IconsV2/ic-cube.svg'
+import { ReactComponent as ICCurvedArrow } from '@IconsV2/ic-curved-arrow.svg'
import { ReactComponent as ICDatabaseBackup } from '@IconsV2/ic-database-backup.svg'
import { ReactComponent as ICDelete } from '@IconsV2/ic-delete.svg'
import { ReactComponent as ICDeleteDots } from '@IconsV2/ic-delete-dots.svg'
@@ -217,6 +218,7 @@ import { ReactComponent as ICMobile } from '@IconsV2/ic-mobile.svg'
import { ReactComponent as ICMonitoring } from '@IconsV2/ic-monitoring.svg'
import { ReactComponent as ICMoreVertical } from '@IconsV2/ic-more-vertical.svg'
import { ReactComponent as ICNamespace } from '@IconsV2/ic-namespace.svg'
+import { ReactComponent as ICNavCursor } from '@IconsV2/ic-nav-cursor.svg'
import { ReactComponent as ICNew } from '@IconsV2/ic-new.svg'
import { ReactComponent as ICNodeScript } from '@IconsV2/ic-node-script.svg'
import { ReactComponent as ICOidc } from '@IconsV2/ic-oidc.svg'
@@ -412,6 +414,7 @@ export const iconMap = {
'ic-cpu': ICCpu,
'ic-crown': ICCrown,
'ic-cube': ICCube,
+ 'ic-curved-arrow': ICCurvedArrow,
'ic-database-backup': ICDatabaseBackup,
'ic-delete-dots': ICDeleteDots,
'ic-delete-lightning': ICDeleteLightning,
@@ -533,6 +536,7 @@ export const iconMap = {
'ic-monitoring': ICMonitoring,
'ic-more-vertical': ICMoreVertical,
'ic-namespace': ICNamespace,
+ 'ic-nav-cursor': ICNavCursor,
'ic-new': ICNew,
'ic-node-script': ICNodeScript,
'ic-oidc': ICOidc,
diff --git a/src/Shared/Components/Illustration/Illustration.tsx b/src/Shared/Components/Illustration/Illustration.tsx
index 63649ea9e..409da2299 100644
--- a/src/Shared/Components/Illustration/Illustration.tsx
+++ b/src/Shared/Components/Illustration/Illustration.tsx
@@ -1,15 +1,21 @@
// NOTE: This file is auto-generated. Do not edit directly. Run the script `npm run generate-illustration` to update.
+import CmdBarVisual from '@Illustrations/cmd-bar-visual.webp'
import CreateBackupSchedule from '@Illustrations/create-backup-schedule.webp'
import CreateBackupSnapshot from '@Illustrations/create-backup-snapshot.webp'
+import { ReactComponent as ImgCelebration } from '@Illustrations/img-celebration.svg'
import ImgCode from '@Illustrations/img-code.webp'
import ImgDevtronFreemium from '@Illustrations/img-devtron-freemium.webp'
import { ReactComponent as ImgFolderEmpty } from '@Illustrations/img-folder-empty.svg'
+import { ReactComponent as ImgInstallFreemiumSaas } from '@Illustrations/img-install-freemium-saas.svg'
+import { ReactComponent as ImgInstallViaAwsMarketplace } from '@Illustrations/img-install-via-aws-marketplace.svg'
+import { ReactComponent as ImgInstallingDevtron } from '@Illustrations/img-installing-devtron.svg'
import ImgManOnRocket from '@Illustrations/img-man-on-rocket.webp'
import { ReactComponent as ImgMechanicalOperation } from '@Illustrations/img-mechanical-operation.svg'
import { ReactComponent as ImgNoBackupLocation } from '@Illustrations/img-no-backup-location.svg'
import { ReactComponent as ImgNoRestores } from '@Illustrations/img-no-restores.svg'
import ImgNoResult from '@Illustrations/img-no-result.webp'
+import { ReactComponent as ImgPageNotFound } from '@Illustrations/img-page-not-found.svg'
import NoClusterCostEnabled from '@Illustrations/no-cluster-cost-enabled.webp'
// eslint-disable-next-line no-restricted-imports
@@ -17,10 +23,16 @@ import { IllustrationBase } from './IllustrationBase'
import { IllustrationBaseProps } from './types'
export const illustrationMap = {
+ 'img-celebration': ImgCelebration,
'img-folder-empty': ImgFolderEmpty,
+ 'img-install-freemium-saas': ImgInstallFreemiumSaas,
+ 'img-install-via-aws-marketplace': ImgInstallViaAwsMarketplace,
+ 'img-installing-devtron': ImgInstallingDevtron,
'img-mechanical-operation': ImgMechanicalOperation,
'img-no-backup-location': ImgNoBackupLocation,
'img-no-restores': ImgNoRestores,
+ 'img-page-not-found': ImgPageNotFound,
+ 'cmd-bar-visual': CmdBarVisual,
'create-backup-schedule': CreateBackupSchedule,
'create-backup-snapshot': CreateBackupSnapshot,
'img-code': ImgCode,
diff --git a/src/Shared/Components/License/DevtronLicenseCard.tsx b/src/Shared/Components/License/DevtronLicenseCard.tsx
index f2904251d..754cd385b 100644
--- a/src/Shared/Components/License/DevtronLicenseCard.tsx
+++ b/src/Shared/Components/License/DevtronLicenseCard.tsx
@@ -27,7 +27,7 @@ import { LicensingErrorCodes } from '@Shared/types'
import { Button, ButtonComponentType, ButtonVariantType } from '../Button'
import { Icon } from '../Icon'
-import { DevtronLicenseCardProps, LicenseStatus } from './types'
+import { DevtronLicenseCardProps, LicenseCardSubTextProps, LicenseStatus } from './types'
import { getLicenseColorsAccordingToStatus } from './utils'
import './licenseCard.scss'
@@ -49,10 +49,13 @@ const LicenseCardSubText = ({
isFreemium,
licenseStatus,
licenseStatusError,
-}: Pick) => {
- if (isFreemium) {
- const freemiumLimitReached = licenseStatusError?.code === LicensingErrorCodes.ClusterLimitExceeded
+ isFreeForever,
+}: LicenseCardSubTextProps) => {
+ const freemiumLimitReached = isFreemium && licenseStatusError?.code === LicensingErrorCodes.ClusterLimitExceeded
+ const showFreemiumMessage =
+ isFreeForever || freemiumLimitReached || (isFreemium && licenseStatus === LicenseStatus.ACTIVE)
+ if (showFreemiumMessage) {
return (
@@ -130,10 +133,17 @@ export const DevtronLicenseCard = ({
appTheme,
handleCopySuccess,
licenseStatusError,
+ isSaasInstance,
}: DevtronLicenseCardProps) => {
- const { bgColor, textColor } = getLicenseColorsAccordingToStatus({ isFreemium, licenseStatus, licenseStatusError })
- const remainingTime = getTTLInHumanReadableFormat(ttl)
- const remainingTimeString = ttl < 0 ? `Expired ${remainingTime} ago` : `${remainingTime} remaining`
+ const isFreeForever = isFreemium && !isSaasInstance
+
+ const { bgColor, textColor } = getLicenseColorsAccordingToStatus({
+ isFreemium,
+ licenseStatus,
+ licenseStatusError,
+ isSaasInstance,
+ })
+
const isThemeDark = appTheme === AppThemeType.dark
const cardRef = useRef
(null)
@@ -178,6 +188,15 @@ export const DevtronLicenseCard = ({
? useMotionTemplate`linear-gradient(55deg, transparent, rgba(122, 127, 131, ${sheenOpacity}) ${sheenPosition}%, transparent)`
: useMotionTemplate`linear-gradient(55deg, transparent, rgba(255, 255, 255, ${sheenOpacity}) ${sheenPosition}%, transparent)`
+ const getRemainingTimeString = () => {
+ if (isFreeForever) {
+ return null
+ }
+
+ const remainingTime = getTTLInHumanReadableFormat(ttl)
+ return ttl < 0 ? `Expired ${remainingTime} ago` : `${remainingTime} remaining`
+ }
+
return (
@@ -217,12 +236,12 @@ export const DevtronLicenseCard = ({
- {isFreemium ? 'VALID FOREVER' : expiryDate}
+ {isFreeForever ? 'VALID FOREVER' : expiryDate}
- {!isFreemium && (
+ {!isFreeForever && (
<>
ยท
- {remainingTimeString}
+ {getRemainingTimeString()}
>
)}
@@ -239,6 +258,7 @@ export const DevtronLicenseCard = ({
isFreemium={isFreemium}
licenseStatusError={licenseStatusError}
licenseStatus={licenseStatus}
+ isFreeForever={isFreeForever}
/>
)
diff --git a/src/Shared/Components/License/index.tsx b/src/Shared/Components/License/index.tsx
index 75fa48db5..52c765f60 100644
--- a/src/Shared/Components/License/index.tsx
+++ b/src/Shared/Components/License/index.tsx
@@ -17,5 +17,6 @@
export { default as ActivateLicenseDialog } from './ActivateLicenseDialog'
export { default as DevtronLicenseCard } from './DevtronLicenseCard'
export { ICDevtronWithBorder, default as InstallationFingerprintInfo } from './License.components'
+export { activateLicense } from './services'
export * from './types'
export { parseDevtronLicenseData, parseDevtronLicenseDTOIntoLicenseCardData } from './utils'
diff --git a/src/Shared/Components/License/types.ts b/src/Shared/Components/License/types.ts
index 38b2be000..4c2d639ae 100644
--- a/src/Shared/Components/License/types.ts
+++ b/src/Shared/Components/License/types.ts
@@ -32,6 +32,7 @@ export type DevtronLicenseCardProps = {
isFreemium: boolean
appTheme: AppThemeType
licenseStatusError: LicenseErrorStruct
+ isSaasInstance: boolean
} & (
| {
licenseKey: string
@@ -45,6 +46,11 @@ export type DevtronLicenseCardProps = {
}
)
+export interface LicenseCardSubTextProps
+ extends Pick {
+ isFreeForever: boolean
+}
+
export type DevtronLicenseInfo = Omit &
Pick
diff --git a/src/Shared/Components/License/utils.tsx b/src/Shared/Components/License/utils.tsx
index 540d14613..423eb9648 100644
--- a/src/Shared/Components/License/utils.tsx
+++ b/src/Shared/Components/License/utils.tsx
@@ -27,16 +27,22 @@ export const getLicenseColorsAccordingToStatus = ({
isFreemium,
licenseStatus,
licenseStatusError,
-}: Pick): {
+ isSaasInstance,
+}: Pick): {
bgColor: string
textColor: string
} => {
if (isFreemium) {
const freemiumLimitReached = licenseStatusError?.code === LicensingErrorCodes.ClusterLimitExceeded
- return freemiumLimitReached
- ? { bgColor: 'var(--R100)', textColor: 'var(--R500)' }
- : { bgColor: 'var(--G100)', textColor: 'var(--G500)' }
+ if (freemiumLimitReached) {
+ return { bgColor: 'var(--R100)', textColor: 'var(--R500)' }
+ }
+
+ if (!isSaasInstance) {
+ return { bgColor: 'var(--G100)', textColor: 'var(--G500)' }
+ }
}
+
switch (licenseStatus) {
case LicenseStatus.ACTIVE:
return { bgColor: 'var(--G100)', textColor: 'var(--G500)' }
@@ -68,16 +74,24 @@ export const parseDevtronLicenseDTOIntoLicenseCardData = => {
const {
isTrial,
- expiry,
- ttl,
+ expiry: onPremExpiry,
+ ttl: onPremTTL,
reminderThreshold,
organisationMetadata,
license,
claimedByUserDetails,
isFreemium,
licenseStatusError,
+ isSaasInstance,
+ timeElapsedSinceCreation,
+ creationTime,
} = licenseDTO || {}
+ // In case of Saas expiry date is 30 days from creation time
+ const expiry = isSaasInstance && creationTime ? moment(creationTime).add(30, 'days').toISOString() : onPremExpiry
+ // For TTL will use timeElapsedSinceCreation to calculate remaining time for Saas license with 30 days validity, since browser time may differ from server time
+ const ttl = isSaasInstance && timeElapsedSinceCreation ? 30 * 24 * 60 * 60 - timeElapsedSinceCreation : onPremTTL
+
return {
enterpriseName: organisationMetadata?.name || 'Devtron Enterprise',
expiryDate: expiry ? moment(expiry).format(DATE_TIME_FORMATS['DD/MM/YYYY']) : '',
@@ -86,6 +100,7 @@ export const parseDevtronLicenseDTOIntoLicenseCardData = (
diff --git a/src/Shared/Helpers.tsx b/src/Shared/Helpers.tsx
index 902bd8d9a..6f5a16c46 100644
--- a/src/Shared/Helpers.tsx
+++ b/src/Shared/Helpers.tsx
@@ -54,7 +54,7 @@ import {
ZERO_TIME_STRING,
} from '../Common'
import { getAggregator, GVKType } from '../Pages'
-import { AggregatedNodes, PodMetadatum } from './Components'
+import { AggregatedNodes } from './Components'
import { CUBIC_BEZIER_CURVE, UNSAVED_CHANGES_PROMPT_MESSAGE } from './constants'
import {
AggregationKeys,
@@ -65,6 +65,7 @@ import {
IntersectionOptions,
Node,
Nodes,
+ PodMetaData,
TargetPlatformItemDTO,
TargetPlatformsDTO,
WebhookEventNameType,
@@ -298,7 +299,7 @@ export const renderValidInputButtonTippy = (children: ReactElement) => (
)
-export function aggregateNodes(nodes: any[], podMetadata: PodMetadatum[]): AggregatedNodes {
+export function aggregateNodes(nodes: any[], podMetadata: PodMetaData[]): AggregatedNodes {
const podMetadataMap = mapByKey(podMetadata, 'name')
// group nodes
const nodesGroup = nodes.reduce((agg, curr) => {
diff --git a/src/Shared/constants.tsx b/src/Shared/constants.tsx
index e28cf97cd..3052efdd0 100644
--- a/src/Shared/constants.tsx
+++ b/src/Shared/constants.tsx
@@ -592,6 +592,7 @@ export const CUBIC_BEZIER_CURVE: [number, number, number, number] = [0.33, 1, 0.
// Use this class on an element to allow clicking on it outside focus trap
export const ALLOW_ACTION_OUTSIDE_FOCUS_TRAP = 'allow-action-outside-focus-trap'
+export const LICENSE_KEY_QUERY_PARAM = 'licenseKey'
export const REMOTE_CONNECTION_TYPE_LABEL_MAP: Record
= {
[RemoteConnectionType.Direct]: 'Direct Connection',
diff --git a/src/Shared/types.ts b/src/Shared/types.ts
index 76a6859f3..907a59b89 100644
--- a/src/Shared/types.ts
+++ b/src/Shared/types.ts
@@ -175,13 +175,24 @@ export interface iNode extends Node {
status: string
pNode?: iNode
}
+
+export interface HelmReleaseStatus {
+ status: string
+ message: string
+ description: string
+}
+
export interface ResourceTree {
- conditions: any
+ nodes: Node[]
newGenerationReplicaSet: string
- nodes: Array
- podMetadata: Array
status: string
+ podMetadata: PodMetaData[]
+ conditions?: any
+ releaseStatus?: HelmReleaseStatus
resourcesSyncResult?: Record
+ hasDrift?: boolean
+ // lastSnapshotTime and wfrId are only available for isolated
+ lastSnapshotTime?: string
wfrId?: number
}
@@ -193,12 +204,6 @@ export enum AppType {
EXTERNAL_FLUX_APP = 'external_flux_app',
}
-export interface HelmReleaseStatus {
- status: string
- message: string
- description: string
-}
-
interface MaterialInfo {
author: string
branch: string
@@ -222,7 +227,7 @@ export interface AppDetails {
appStoreChartName?: string
appStoreInstalledAppVersionId?: number
ciArtifactId?: number
- deprecated?: false
+ deprecated?: boolean
environmentId?: number
environmentName: string
installedAppId?: number
@@ -258,6 +263,12 @@ export interface AppDetails {
FluxAppStatusDetail?: FluxAppStatusDetail
isPipelineTriggered?: boolean
releaseMode?: ReleaseMode
+ cdPipelineId?: number
+ triggerType?: string
+ parentEnvironmentName?: string
+ ciPipelineId?: number
+ trafficSwitched?: boolean
+ pcoId?: number
}
export interface ConfigDriftModalProps extends Required> {
@@ -1106,18 +1117,10 @@ export interface LicenseErrorStruct {
userMessage: string
}
-export interface DevtronLicenseBaseDTO {
+export type DevtronLicenseBaseDTO = {
fingerprint: string | null
isTrial: boolean | null
isFreemium: boolean | null
- /**
- * In timestamp format
- */
- expiry: string | null
- /**
- * Can be negative, depicts time left in seconds for license to expire
- */
- ttl: number | null
/**
* Show a reminder after these many DAYS left for license to expire, i.e,
* Show if `ttl` is less than `reminderThreshold` [converted to seconds]
@@ -1128,7 +1131,31 @@ export interface DevtronLicenseBaseDTO {
domain: string | null
} | null
license: string | null
-}
+} & (
+ | {
+ isSaasInstance: true
+ /**
+ * In seconds
+ */
+ timeElapsedSinceCreation: number
+ creationTime: string
+ ttl?: never
+ expiry?: never
+ }
+ | {
+ isSaasInstance?: false
+ timeElapsedSinceCreation?: never
+ creationTime?: never
+ /**
+ * Can be negative, depicts time left in seconds for license to expire
+ */
+ ttl: number | null
+ /**
+ * In timestamp format
+ */
+ expiry: string | null
+ }
+)
export type DevtronLicenseDTO = DevtronLicenseBaseDTO &
(isCentralDashboard extends true
@@ -1141,9 +1168,14 @@ export type DevtronLicenseDTO = Devt
showLicenseData?: never
licenseStatusError?: never
moduleLimits?: never
+ instanceData: {
+ devtronUrl: string
+ devtronPassword: string
+ } | null
}
: {
claimedByUserDetails?: never
+ instanceData?: never
showLicenseData: boolean
licenseStatusError?: LicenseErrorStruct
moduleLimits: {
diff --git a/src/index.ts b/src/index.ts
index fbff34eac..afb611a11 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -188,6 +188,8 @@ export interface customEnv {
* @default false
*/
FEATURE_STORAGE_ENABLE?: boolean
+ /** Org ID for grafana */
+ GRAFANA_ORG_ID?: number
}
declare global {
interface Window {
diff --git a/vite.config.ts b/vite.config.ts
index bbf0c0665..5c782fa6b 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -64,10 +64,6 @@ export default defineConfig({
assetFileNames: 'assets/[name][extname]',
entryFileNames: '[name].js',
manualChunks(id: string) {
- if (id.includes('/node_modules/react-dates')) {
- return '@react-dates'
- }
-
if (id.includes('/node_modules/framer-motion')) {
return '@framer-motion'
}