diff --git a/package-lock.json b/package-lock.json index 15f253511..0c65f05ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.19.0-pre-5", + "version": "1.19.4-pre-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.19.0-pre-5", + "version": "1.19.4-pre-0", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index e5ebb3992..1d22aba8f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.19.0-pre-5", + "version": "1.19.4-pre-0", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", diff --git a/src/Assets/IconV2/ic-apica.svg b/src/Assets/IconV2/ic-apica.svg deleted file mode 100755 index 3dd30b311..000000000 --- a/src/Assets/IconV2/ic-apica.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/src/Assets/IconV2/ic-aws-codecommit.svg b/src/Assets/IconV2/ic-aws-codecommit.svg new file mode 100644 index 000000000..716c885a0 --- /dev/null +++ b/src/Assets/IconV2/ic-aws-codecommit.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Assets/IconV2/ic-upgrade-enterprise.svg b/src/Assets/IconV2/ic-upgrade-enterprise.svg new file mode 100644 index 000000000..eb7ca93cf --- /dev/null +++ b/src/Assets/IconV2/ic-upgrade-enterprise.svg @@ -0,0 +1,320 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Common/CIPipeline.Types.ts b/src/Common/CIPipeline.Types.ts index c0bcb8aa0..7fb61ff02 100644 --- a/src/Common/CIPipeline.Types.ts +++ b/src/Common/CIPipeline.Types.ts @@ -27,6 +27,7 @@ export interface MaterialType { gitProviderId: number regex?: string isRegex: boolean + url: string } export interface Githost { diff --git a/src/Shared/Components/CICDHistory/Artifacts.tsx b/src/Shared/Components/CICDHistory/Artifacts.tsx index 0c4436ba9..7d9de107f 100644 --- a/src/Shared/Components/CICDHistory/Artifacts.tsx +++ b/src/Shared/Components/CICDHistory/Artifacts.tsx @@ -18,9 +18,7 @@ import { useParams } from 'react-router-dom' import { ReactComponent as Down } from '@Icons/ic-arrow-forward.svg' import { ReactComponent as OpenInNew } from '@Icons/ic-arrow-out.svg' -import docker from '@Icons/ic-docker.svg' import { ReactComponent as Download } from '@Icons/ic-download.svg' -import folder from '@Icons/ic-folder.svg' import { ReactComponent as ICHelpOutline } from '@Icons/ic-help.svg' import { ReactComponent as MechanicalOperation } from '@Icons/ic-mechanical-operation.svg' import noartifact from '@Images/no-artifact.webp' @@ -30,6 +28,8 @@ import { useDownload } from '@Shared/Hooks' import { ClipboardButton, extractImage, GenericEmptyState, ImageTagsContainer, useGetUserRoles } from '../../../Common' import { EMPTY_STATE_STATUS } from '../../constants' import { DocLink } from '../DocLink' +import { Icon } from '../Icon' +import { RegistryIcon } from '../RegistryIcon' import { TargetPlatformBadgeList } from '../TargetPlatforms' import { TERMINAL_STATUS_MAP } from './constants' import { ArtifactType, CIListItemType } from './types' @@ -62,6 +62,7 @@ export const CIListItem = ({ renderCIListHeader, targetPlatforms, isDeploymentWithoutApproval, + artifact, }: CIListItemType) => { const showCIListHeader = !!renderCIListHeader && @@ -99,7 +100,11 @@ export const CIListItem = ({ >
- type + {type === 'report' ? ( + + ) : ( + + )}
{children}
@@ -228,6 +233,7 @@ const Artifacts = ({ isSuperAdmin={isSuperAdmin} renderCIListHeader={renderCIListHeader} targetPlatforms={targetPlatforms} + artifact={artifact} >
diff --git a/src/Shared/Components/CICDHistory/History.components.tsx b/src/Shared/Components/CICDHistory/History.components.tsx index 712a6d8d2..be9022f0b 100644 --- a/src/Shared/Components/CICDHistory/History.components.tsx +++ b/src/Shared/Components/CICDHistory/History.components.tsx @@ -204,6 +204,7 @@ export const GitChanges = ({ renderCIListHeader={renderCIListHeader} targetPlatforms={targetPlatforms} isDeploymentWithoutApproval={isDeploymentWithoutApproval} + artifact={artifact} >
diff --git a/src/Shared/Components/CICDHistory/types.tsx b/src/Shared/Components/CICDHistory/types.tsx index 4c63f94e8..7f6f8483f 100644 --- a/src/Shared/Components/CICDHistory/types.tsx +++ b/src/Shared/Components/CICDHistory/types.tsx @@ -435,6 +435,7 @@ export type CIListItemType = Pick = { + 450: 'w-450', 500: 'w-500', 600: 'w-600', 800: 'w-800', diff --git a/src/Shared/Components/GenericModal/types.ts b/src/Shared/Components/GenericModal/types.ts index 9a0e46c3d..e5993b388 100644 --- a/src/Shared/Components/GenericModal/types.ts +++ b/src/Shared/Components/GenericModal/types.ts @@ -28,7 +28,8 @@ export interface GenericModalProps extends Partial + url.includes('git-codecommit.') && url.includes('.amazonaws.com') + export const getGitIconName = (repoUrl: string): IconName => { if (repoUrl.includes(GitProviderType.GITHUB)) { return 'ic-github' @@ -28,5 +31,12 @@ export const getGitIconName = (repoUrl: string): IconName => { if (repoUrl.includes(GitProviderType.BITBUCKET)) { return 'ic-bitbucket' } + if (repoUrl.includes(GitProviderType.AZURE)) { + return 'ic-azure' + } + if (isAWSCodeCommitURL(repoUrl)) { + return 'ic-aws-codecommit' + } + return 'ic-git' } diff --git a/src/Shared/Components/Header/HelpButton.tsx b/src/Shared/Components/Header/HelpButton.tsx index ba6e16be0..975f046ea 100644 --- a/src/Shared/Components/Header/HelpButton.tsx +++ b/src/Shared/Components/Header/HelpButton.tsx @@ -148,7 +148,7 @@ export const HelpButton = ({ serverInfo, fetchingServerInfo, onClick, hideGettin alignment="end" width={220} options={getHelpActionMenuOptions({ - isTrial: licenseData?.isTrial ?? false, + isTrialOrFreemium: (licenseData?.isTrial || licenseData?.isFreemium) ?? false, isEnterprise, })} onClick={handleActionMenuClick} diff --git a/src/Shared/Components/Header/utils.ts b/src/Shared/Components/Header/utils.ts index 1896ea901..50dae36d9 100644 --- a/src/Shared/Components/Header/utils.ts +++ b/src/Shared/Components/Header/utils.ts @@ -44,10 +44,10 @@ export const setActionWithExpiry = (key: string, days: number): void => { export const getHelpActionMenuOptions = ({ isEnterprise, - isTrial, + isTrialOrFreemium, }: { isEnterprise: boolean - isTrial: boolean + isTrialOrFreemium: boolean }): HelpButtonActionMenuProps['options'] => [ { items: COMMON_HELP_ACTION_MENU_ITEMS, @@ -56,7 +56,9 @@ export const getHelpActionMenuOptions = ({ ? [ { groupLabel: 'Enterprise Support', - items: isTrial ? ENTERPRISE_TRIAL_HELP_ACTION_MENU_ITEMS : ENTERPRISE_HELP_ACTION_MENU_ITEMS, + items: isTrialOrFreemium + ? ENTERPRISE_TRIAL_HELP_ACTION_MENU_ITEMS + : ENTERPRISE_HELP_ACTION_MENU_ITEMS, }, ] : [ diff --git a/src/Shared/Components/Icon/Icon.tsx b/src/Shared/Components/Icon/Icon.tsx index 0a7cc6418..0f886fe00 100644 --- a/src/Shared/Components/Icon/Icon.tsx +++ b/src/Shared/Components/Icon/Icon.tsx @@ -5,7 +5,6 @@ import { ReactComponent as ICAborted } from '@IconsV2/ic-aborted.svg' import { ReactComponent as ICActivity } from '@IconsV2/ic-activity.svg' 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' @@ -15,6 +14,7 @@ import { ReactComponent as ICArrowsClockwise } from '@IconsV2/ic-arrows-clockwis import { ReactComponent as ICArrowsLeftRight } from '@IconsV2/ic-arrows-left-right.svg' import { ReactComponent as ICAsterisk } from '@IconsV2/ic-asterisk.svg' import { ReactComponent as ICAther } from '@IconsV2/ic-ather.svg' +import { ReactComponent as ICAwsCodecommit } from '@IconsV2/ic-aws-codecommit.svg' import { ReactComponent as ICAzure } from '@IconsV2/ic-azure.svg' import { ReactComponent as ICAzureAks } from '@IconsV2/ic-azure-aks.svg' import { ReactComponent as ICBgCluster } from '@IconsV2/ic-bg-cluster.svg' @@ -221,6 +221,7 @@ import { ReactComponent as ICTravclan } from '@IconsV2/ic-travclan.svg' import { ReactComponent as ICTwoCubes } from '@IconsV2/ic-two-cubes.svg' import { ReactComponent as ICUbuntu } from '@IconsV2/ic-ubuntu.svg' import { ReactComponent as ICUnknown } from '@IconsV2/ic-unknown.svg' +import { ReactComponent as ICUpgradeEnterprise } from '@IconsV2/ic-upgrade-enterprise.svg' import { ReactComponent as ICUserCircle } from '@IconsV2/ic-user-circle.svg' import { ReactComponent as ICUserKey } from '@IconsV2/ic-user-key.svg' import { ReactComponent as ICUsers } from '@IconsV2/ic-users.svg' @@ -243,7 +244,6 @@ export const iconMap = { 'ic-activity': ICActivity, 'ic-add': ICAdd, 'ic-amazon-eks': ICAmazonEks, - 'ic-apica': ICApica, 'ic-app-group': ICAppGroup, 'ic-app-template': ICAppTemplate, 'ic-arrow-clockwise': ICArrowClockwise, @@ -253,6 +253,7 @@ export const iconMap = { 'ic-arrows-left-right': ICArrowsLeftRight, 'ic-asterisk': ICAsterisk, 'ic-ather': ICAther, + 'ic-aws-codecommit': ICAwsCodecommit, 'ic-azure-aks': ICAzureAks, 'ic-azure': ICAzure, 'ic-bg-cluster': ICBgCluster, @@ -459,6 +460,7 @@ export const iconMap = { 'ic-two-cubes': ICTwoCubes, 'ic-ubuntu': ICUbuntu, 'ic-unknown': ICUnknown, + 'ic-upgrade-enterprise': ICUpgradeEnterprise, 'ic-user-circle': ICUserCircle, 'ic-user-key': ICUserKey, 'ic-users': ICUsers, diff --git a/src/Shared/Components/License/DevtronLicenseCard.tsx b/src/Shared/Components/License/DevtronLicenseCard.tsx index d463f0a8c..f2904251d 100644 --- a/src/Shared/Components/License/DevtronLicenseCard.tsx +++ b/src/Shared/Components/License/DevtronLicenseCard.tsx @@ -23,6 +23,7 @@ import { ClipboardButton, getTTLInHumanReadableFormat } from '@Common/index' import { CONTACT_SUPPORT_LINK, ENTERPRISE_SUPPORT_LINK } from '@Shared/constants' import { AppThemeType } from '@Shared/Providers' import { getThemeOppositeThemeClass } from '@Shared/Providers/ThemeProvider/utils' +import { LicensingErrorCodes } from '@Shared/types' import { Button, ButtonComponentType, ButtonVariantType } from '../Button' import { Icon } from '../Icon' @@ -33,6 +34,90 @@ import './licenseCard.scss' const DAMPEN_FACTOR = 50 +const ContactSupportButton = () => ( +
+ +
+ )} +
+ ) + } + + // Cases when not freemium + + if (licenseStatus === LicenseStatus.ACTIVE) { + return null + } + + const isLicenseExpired = licenseStatus === LicenseStatus.EXPIRED + + return ( +
+
+ + To renew your license mail us at  + {ENTERPRISE_SUPPORT_LINK} or contact your Devtron + representative. + + +
+ +
+ ) +} + export const DevtronLicenseCard = ({ enterpriseName, licenseKey, @@ -40,14 +125,15 @@ export const DevtronLicenseCard = ({ expiryDate, licenseStatus, isTrial, + isFreemium, ttl, appTheme, handleCopySuccess, + licenseStatusError, }: DevtronLicenseCardProps) => { - const { bgColor, textColor } = getLicenseColorsAccordingToStatus(licenseStatus) + const { bgColor, textColor } = getLicenseColorsAccordingToStatus({ isFreemium, licenseStatus, licenseStatusError }) const remainingTime = getTTLInHumanReadableFormat(ttl) const remainingTimeString = ttl < 0 ? `Expired ${remainingTime} ago` : `${remainingTime} remaining` - const isLicenseValid = licenseStatus !== LicenseStatus.EXPIRED const isThemeDark = appTheme === AppThemeType.dark const cardRef = useRef(null) @@ -93,7 +179,7 @@ export const DevtronLicenseCard = ({ : useMotionTemplate`linear-gradient(55deg, transparent, rgba(255, 255, 255, ${sheenOpacity}) ${sheenPosition}%, transparent)` return ( -
+
- {expiryDate} - · - {remainingTimeString} + + {isFreemium ? 'VALID FOREVER' : expiryDate} + + {!isFreemium && ( + <> + · + {remainingTimeString} + + )}
- {isTrial && ( + {(isTrial || isFreemium) && ( - TRIAL LICENSE + {isFreemium ? 'FREEMIUM' : 'TRIAL'} LICENSE )}
- {licenseStatus !== LicenseStatus.ACTIVE && ( -
-
- - To renew your license mail us at  - {ENTERPRISE_SUPPORT_LINK} or contact your - Devtron representative. - - -
-
- )} +
) } diff --git a/src/Shared/Components/License/constants.ts b/src/Shared/Components/License/constants.ts new file mode 100644 index 000000000..2033d8348 --- /dev/null +++ b/src/Shared/Components/License/constants.ts @@ -0,0 +1 @@ +export const ALLOWED_CLUSTER_IN_FREEMIUM = 2 diff --git a/src/Shared/Components/License/licenseCard.scss b/src/Shared/Components/License/licenseCard.scss index 2663e3b99..57710e14b 100644 --- a/src/Shared/Components/License/licenseCard.scss +++ b/src/Shared/Components/License/licenseCard.scss @@ -14,10 +14,18 @@ * limitations under the License. */ -.license-card { +.license-card-wrapper { + .license-card { + .trial-license-badge { + background-color: var(--divider-secondary-translucent); + letter-spacing: 0.55px; + } + } - .trial-license-badge { - background-color: var(--divider-secondary-translucent); - letter-spacing: 0.55px; + .mail-support { + .button { + text-transform: lowercase; + } } } + diff --git a/src/Shared/Components/License/types.ts b/src/Shared/Components/License/types.ts index 878971301..38b2be000 100644 --- a/src/Shared/Components/License/types.ts +++ b/src/Shared/Components/License/types.ts @@ -15,7 +15,7 @@ */ import { AppThemeType } from '@Shared/Providers' -import { DevtronLicenseBaseDTO, DevtronLicenseDTO } from '@Shared/types' +import { DevtronLicenseBaseDTO, DevtronLicenseDTO, LicenseErrorStruct } from '@Shared/types' export enum LicenseStatus { ACTIVE = 'ACTIVE', @@ -29,7 +29,9 @@ export type DevtronLicenseCardProps = { ttl: number licenseStatus: LicenseStatus isTrial: boolean + isFreemium: boolean appTheme: AppThemeType + licenseStatusError: LicenseErrorStruct } & ( | { licenseKey: string @@ -44,7 +46,7 @@ export type DevtronLicenseCardProps = { ) export type DevtronLicenseInfo = Omit & - Pick + Pick export interface ActivateLicenseDialogProps extends Pick { enterpriseName: string diff --git a/src/Shared/Components/License/utils.tsx b/src/Shared/Components/License/utils.tsx index fc6d78dcb..540d14613 100644 --- a/src/Shared/Components/License/utils.tsx +++ b/src/Shared/Components/License/utils.tsx @@ -18,13 +18,25 @@ import moment from 'moment' import { DATE_TIME_FORMATS } from '@Common/Constants' import { getUrlWithSearchParams } from '@Common/index' -import { DevtronLicenseDTO } from '@Shared/types' +import { DevtronLicenseDTO, LicensingErrorCodes } from '@Shared/types' +import { ALLOWED_CLUSTER_IN_FREEMIUM } from './constants' import { DevtronLicenseCardProps, DevtronLicenseInfo, LicenseStatus } from './types' -export const getLicenseColorsAccordingToStatus = ( - licenseStatus: LicenseStatus, -): { bgColor: string; textColor: string } => { +export const getLicenseColorsAccordingToStatus = ({ + isFreemium, + licenseStatus, + licenseStatusError, +}: 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)' } + } switch (licenseStatus) { case LicenseStatus.ACTIVE: return { bgColor: 'var(--G100)', textColor: 'var(--G500)' } @@ -54,8 +66,17 @@ export const parseDevtronLicenseDTOIntoLicenseCardData = , currentUserEmail?: isCentralDashboard extends true ? string : never, ): Omit => { - const { isTrial, expiry, ttl, reminderThreshold, organisationMetadata, license, claimedByUserDetails } = - licenseDTO || {} + const { + isTrial, + expiry, + ttl, + reminderThreshold, + organisationMetadata, + license, + claimedByUserDetails, + isFreemium, + licenseStatusError, + } = licenseDTO || {} return { enterpriseName: organisationMetadata?.name || 'Devtron Enterprise', @@ -63,6 +84,8 @@ export const parseDevtronLicenseDTOIntoLicenseCardData = { style={{ maxHeight: '90%', }} - loading="lazy" />
diff --git a/src/Shared/Components/LoginBanner/constants.tsx b/src/Shared/Components/LoginBanner/constants.tsx index 75cdb05e4..612d37b4d 100644 --- a/src/Shared/Components/LoginBanner/constants.tsx +++ b/src/Shared/Components/LoginBanner/constants.tsx @@ -47,12 +47,6 @@ export const TESTIMONIAL_CARD_DATA: TestimonialCardConfig[] = [ name: 'Vinod Vijapur', iconName: 'ic-73strings', }, - { - quote: 'Partnering with Devtron has revolutionized the way we deliver solutions to our customers. Their Kubernetes-native platform has empowered us to automate and scale our deployments, reducing time-to-market and significantly boosting DevOps efficiency. The collaboration has not only streamlined our processes but also enhanced our ability to serve customers with agility and precision.', - designation: 'CPTO at Apica', - name: 'Ranjan Parthasarathy', - iconName: 'ic-apica', - }, { quote: "Devtron CI has been instrumental in our migration to ARM architecture. The automation and efficiency it provides have not only cut costs but also significantly improved our system performance. Devtron's support made the transition smooth and effective, setting a new standard for our infrastructure operations.", designation: 'Principal Engineer at Spinny', diff --git a/src/Shared/Components/RegistryIcon/RegistryIcon.tsx b/src/Shared/Components/RegistryIcon/RegistryIcon.tsx index 215ee1e79..96583fd52 100644 --- a/src/Shared/Components/RegistryIcon/RegistryIcon.tsx +++ b/src/Shared/Components/RegistryIcon/RegistryIcon.tsx @@ -34,6 +34,20 @@ const registryIconMap: Record = { [RegistryType.OTHER]: 'ic-container-registry', } -export const RegistryIcon = ({ registryType, size = 20 }: RegistryIconProps) => ( - +const getRegistryUrlIconName = (registryUrl: string): IconName => { + if (!registryUrl) return registryIconMap[RegistryType.OTHER] + + const matchedType = Object.values(RegistryType).find( + (type) => type !== RegistryType.OTHER && registryUrl.includes(type), + ) + + return matchedType ? registryIconMap[matchedType] : registryIconMap[RegistryType.OTHER] +} + +export const RegistryIcon = ({ registryType, size = 20, registryUrl }: RegistryIconProps) => ( + ) diff --git a/src/Shared/Components/RegistryIcon/types.ts b/src/Shared/Components/RegistryIcon/types.ts index bd974aa22..00b254c6f 100644 --- a/src/Shared/Components/RegistryIcon/types.ts +++ b/src/Shared/Components/RegistryIcon/types.ts @@ -18,11 +18,28 @@ import { RegistryType } from '@Shared/types' import { IconsProps } from '../Icon' -export interface RegistryIconProps { - registryType: RegistryType - /** - * The size of the icon in pixels. - * @default 20 - */ - size?: IconsProps['size'] -} +export type RegistryIconProps = + | { + registryType: RegistryType + /** + * The size of the icon in pixels. + * @default 20 + */ + size?: IconsProps['size'] + /** + * The registry URL to determine the icon. + */ + registryUrl?: never + } + | { + registryType?: never + /** + * The size of the icon in pixels. + * @default 20 + */ + size?: IconsProps['size'] + /** + * The registry URL to determine the icon. + */ + registryUrl: string + } diff --git a/src/Shared/constants.tsx b/src/Shared/constants.tsx index c5bfd815e..a644380a6 100644 --- a/src/Shared/constants.tsx +++ b/src/Shared/constants.tsx @@ -529,6 +529,7 @@ export const EULA_LINK = 'https://devtron.ai/end-user-license-agreement-eula' export const CONTACT_SUPPORT_LINK = 'https://devtron.ai/enterprise-support' export const PRIVACY_POLICY_LINK = 'https://devtron.ai/privacy-policy' export const TERMS_OF_USE_LINK = 'https://devtron.ai/terms-of-use' +export const CONTACT_SALES_LINK = 'https://devtron.ai/contact-sales' export const enum DeleteComponentsName { Cluster = 'cluster', diff --git a/src/Shared/types.ts b/src/Shared/types.ts index b49b71474..0d2fc6724 100644 --- a/src/Shared/types.ts +++ b/src/Shared/types.ts @@ -1099,6 +1099,7 @@ export enum LicensingErrorCodes { LicKeyMismatch = '11006', NoCertFound = '11007', LicKeyNotFound = '11008', + ClusterLimitExceeded = '11011', } export interface LicenseErrorStruct { @@ -1109,6 +1110,7 @@ export interface LicenseErrorStruct { export interface DevtronLicenseBaseDTO { fingerprint: string | null isTrial: boolean | null + isFreemium: boolean | null /** * In timestamp format */ @@ -1139,11 +1141,16 @@ export type DevtronLicenseDTO = Devt } | null showLicenseData?: never licenseStatusError?: never + moduleLimits?: never } : { claimedByUserDetails?: never showLicenseData: boolean licenseStatusError?: LicenseErrorStruct + moduleLimits: { + allAllowed: boolean + maxAllowedClusters: number + } }) export type CountryISO2Type = ParsedCountry['iso2'] @@ -1177,6 +1184,7 @@ export type IconBaseSizeType = | 42 | 44 | 48 + | 64 | 72 | 80