diff --git a/airbyte-webapp/.gitignore b/airbyte-webapp/.gitignore index d2d8f0f630a20..ffcb7d3c3a8c2 100644 --- a/airbyte-webapp/.gitignore +++ b/airbyte-webapp/.gitignore @@ -27,5 +27,7 @@ yarn-error.log* .env.development .env.production +storybook-static/ + # Ignore generated API client, since it's automatically generated /src/core/request/AirbyteClient.ts diff --git a/airbyte-webapp/package-lock.json b/airbyte-webapp/package-lock.json index a758a58b59fe7..08b97bd8bb776 100644 --- a/airbyte-webapp/package-lock.json +++ b/airbyte-webapp/package-lock.json @@ -8,6 +8,7 @@ "name": "airbyte-webapp", "version": "0.40.0-alpha", "dependencies": { + "@floating-ui/react-dom": "^1.0.0", "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/free-brands-svg-icons": "^6.1.1", "@fortawesome/free-regular-svg-icons": "^6.1.1", @@ -3317,6 +3318,31 @@ "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.6.2.tgz", "integrity": "sha512-zThUKcqIU6utWzM93uEvhlh8qj8A5LMPFJPvk/ODb+8GSSif19xM2Lw1M2ijyBy8+6skSkQBbavPzOU5Oh/8tQ==" }, + "node_modules/@floating-ui/core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.0.0.tgz", + "integrity": "sha512-sm3nW0hHAxTv3gRDdCH8rNVQxijF+qPFo5gAeXCErRjKC7Qc28lIQ3R9Vd7Gw+KgwfA7RhRydDFuGeI0peGq7A==" + }, + "node_modules/@floating-ui/dom": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.0.0.tgz", + "integrity": "sha512-PMqJvY5Fae8HVQgUqM+lidprS6p9LSvB0AUhCdYKqr3YCaV+WaWCeVNBtXPRY2YIdrgcsL2+vd5F07FxgihHUw==", + "dependencies": { + "@floating-ui/core": "^1.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.0.0.tgz", + "integrity": "sha512-uiOalFKPG937UCLm42RxjESTWUVpbbatvlphQAU6bsv+ence6IoVG8JOUZcy8eW81NkU+Idiwvx10WFLmR4MIg==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@formatjs/ecma402-abstract": { "version": "1.11.4", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", @@ -49474,6 +49500,27 @@ "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.6.2.tgz", "integrity": "sha512-zThUKcqIU6utWzM93uEvhlh8qj8A5LMPFJPvk/ODb+8GSSif19xM2Lw1M2ijyBy8+6skSkQBbavPzOU5Oh/8tQ==" }, + "@floating-ui/core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.0.0.tgz", + "integrity": "sha512-sm3nW0hHAxTv3gRDdCH8rNVQxijF+qPFo5gAeXCErRjKC7Qc28lIQ3R9Vd7Gw+KgwfA7RhRydDFuGeI0peGq7A==" + }, + "@floating-ui/dom": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.0.0.tgz", + "integrity": "sha512-PMqJvY5Fae8HVQgUqM+lidprS6p9LSvB0AUhCdYKqr3YCaV+WaWCeVNBtXPRY2YIdrgcsL2+vd5F07FxgihHUw==", + "requires": { + "@floating-ui/core": "^1.0.0" + } + }, + "@floating-ui/react-dom": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.0.0.tgz", + "integrity": "sha512-uiOalFKPG937UCLm42RxjESTWUVpbbatvlphQAU6bsv+ence6IoVG8JOUZcy8eW81NkU+Idiwvx10WFLmR4MIg==", + "requires": { + "@floating-ui/dom": "^1.0.0" + } + }, "@formatjs/ecma402-abstract": { "version": "1.11.4", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", diff --git a/airbyte-webapp/package.json b/airbyte-webapp/package.json index 0f3780fe62b11..22c48721e0a6d 100644 --- a/airbyte-webapp/package.json +++ b/airbyte-webapp/package.json @@ -22,6 +22,7 @@ "validate-links": "ts-node --skip-project ./scripts/validate-links.ts" }, "dependencies": { + "@floating-ui/react-dom": "^1.0.0", "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/free-brands-svg-icons": "^6.1.1", "@fortawesome/free-regular-svg-icons": "^6.1.1", diff --git a/airbyte-webapp/src/components/ReleaseStageBadge/ReleaseStageBadge.tsx b/airbyte-webapp/src/components/ReleaseStageBadge/ReleaseStageBadge.tsx index b95fa30198e0a..c0397ae239269 100644 --- a/airbyte-webapp/src/components/ReleaseStageBadge/ReleaseStageBadge.tsx +++ b/airbyte-webapp/src/components/ReleaseStageBadge/ReleaseStageBadge.tsx @@ -41,7 +41,7 @@ export const ReleaseStageBadge: React.FC = ({ stage, sma ); return tooltip ? ( - + ) : ( diff --git a/airbyte-webapp/src/components/ToolTip/ToolTip.module.scss b/airbyte-webapp/src/components/ToolTip/ToolTip.module.scss new file mode 100644 index 0000000000000..53820fd7897de --- /dev/null +++ b/airbyte-webapp/src/components/ToolTip/ToolTip.module.scss @@ -0,0 +1,34 @@ +@use "../../scss/colors"; +@use "../../scss/variables"; +@use "../../scss/z-indices"; + +.container { + display: inline; + position: relative; +} + +.tooltip { + font-size: 12px; + line-height: initial; + + padding: variables.$spacing-md; + border-radius: 5px; + max-width: 300px; + z-index: z-indices.$tooltip; + box-shadow: 0px 2px 4px rgba(colors.$dark-blue, 0.12); + background: rgba(colors.$dark-blue, 0.9); + color: colors.$white; + + a { + color: rgba(colors.$white, 0.5); + } + + &.light { + background: rgba(colors.$white, 0.9); + color: colors.$dark-blue; + + a { + color: colors.$blue; + } + } +} diff --git a/airbyte-webapp/src/components/ToolTip/ToolTip.tsx b/airbyte-webapp/src/components/ToolTip/ToolTip.tsx index acdf099170662..c4fbeb71a3134 100644 --- a/airbyte-webapp/src/components/ToolTip/ToolTip.tsx +++ b/airbyte-webapp/src/components/ToolTip/ToolTip.tsx @@ -1,51 +1,80 @@ -import React from "react"; -import styled from "styled-components"; - -interface ToolTipProps { - control: React.ReactNode; - className?: string; - disabled?: boolean; - cursor?: "pointer" | "help" | "not-allowed"; -} - -const Control = styled.div<{ $cursor?: "pointer" | "help" | "not-allowed"; $showCursor?: boolean }>` - display: inline; - position: relative; - ${({ $cursor, $showCursor = true }) => ($showCursor && $cursor ? `cursor: ${$cursor}` : "")}; -`; - -const ToolTipView = styled.div<{ $disabled?: boolean }>` - display: none; - font-size: 14px; - line-height: initial; - position: absolute; - padding: 9px 8px 8px; - box-shadow: 0 24px 38px rgba(53, 53, 66, 0.14), 0 9px 46px rgba(53, 53, 66, 0.12), 0 11px 15px rgba(53, 53, 66, 0.2); - border-radius: 4px; - background: rgba(26, 26, 33, 0.9); - color: ${({ theme }) => theme.whiteColor}; - top: calc(100% + 10px); - left: -50px; - min-width: 100px; - width: max-content; - max-width: 380px; - z-index: 10; - - div:hover > &&, - &&:hover { - display: ${({ $disabled }) => ($disabled ? "none" : "block")}; - } -`; - -const ToolTip: React.FC = ({ children, control, className, disabled, cursor }) => { +import { flip, offset, shift, useFloating } from "@floating-ui/react-dom"; +import classNames from "classnames"; +import React, { useState, useEffect } from "react"; + +import { tooltipContext } from "./context"; +import styles from "./ToolTip.module.scss"; +import { ToolTipProps } from "./types"; + +const MOUSE_OUT_TIMEOUT_MS = 50; + +export const ToolTip: React.FC = (props) => { + const { children, control, className, disabled, cursor, theme = "dark", placement = "bottom" } = props; + + const [isMouseOver, setIsMouseOver] = useState(false); + const [isVisible, setIsVisible] = useState(false); + + const { x, y, reference, floating, strategy } = useFloating({ + placement, + middleware: [ + offset(5), // $spacing-sm + flip(), + shift(), + ], + }); + + useEffect(() => { + if (isMouseOver) { + setIsVisible(true); + return; + } + + const timeout = window.setTimeout(() => { + setIsVisible(false); + }, MOUSE_OUT_TIMEOUT_MS); + + return () => { + window.clearTimeout(timeout); + }; + }, [isMouseOver]); + + const canShowTooltip = isVisible && !disabled; + + const onMouseOver = () => { + setIsMouseOver(true); + }; + + const onMouseOut = () => { + setIsMouseOver(false); + }; + return ( - - {control} - - {children} - - + <> +
+ {control} +
+ {canShowTooltip && ( +
+ {children} +
+ )} + ); }; - -export default ToolTip; diff --git a/airbyte-webapp/src/components/ToolTip/TooltipLearnMoreLink.module.scss b/airbyte-webapp/src/components/ToolTip/TooltipLearnMoreLink.module.scss new file mode 100644 index 0000000000000..0766e4d8a5f90 --- /dev/null +++ b/airbyte-webapp/src/components/ToolTip/TooltipLearnMoreLink.module.scss @@ -0,0 +1,5 @@ +@use "../../scss/variables"; + +.container { + margin-top: variables.$spacing-md; +} diff --git a/airbyte-webapp/src/components/ToolTip/TooltipLearnMoreLink.tsx b/airbyte-webapp/src/components/ToolTip/TooltipLearnMoreLink.tsx new file mode 100644 index 0000000000000..bb988391854f4 --- /dev/null +++ b/airbyte-webapp/src/components/ToolTip/TooltipLearnMoreLink.tsx @@ -0,0 +1,15 @@ +import { FormattedMessage } from "react-intl"; + +import styles from "./TooltipLearnMoreLink.module.scss"; + +interface TooltipLearnMoreLinkProps { + url: string; +} + +export const TooltipLearnMoreLink: React.VFC = ({ url }) => ( +
+ + + +
+); diff --git a/airbyte-webapp/src/components/ToolTip/TooltipTable.module.scss b/airbyte-webapp/src/components/ToolTip/TooltipTable.module.scss new file mode 100644 index 0000000000000..f402a78dae83e --- /dev/null +++ b/airbyte-webapp/src/components/ToolTip/TooltipTable.module.scss @@ -0,0 +1,13 @@ +@use "../../scss/colors"; +@use "../../scss/variables"; + +.label { + color: rgba(colors.$white, 0.7); + padding-right: variables.$spacing-sm; +} + +.light { + .label { + color: rgba(colors.$dark-blue, 0.7); + } +} diff --git a/airbyte-webapp/src/components/ToolTip/TooltipTable.tsx b/airbyte-webapp/src/components/ToolTip/TooltipTable.tsx new file mode 100644 index 0000000000000..23812787ff11b --- /dev/null +++ b/airbyte-webapp/src/components/ToolTip/TooltipTable.tsx @@ -0,0 +1,24 @@ +import { useTooltipContext } from "./context"; +import styles from "./TooltipTable.module.scss"; + +interface TooltipTableProps { + rows: React.ReactNode[][]; +} + +export const TooltipTable: React.VFC = ({ rows }) => { + const { theme } = useTooltipContext(); + + return rows.length > 0 ? ( + + + {rows?.map((cols) => ( + + {cols.map((col, index) => ( + + ))} + + ))} + +
{col}
+ ) : null; +}; diff --git a/airbyte-webapp/src/components/ToolTip/context.ts b/airbyte-webapp/src/components/ToolTip/context.ts new file mode 100644 index 0000000000000..27721361986be --- /dev/null +++ b/airbyte-webapp/src/components/ToolTip/context.ts @@ -0,0 +1,15 @@ +import { createContext, useContext } from "react"; + +import { TooltipContext } from "./types"; + +export const tooltipContext = createContext(null); + +export const useTooltipContext = () => { + const ctx = useContext(tooltipContext); + + if (!ctx) { + throw new Error("useTooltipContext should be used within tooltipContext.Provider"); + } + + return ctx; +}; diff --git a/airbyte-webapp/src/components/ToolTip/index.stories.tsx b/airbyte-webapp/src/components/ToolTip/index.stories.tsx new file mode 100644 index 0000000000000..b7296ce38e07e --- /dev/null +++ b/airbyte-webapp/src/components/ToolTip/index.stories.tsx @@ -0,0 +1,57 @@ +import { ComponentStory, ComponentMeta } from "@storybook/react"; + +import { ToolTip } from "./ToolTip"; +import { TooltipLearnMoreLink } from "./TooltipLearnMoreLink"; +import { TooltipTable } from "./TooltipTable"; + +export default { + title: "Ui/ToolTip", + component: ToolTip, + argTypes: { + control: { type: { name: "string", required: true } }, + children: { type: { name: "string", required: true } }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( +
+ +
+); + +export const Primary = Template.bind({}); +Primary.args = { + control: "Hover to see Tooltip", + children: ( + <> + Looking for a job?{" "} + + Apply at Airbyte! + + + ), +}; + +export const WithLearnMoreUrl = Template.bind({}); +WithLearnMoreUrl.args = { + control: "Hover to see Tooltip with Body", + children: ( + <> + Airbyte is hiring! + + ), +}; + +export const WithTable = Template.bind({}); +WithTable.args = { + control: "Hover to see Tooltip with Table", + children: ( + + ), +}; diff --git a/airbyte-webapp/src/components/ToolTip/index.ts b/airbyte-webapp/src/components/ToolTip/index.ts new file mode 100644 index 0000000000000..cd922b6493f3a --- /dev/null +++ b/airbyte-webapp/src/components/ToolTip/index.ts @@ -0,0 +1,8 @@ +import { ToolTip } from "./ToolTip"; + +export * from "./context"; +export * from "./TooltipTable"; +export * from "./types"; + +export default ToolTip; +export { ToolTip }; diff --git a/airbyte-webapp/src/components/ToolTip/index.tsx b/airbyte-webapp/src/components/ToolTip/index.tsx deleted file mode 100644 index 5940d28115417..0000000000000 --- a/airbyte-webapp/src/components/ToolTip/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import InfoIcon from "./components/InfoIcon"; -import ToolTip from "./ToolTip"; - -export default ToolTip; -export { ToolTip, InfoIcon }; diff --git a/airbyte-webapp/src/components/ToolTip/types.ts b/airbyte-webapp/src/components/ToolTip/types.ts new file mode 100644 index 0000000000000..579dbf3a0c905 --- /dev/null +++ b/airbyte-webapp/src/components/ToolTip/types.ts @@ -0,0 +1,15 @@ +import { Placement } from "@floating-ui/react-dom"; + +export type ToolTipCursor = "pointer" | "help" | "not-allowed" | "initial"; +export type ToolTipTheme = "dark" | "light"; + +export interface ToolTipProps { + control: React.ReactNode; + className?: string; + disabled?: boolean; + cursor?: ToolTipCursor; + theme?: ToolTipTheme; + placement?: Placement; +} + +export type TooltipContext = ToolTipProps; diff --git a/airbyte-webapp/src/components/ToolTip/components/InfoIcon.tsx b/airbyte-webapp/src/components/icons/InfoIcon.tsx similarity index 88% rename from airbyte-webapp/src/components/ToolTip/components/InfoIcon.tsx rename to airbyte-webapp/src/components/icons/InfoIcon.tsx index 556f3ad5ce07a..b0ecc4294f754 100644 --- a/airbyte-webapp/src/components/ToolTip/components/InfoIcon.tsx +++ b/airbyte-webapp/src/components/icons/InfoIcon.tsx @@ -1,4 +1,4 @@ -const InfoIcon = ({ color = "currentColor" }: { color?: string }): JSX.Element => ( +export const InfoIcon = ({ color = "currentColor" }: { color?: string }): JSX.Element => ( ); - -export default InfoIcon; diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index 826ce956fb4ef..1d2801a13284f 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -532,6 +532,7 @@ "ui.keyValuePair": "{key}: {value}", "ui.keyValuePairV2": "{key} ({value})", "ui.keyValuePairV3": "{key}, {value}", + "ui.learnMore": "Learn more", "airbyte.datatype.string": "String", "airbyte.datatype.date": "Date", diff --git a/airbyte-webapp/src/packages/cloud/views/users/UsersSettingsView/components/InfoIcon.tsx b/airbyte-webapp/src/packages/cloud/views/users/UsersSettingsView/components/InfoIcon.tsx deleted file mode 100644 index 556f3ad5ce07a..0000000000000 --- a/airbyte-webapp/src/packages/cloud/views/users/UsersSettingsView/components/InfoIcon.tsx +++ /dev/null @@ -1,10 +0,0 @@ -const InfoIcon = ({ color = "currentColor" }: { color?: string }): JSX.Element => ( - - - -); - -export default InfoIcon; diff --git a/airbyte-webapp/src/packages/cloud/views/users/UsersSettingsView/components/RoleToolTip.tsx b/airbyte-webapp/src/packages/cloud/views/users/UsersSettingsView/components/RoleToolTip.tsx index 3c08916eeae78..2b875e502fac0 100644 --- a/airbyte-webapp/src/packages/cloud/views/users/UsersSettingsView/components/RoleToolTip.tsx +++ b/airbyte-webapp/src/packages/cloud/views/users/UsersSettingsView/components/RoleToolTip.tsx @@ -2,10 +2,9 @@ import React from "react"; import { FormattedMessage } from "react-intl"; import styled from "styled-components"; +import { InfoIcon } from "components/icons/InfoIcon"; import ToolTip from "components/ToolTip"; -import InfoIcon from "./InfoIcon"; - const Info = styled.div` margin-left: 7px; vertical-align: middle; diff --git a/airbyte-webapp/src/scss/_z-indices.scss b/airbyte-webapp/src/scss/_z-indices.scss new file mode 100644 index 0000000000000..dfbe539c25185 --- /dev/null +++ b/airbyte-webapp/src/scss/_z-indices.scss @@ -0,0 +1,2 @@ +$sidebar: 9999; +$tooltip: 9999 + 1; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/InformationToolTip.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/InformationToolTip.tsx index 8f8790f8f7eb9..01618155ba5e8 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/InformationToolTip.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/InformationToolTip.tsx @@ -1,18 +1,8 @@ import React from "react"; import styled from "styled-components"; -import ToolTip, { InfoIcon } from "components/ToolTip"; - -const ToolTipBlock = styled(ToolTip)` - top: calc(100%); - background: ${({ theme }) => theme.darkBlue90}; - color: ${({ theme }) => theme.whiteColor}; - padding: 11px 19px; - min-width: 250px; - font-size: 11px; - line-height: 16px; - font-weight: 500; -`; +import { InfoIcon } from "components/icons/InfoIcon"; +import ToolTip from "components/ToolTip"; const Info = styled.div` display: inline-block; @@ -23,7 +13,7 @@ const Info = styled.div` const InformationToolTip: React.FC = ({ children }) => { return ( - @@ -31,7 +21,7 @@ const InformationToolTip: React.FC = ({ children }) => { } > {children} - +
); }; diff --git a/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx b/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx index 774648e3380b4..c1ac58655d659 100644 --- a/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx +++ b/airbyte-webapp/src/views/Connection/ConnectionForm/components/SyncCatalogField.tsx @@ -7,6 +7,7 @@ import styled from "styled-components"; import { CheckBox, H5 } from "components"; import { LoadingBackdrop } from "components/LoadingBackdrop"; import { Cell, Header } from "components/SimpleTableComponents"; +import { TooltipLearnMoreLink } from "components/ToolTip/TooltipLearnMoreLink"; import { useConfig } from "config"; import { SyncSchemaStream } from "core/domain/catalog"; @@ -56,18 +57,6 @@ const NextLineText = styled.div` margin-top: 10px; `; -const LearnMoreLink = styled.a` - opacity: 0.6; - display: block; - margin-top: 10px; - color: ${({ theme }) => theme.whiteColor}; - text-decoration: none; - - &:hover { - opacity: 0.9; - } -`; - interface SchemaViewProps extends FieldProps { additionalControl?: React.ReactNode; destinationSupportedSyncModes: DestinationSyncMode[]; @@ -109,9 +98,7 @@ const CatalogHeader: React.FC<{ mode?: ConnectionFormMode }> = ({ mode }) => { - - - +