Skip to content

Commit 5a68c16

Browse files
authored
Merge pull request #642 from devtron-labs/feat/license-card-animation
feat: license card animation
2 parents f70eb97 + 47bbd41 commit 5a68c16

File tree

9 files changed

+178
-27
lines changed

9 files changed

+178
-27
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devtron-labs/devtron-fe-common-lib",
3-
"version": "1.9.5-beta-11",
3+
"version": "1.9.5-beta-13",
44
"description": "Supporting common component library",
55
"type": "module",
66
"main": "dist/index.js",

src/Common/Helper.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,5 +1078,8 @@ export const getHashedValue = async (value: string): Promise<string | null> => {
10781078
}
10791079
}
10801080

1081-
export const getTTLInHumanReadableFormat = (ttl: number): string =>
1082-
moment.duration(Math.abs(ttl), 'seconds').humanize(false)
1081+
export const getTTLInHumanReadableFormat = (ttl: number): string => {
1082+
const humanizedDuration = moment.duration(Math.abs(ttl), 'seconds').humanize(false)
1083+
// Since moment.js return "a" or "an" for singular values so replacing with 1.
1084+
return humanizedDuration.replace(/^(a|an) /, '1 ');
1085+
}

src/Shared/Components/DevtronLicenseCard/DevtronLicenseCard.tsx

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1+
import { useEffect, useRef } from 'react'
2+
import { animate, motion, useMotionTemplate, useMotionValue, useTransform } from 'framer-motion'
13
import { ClipboardButton, getTTLInHumanReadableFormat } from '@Common/index'
24
import { ReactComponent as ICChatSupport } from '@IconsV2/ic-chat-circle-dots.svg'
3-
import { ENTERPRISE_SUPPORT_LINK } from '@Shared/index'
5+
import {
6+
CONTACT_SUPPORT_LINK,
7+
DevtronLicenseCardProps,
8+
ENTERPRISE_SUPPORT_LINK,
9+
getHandleOpenURL,
10+
LicenseStatus,
11+
} from '@Shared/index'
412
import { Button, ButtonVariantType } from '../Button'
513
import { Icon } from '../Icon'
614
import { getLicenseColorsAccordingToStatus } from './utils'
7-
import { DevtronLicenseCardProps, LicenseStatus } from './types'
815
import './licenseCard.scss'
916

