diff --git a/airbyte-webapp/src/components/base/Banner/AlertBanner.module.scss b/airbyte-webapp/src/components/base/Banner/AlertBanner.module.scss new file mode 100644 index 000000000000..a6e493d0c799 --- /dev/null +++ b/airbyte-webapp/src/components/base/Banner/AlertBanner.module.scss @@ -0,0 +1,24 @@ +@use "../../../scss/colors"; + +.alertBannerContainer { + height: 30px; + width: 100%; + text-align: center; + position: fixed; + z-index: 3; + font-size: 12px; + line-height: 30px; + color: colors.$black; + + & a { + color: colors.$black; + } +} + +.beige { + background-color: colors.$beige-100; +} + +.red { + background-color: colors.$red; +} diff --git a/airbyte-webapp/src/components/base/Banner/AlertBanner.tsx b/airbyte-webapp/src/components/base/Banner/AlertBanner.tsx index 77036a63b155..7e18fadf65a5 100644 --- a/airbyte-webapp/src/components/base/Banner/AlertBanner.tsx +++ b/airbyte-webapp/src/components/base/Banner/AlertBanner.tsx @@ -1,41 +1,18 @@ +import classnames from "classnames"; import React from "react"; -import { FormattedMessage } from "react-intl"; -import styled from "styled-components"; -import { Link } from "components/Link"; - -import { CloudRoutes } from "packages/cloud/cloudRoutes"; -import { CreditStatus } from "packages/cloud/lib/domain/cloudWorkspaces/types"; - -const Container = styled.div<{ errorType?: string }>` - height: 30px; - width: 100%; - background: ${({ errorType, theme }) => (errorType === "credits" ? theme.redColor : theme.warningColor)}; - color: ${({ theme }) => theme.blackColor}; - text-align: center; - position: fixed; - z-index: 3; - font-size: 12px; - line-height: 30px; -`; -const CreditsLink = styled(Link)` - color: ${({ theme }) => theme.blackColor}; -`; +import styles from "./AlertBanner.module.scss"; interface AlertBannerProps { - alertType: string; - id: CreditStatus | string; + color?: "default" | "warning"; + message: React.ReactNode; } -export const AlertBanner: React.FC = ({ alertType: errorType, id }) => ( - - {errorType === "credits" ? ( - {content} }} - /> - ) : ( - - )} - -); +export const AlertBanner: React.FC = ({ color, message }) => { + const bannerStyle = classnames(styles.alertBannerContainer, { + [styles.beige]: color === "default" || !color, + [styles.red]: color === "warning", + }); + + return
{message}
; +}; diff --git a/airbyte-webapp/src/packages/cloud/lib/domain/cloudWorkspaces/types.ts b/airbyte-webapp/src/packages/cloud/lib/domain/cloudWorkspaces/types.ts index ba6740ac585f..36d8357bacec 100644 --- a/airbyte-webapp/src/packages/cloud/lib/domain/cloudWorkspaces/types.ts +++ b/airbyte-webapp/src/packages/cloud/lib/domain/cloudWorkspaces/types.ts @@ -12,6 +12,7 @@ export interface CloudWorkspace { remainingCredits: number; creditStatus?: CreditStatus; lastCreditPurchaseIncrementTimestamp?: number | null; + trialExpiryTimestamp?: number | null; } export interface CreditConsumptionByConnector { diff --git a/airbyte-webapp/src/packages/cloud/locales/en.json b/airbyte-webapp/src/packages/cloud/locales/en.json index 2bba0236b801..aac543bbb108 100644 --- a/airbyte-webapp/src/packages/cloud/locales/en.json +++ b/airbyte-webapp/src/packages/cloud/locales/en.json @@ -99,7 +99,7 @@ "modals.addUser.button.cancel": "Cancel", "modals.addUser.button.submit": "Send invitation", "workspaces.viewAllWorkspaces": "View all workspaces", - "settings.accessManagement.roleViewers": "Viewers are in read-only and cannot edit or\u00a0add connections.", + "settings.accessManagement.roleViewers": "Viewers are in read-only and cannot edit or add connections.", "settings.accessManagement.roleEditors": "Editors can edit connections", "settings.accessManagement.roleAdmin": "Admin can also manage users", @@ -130,6 +130,8 @@ "password.validation": "Your password is too weak", "password.invalid": "Invalid password", + "trial.alertMessage": "You are using a trial of Airbyte. Your trial ends in {value, plural, one {# day} other {# days}}.", + "verifyEmail.notification": "You successfully verified your email. Thank you.", "webapp.cannotReachServer": "Cannot reach server." diff --git a/airbyte-webapp/src/packages/cloud/views/layout/MainView/MainView.tsx b/airbyte-webapp/src/packages/cloud/views/layout/MainView/MainView.tsx index 6581d93b453a..717f85a89d8a 100644 --- a/airbyte-webapp/src/packages/cloud/views/layout/MainView/MainView.tsx +++ b/airbyte-webapp/src/packages/cloud/views/layout/MainView/MainView.tsx @@ -1,10 +1,12 @@ -import React from "react"; -import { Outlet } from "react-router-dom"; +import React, { useMemo } from "react"; +import { useIntl } from "react-intl"; +import { Link, Outlet } from "react-router-dom"; import styled from "styled-components"; import { LoadingPage } from "components"; import { AlertBanner } from "components/base/Banner/AlertBanner"; +import { CloudRoutes } from "packages/cloud/cloudRoutes"; import { CreditStatus } from "packages/cloud/lib/domain/cloudWorkspaces/types"; import { useGetCloudWorkspace } from "packages/cloud/services/workspaces/WorkspacesService"; import SideBar from "packages/cloud/views/layout/SideBar"; @@ -36,25 +38,51 @@ const DataBlock = styled.div<{ hasBanner?: boolean }>` `; const MainView: React.FC = (props) => { + const { formatMessage } = useIntl(); const workspace = useCurrentWorkspace(); const cloudWorkspace = useGetCloudWorkspace(workspace.workspaceId); - const showBanner = + const showCreditsBanner = cloudWorkspace.creditStatus && [ CreditStatus.NEGATIVE_BEYOND_GRACE_PERIOD, CreditStatus.NEGATIVE_MAX_THRESHOLD, CreditStatus.NEGATIVE_WITHIN_GRACE_PERIOD, - ].includes(cloudWorkspace.creditStatus); + ].includes(cloudWorkspace.creditStatus) && + !cloudWorkspace.trialExpiryTimestamp; + + const alertToShow = showCreditsBanner ? "credits" : cloudWorkspace.trialExpiryTimestamp ? "trial" : undefined; + + const alertMessage = useMemo(() => { + if (alertToShow === "credits") { + return formatMessage( + { id: `credits.creditsProblem.${cloudWorkspace.creditStatus}` }, + { + values: { + lnk: (content: React.ReactNode) => {content}, + }, + } + ); + } else if (alertToShow === "trial") { + const { trialExpiryTimestamp } = cloudWorkspace; + + //calculate difference between timestamp (in epoch seconds) and now (in epoch seconds) + const trialRemainingSeconds = trialExpiryTimestamp ? trialExpiryTimestamp - Date.now() / 1000 : 0; + + //calculate days (rounding up if decimal) + const trialRemainingDays = Math.ceil(trialRemainingSeconds / (24 * 60 * 60)); + + return formatMessage({ id: "trial.alertMessage" }, { value: trialRemainingDays }); + } + return null; + }, [alertToShow, cloudWorkspace, formatMessage]); return ( }> - {cloudWorkspace.creditStatus && showBanner && ( - - )} - + {alertToShow && } + }> }>{props.children ?? }