17+
const DAMPEN_FACTOR = 40
18+
1019
export const DevtronLicenseCard = ({
1120
enterpriseName,
1221
licenseKey,
@@ -19,10 +28,59 @@ export const DevtronLicenseCard = ({
1928
const { bgColor, textColor } = getLicenseColorsAccordingToStatus(licenseStatus)
2029
const remainingTime = getTTLInHumanReadableFormat(ttl)
2130
const remainingTimeString = ttl < 0 ? `Expired ${remainingTime} ago` : `${remainingTime} remaining`
31+
const isLicenseValid = ttl > 0
32+
33+
const cardRef = useRef<HTMLDivElement>(null)
34+
35+
const mouseX = useMotionValue(window.innerWidth)
36+
const mouseY = useMotionValue(window.innerHeight)
37+
38+
const rotateX = useTransform<number, number>(mouseY, (newMouseY) => {
39+
if (!cardRef.current) return 0
40+
const rect = cardRef.current.getBoundingClientRect()
41+
const newRotateX = newMouseY - rect.top - rect.height / 2
42+
return -newRotateX / DAMPEN_FACTOR
43+
})
44+
const rotateY = useTransform(mouseX, (newMouseX) => {
45+
if (!cardRef.current) return 0
46+
const rect = cardRef.current.getBoundingClientRect()
47+
const newRotateY = newMouseX - rect.left - rect.width / 2
48+
return newRotateY / DAMPEN_FACTOR
49+
})
50+
51+
useEffect(() => {
52+
const handleMouseMove = (e: MouseEvent) => {
53+
animate(mouseX, e.clientX)
54+
animate(mouseY, e.clientY)
55+
}
56+
57+
window.addEventListener('mousemove', handleMouseMove)
58+
59+
return () => {
60+
window.removeEventListener('mousemove', handleMouseMove)
61+
}
62+
}, [])
63+
64+
const diagonalMovement = useTransform<number, number>(
65+
[rotateX, rotateY],
66+
([newRotateX, newRotateY]) => newRotateX + newRotateY,
67+
)
68+
const sheenPosition = useTransform(diagonalMovement, [-5, 5], [-100, 200])
69+
70+
const sheenOpacity = useTransform(sheenPosition, [-100, 50, 200], [0, 0.05, 0])
71+
const sheenGradient = useMotionTemplate`linear-gradient(
72+
55deg,
73+
transparent,
74+
rgba(255 255 255 / ${sheenOpacity}) ${sheenPosition}%,
75+
transparent)`
2276

2377
return (
2478
<div className="flexbox-col p-8 br-16" style={{ backgroundColor: bgColor }}>
25-
<div className="license-card border__secondary-translucent flexbox-col br-12 h-200 bg__tertiary">
79+
<motion.div
80+
className="license-card border__secondary-translucent flexbox-col br-12 h-200 bg__tertiary"
81+
ref={cardRef}
82+
style={{ rotateX, rotateY, backgroundImage: sheenGradient }}
83+
>
2684
<div className="p-20 flexbox-col dc__content-space flex-grow-1">
2785
<div className="flexbox dc__align-items-center dc__content-space">
2886
<span className="font-merriweather cn-9 fs-16 fw-7 lh-1-5 dc__truncate">{enterpriseName}</span>
@@ -37,8 +95,8 @@ export const DevtronLicenseCard = ({
3795
</div>
3896
{licenseKey && <ClipboardButton content={licenseKey} />}
3997
</div>
40-
<div className="flexbox dc__align-items-center dc__gap-4">
41-
<span>{expiryDate}</span>
98+
<div className="flexbox dc__align-items-center dc__gap-4 flex-wrap">
99+
<span className="font-ibm-plex-mono">{expiryDate}</span>
42100
<span></span>
43101
<span style={{ color: textColor }}>{remainingTimeString}</span>
44102
</div>
@@ -49,7 +107,7 @@ export const DevtronLicenseCard = ({
49107
TRIAL LICENSE
50108
</span>
51109
)}
52-
</div>
110+
</motion.div>
53111
{licenseStatus !== LicenseStatus.ACTIVE && (
54112
<div className="p-16 flexbox-col dc__gap-8">
55113
<div className="flexbox dc__gap-8">
@@ -58,14 +116,18 @@ export const DevtronLicenseCard = ({
58116
<a href={`mailto:${ENTERPRISE_SUPPORT_LINK}`}>{ENTERPRISE_SUPPORT_LINK}</a> or contact your
59117
Devtron representative.
60118
</span>
61-
<Icon name={ttl < 0 ? 'ic-timer' : 'ic-error'} color={ttl < 0 ? 'Y500' : 'R500'} size={16} />
119+
<Icon
120+
name={isLicenseValid ? 'ic-timer' : 'ic-error'}
121+
color={isLicenseValid ? 'Y500' : 'R500'}
122+
size={16}
123+
/>
62124
</div>
63-
{/* TODO: Add onClick, and common out the button */}
64125
<Button
65126
dataTestId="contact-support"
66127
startIcon={<ICChatSupport />}
67128
text="Contact support"
68129
variant={ButtonVariantType.text}
130+
onClick={getHandleOpenURL(CONTACT_SUPPORT_LINK)}
69131
/>
70132
</div>
71133
)}

src/Shared/Components/Header/HelpNav.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const HelpNav = ({
3636
setGettingStartedClicked,
3737
showHelpCard,
3838
}: HelpNavType) => {
39-
const { currentServerInfo, handleOpenLicenseInfoDialog } = useMainContext()
39+
const { currentServerInfo, handleOpenLicenseInfoDialog, showLicenseData } = useMainContext()
4040
const isEnterprise = currentServerInfo?.serverInfo?.installationType === InstallationType.ENTERPRISE
4141
const FEEDBACK_FORM_ID = `UheGN3KJ#source=${window.location.hostname}`
4242

@@ -89,6 +89,10 @@ const HelpNav = ({
8989
onClickHelpOptions(option)
9090
}
9191

92+
const handleOpenLicenseDialog = () => {
93+
handleOpenLicenseInfoDialog()
94+
}
95+
9296
const renderHelpOptions = (): JSX.Element => (
9397
<>
9498
{CommonHelpOptions.map((option, index) => (
@@ -105,12 +109,13 @@ const HelpNav = ({
105109
<option.icon />
106110
<div className="ml-12 cn-9 fs-14">{option.name}</div>
107111
</a>
108-
{isEnterprise && index === 1 && (
112+
{isEnterprise && showLicenseData && index === 1 && (
109113
<>
110114
<button
111115
type="button"
112116
className="dc__transparent help-card__option flexbox dc__align-items-center cn-9 dc__gap-12 fs-14"
113-
onClick={handleOpenLicenseInfoDialog}
117+
onClick={handleOpenLicenseDialog}
118+
data-testid="about-devtron"
114119
>
115120
<Icon name="ic-devtron" color="N600" size={20} />
116121
About Devtron

src/Shared/Components/LoginBanner/LoginBanner.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { motion } from 'framer-motion'
2+
import { useEffect, useState } from 'react'
23
import { Icon } from '../Icon'
3-
import { TESTIMONIAL_CARD_DATA } from './constants'
4+
import { TESTIMONIAL_CARD_DATA, TESTIMONIAL_CARD_INTERVAL, TRANSITION_EASE_CURVE } from './constants'
45

56
const AnimatedBackground = () => (
67
<motion.div>
@@ -96,7 +97,20 @@ const AnimatedBackground = () => (
9697
)
9798

9899
const LoginBanner = () => {
99-
const { quote, name, designation, iconName } = TESTIMONIAL_CARD_DATA
100+
const [currentIndex, setCurrentIndex] = useState<number>(0)
101+
102+
useEffect(() => {
103+
const testimonialCount = TESTIMONIAL_CARD_DATA.length
104+
const interval = setInterval(() => {
105+
setCurrentIndex((prevIndex) => (prevIndex + 1) % testimonialCount)
106+
}, TESTIMONIAL_CARD_INTERVAL)
107+
108+
return () => {
109+
clearInterval(interval)
110+
}
111+
}, [])
112+
113+
const { quote, name, designation, iconName } = TESTIMONIAL_CARD_DATA[currentIndex]
100114

101115
return (
102116
<div className="flexbox-col br-12 border__primary dc__position-rel dc__overflow-hidden">
@@ -111,7 +125,22 @@ const LoginBanner = () => {
111125
<Icon name="ic-quote" color="N900" />
112126
<div className="border__primary w-1 flex-grow-1" />
113127
</div>
114-
<div className="flexbox-col dc__gap-20">
128+
<motion.div
129+
key={currentIndex}
130+
variants={{
131+
hidden: { opacity: 0, x: 200 },
132+
visible: { opacity: 1, x: 0 },
133+
}}
134+
initial="hidden"
135+
animate="visible"
136+
exit="hidden"
137+
transition={{
138+
staggerChildren: 0.3,
139+
opacity: { duration: 0.75, ease: TRANSITION_EASE_CURVE },
140+
x: { duration: 0.85, ease: TRANSITION_EASE_CURVE },
141+
}}
142+
className="flexbox-col dc__gap-20"
143+
>
115144
<div className="fs-14 fw-4 lh-1-5 cn-9">{quote}</div>
116145
<div className="flexbox dc__content-space dc__align-items-center">
117146
<div>
@@ -120,7 +149,7 @@ const LoginBanner = () => {
120149
</div>
121150
<Icon name={iconName} color={null} size={24} />
122151
</div>
123-
</div>
152+
</motion.div>
124153
</div>
125154
</div>
126155
)
Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
11
import { TestimonialCardConfig } from './types'
22

33
// TODO: add more testimonials (pending on product team)
4-
export const TESTIMONIAL_CARD_DATA: TestimonialCardConfig = {
5-
quote: "Devtron has been instrumental in our transition to Kubernetes. Its platform helped us streamline our CI/CD processes, ensuring consistent and secure deployments. We've seen significantly improved our deployment speed and security posture thanks to Devtron's built-in DevSecOps features.",
6-
name: 'Sathish Kumar',
7-
designation: 'CloudOps/DevOps Lead at Ather Energy Pvt. Ltd.',
8-
iconName: 'ic-devtron', // TODO: add ather icon
9-
}
4+
export const TESTIMONIAL_CARD_DATA: TestimonialCardConfig[] = [
5+
{
6+
quote: "Devtron has been instrumental in our transition to Kubernetes. Its platform helped us streamline our CI/CD processes, ensuring consistent and secure deployments. We've seen significantly improved our deployment speed and security posture thanks to Devtron's built-in DevSecOps features.",
7+
name: 'Sathish Kumar',
8+
designation: 'CloudOps/DevOps Lead at Ather Energy Pvt. Ltd.',
9+
iconName: 'ic-devtron',
10+
},
11+
{
12+
quote: 'One of the best tools in the market. A production-ready platform that helps in automating all the requirements on Kubernetes. The platform is easy to get started with. Well done product! We run a lot of migrations and backup jobs daily, and Devtron makes it super easy and auditable for us.',
13+
name: 'Arun Jain',
14+
designation: 'Devtron user',
15+
iconName: 'ic-devtron',
16+
},
17+
{
18+
quote: "Devtron has been instrumental in our transition to Kubernetes. Its platform helped us streamline our CI/CD processes, ensuring consistent and secure deployments. We've seen significantly improved our deployment speed and security posture thanks to Devtron's built-in DevSecOps features.",
19+
name: 'Abhishek',
20+
designation: 'CloudOps Lead at Ather Energy Pvt. Ltd.',
21+
iconName: 'ic-devtron',
22+
},
23+
{
24+
quote: 'Devtron has changed the way we used to operate, from bunch of scripts to now just a dashboard. The UI is pretty good and fast. Devtron has truly removed the dependencies of our developers from DevOps peeps. The UI is user-friendly and simple for new users to get acquainted with the tool.',
25+
name: 'Utkarsh Arya',
26+
designation: 'Product Manager',
27+
iconName: 'ic-devtron',
28+
},
29+
]
30+
31+
export const TESTIMONIAL_CARD_INTERVAL = 5000 // duration (in ms) for each testimonial card slide animation
32+
export const TRANSITION_EASE_CURVE = [0.25, 0.8, 0.25, 1]

src/Shared/Providers/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { MutableRefObject, ReactNode } from 'react'
1818
import { ServerInfo } from '../Components/Header/types'
1919
import { SERVER_MODE } from '../../Common'
20+
import { LicenseInfoDialogType } from '..'
2021

2122
export interface MainContext {
2223
serverMode: SERVER_MODE
@@ -55,7 +56,11 @@ export interface MainContext {
5556
isManifestScanningEnabled: boolean
5657
canOnlyViewPermittedEnvOrgLevel: boolean
5758
viewIsPipelineRBACConfiguredNode: ReactNode
58-
handleOpenLicenseInfoDialog: () => void
59+
handleOpenLicenseInfoDialog: (
60+
initialDialogType?: LicenseInfoDialogType.ABOUT | LicenseInfoDialogType.LICENSE,
61+
) => void
62+
showLicenseData: boolean
63+
setShowLicenseData: (showLicenseData: boolean) => void
5964
}
6065

6166
export interface MainContextProviderProps {

src/Shared/types.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,26 @@ export interface AppEnvIdType {
10331033
envId: number
10341034
}
10351035

1036+
export enum LicenseInfoDialogType {
1037+
ABOUT = 'about',
1038+
LICENSE = 'license',
1039+
UPDATE = 'update',
1040+
}
1041+
1042+
export enum LicensingErrorCodes {
1043+
FingerPrintMisMatch = '11001',
1044+
LicenseExpired = '11002',
1045+
TamperedCertificate = '11002',
1046+
NoPublicKey = '11003',
1047+
InstallationModeMismatch = '11004',
1048+
NoCertFound = '11006',
1049+
}
1050+
1051+
export interface LicenseErrorStruct {
1052+
code: LicensingErrorCodes
1053+
userMessage: string
1054+
}
1055+
10361056
export interface DevtronLicenseBaseDTO {
10371057
fingerprint: string | null
10381058
isTrial: boolean | null
@@ -1064,9 +1084,13 @@ export type DevtronLicenseDTO<isCentralDashboard extends boolean = false> = Devt
10641084
lastName: string | null
10651085
email: string | null
10661086
} | null
1087+
showLicenseData?: never
1088+
licenseStatusError?: never
10671089
}
10681090
: {
10691091
claimedByUserDetails?: never
1092+
showLicenseData: boolean
1093+
licenseStatusError?: LicenseErrorStruct
10701094
})
10711095

10721096
export type CountryISO2Type = ParsedCountry['iso2']

0 commit comments

Comments
 (0)