diff --git a/.storybook/main.js b/.storybook/main.js index 2211f3f0f8745ae6448c3143f0f2d89077fae009..18be3bbfa97100ea219d993afa47f8a0a84b2559 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -5,8 +5,8 @@ module.exports = { builder: "webpack5", }, stories: [ - "../frontend/**/*.stories.mdx", - "../frontend/**/*.stories.@(js|jsx|ts|tsx)", + "../(frontend|enterprise)/**/*.stories.mdx", + "../(frontend|enterprise)/**/*.stories.@(js|jsx|ts|tsx)", ], addons: ["@storybook/addon-essentials", "@storybook/addon-links"], webpackFinal: storybookConfig => ({ diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/BrandColorSettings/BrandColorSettings.styled.tsx b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/BrandColorSettings/BrandColorSettings.styled.tsx new file mode 100644 index 0000000000000000000000000000000000000000..85af4bb0cc7c3174e6b2e51d7f427bff1e22ec45 --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/BrandColorSettings/BrandColorSettings.styled.tsx @@ -0,0 +1,58 @@ +import styled from "@emotion/styled"; +import { css } from "@emotion/react"; +import { color } from "metabase/lib/colors"; + +const cellStyles = css` + padding-left: 1.5rem; + padding-right: 1.5rem; + + &:first-of-type { + flex: 0 0 auto; + width: 12rem; + } +`; + +export const TableHeader = styled.div` + border: 1px solid ${color("border")}; + border-bottom: none; + border-radius: 0.5rem 0.5rem 0 0; + background-color: ${color("bg-light")}; +`; + +export const TableHeaderRow = styled.div` + display: flex; + align-items: center; +`; + +export const TableHeaderCell = styled.div` + ${cellStyles}; + color: ${color("text-medium")}; + font-size: 0.5rem; + line-height: 0.625rem; + font-weight: bold; + text-transform: uppercase; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +`; + +export const TableBody = styled.div` + border: 1px solid ${color("border")}; + border-top: none; + border-radius: 0 0 0.5rem 0.5rem; +`; + +export const TableBodyRow = styled.div` + display: flex; + align-items: center; + + &:not(:first-of-type) { + border-top: 1px solid ${color("border")}; + } +`; + +export const TableBodyCell = styled.div` + ${cellStyles}; + color: ${color("text-medium")}; + padding-top: 1rem; + padding-bottom: 1rem; +`; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/BrandColorSettings/BrandColorSettings.tsx b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/BrandColorSettings/BrandColorSettings.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cfeedf632b470d9281d75858e2880735d9e42b8b --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/BrandColorSettings/BrandColorSettings.tsx @@ -0,0 +1,127 @@ +import React, { memo, useCallback, useMemo, useRef } from "react"; +import { t } from "ttag"; +import { set, omit } from "lodash"; +import ColorPicker from "metabase/core/components/ColorPicker"; +import { getBrandColorOptions } from "./utils"; +import { ColorOption } from "./types"; +import { + TableBody, + TableBodyCell, + TableBodyRow, + TableHeader, + TableHeaderCell, + TableHeaderRow, +} from "./BrandColorSettings.styled"; + +export interface BrandColorSettingsProps { + colors: Record<string, string>; + originalColors: Record<string, string>; + onChange?: (colors: Record<string, string>) => void; +} + +const BrandColorSettings = ({ + colors, + originalColors, + onChange, +}: BrandColorSettingsProps): JSX.Element => { + const colorsRef = useRef(colors); + colorsRef.current = colors; + + const options = useMemo(() => { + return getBrandColorOptions(); + }, []); + + const handleChange = useCallback( + (colorName: string, color?: string) => { + if (color) { + onChange?.(set({ ...colorsRef.current }, colorName, color)); + } else { + onChange?.(omit({ ...colorsRef.current }, colorName)); + } + }, + [onChange], + ); + + return ( + <BrandColorTable + colors={colors} + originalColors={originalColors} + options={options} + onChange={handleChange} + /> + ); +}; + +interface BrandColorTableProps { + colors: Record<string, string>; + originalColors: Record<string, string>; + options: ColorOption[]; + onChange: (colorName: string, color?: string) => void; +} + +const BrandColorTable = ({ + colors, + originalColors, + options, + onChange, +}: BrandColorTableProps): JSX.Element => { + return ( + <div> + <TableHeader> + <TableHeaderRow> + <TableHeaderCell>{t`Color`}</TableHeaderCell> + <TableHeaderCell>{t`Where it's used`}</TableHeaderCell> + </TableHeaderRow> + </TableHeader> + <TableBody> + {options.map(option => ( + <BrandColorRow + key={option.name} + color={colors[option.name]} + originalColor={originalColors[option.name]} + option={option} + onChange={onChange} + /> + ))} + </TableBody> + </div> + ); +}; + +interface BrandColorRowProps { + color?: string; + originalColor: string; + option: ColorOption; + onChange: (colorName: string, color?: string) => void; +} + +const BrandColorRow = memo(function BrandColorRow({ + color, + originalColor, + option, + onChange, +}: BrandColorRowProps) { + const handleChange = useCallback( + (color?: string) => { + onChange(option.name, color); + }, + [option, onChange], + ); + + return ( + <TableBodyRow> + <TableBodyCell> + <ColorPicker + color={color ?? originalColor} + isBordered + isSelected + isDefault={color == null || color === originalColor} + onChange={handleChange} + /> + </TableBodyCell> + <TableBodyCell>{option.description}</TableBodyCell> + </TableBodyRow> + ); +}); + +export default BrandColorSettings; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/BrandColorSettings/index.ts b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/BrandColorSettings/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..00d24fd47e772ba6c5d5b9e47abafa4d13836ba7 --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/BrandColorSettings/index.ts @@ -0,0 +1 @@ +export { default } from "./BrandColorSettings"; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/BrandColorSettings/types.ts b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/BrandColorSettings/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5d0ad04d13b682b2bccaee7b3364424701b3acf --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/BrandColorSettings/types.ts @@ -0,0 +1,4 @@ +export interface ColorOption { + name: string; + description: string; +} diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/BrandColorSettings/utils.ts b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/BrandColorSettings/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..40b1fcde2fb72ca6adb7046ec6817d2cc2a2be2f --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/BrandColorSettings/utils.ts @@ -0,0 +1,17 @@ +import { t } from "ttag"; +import { ColorOption } from "./types"; + +export const getBrandColorOptions = (): ColorOption[] => [ + { + name: "brand", + description: t`The main color used throughout the app for buttons, links, and the default chart color.`, + }, + { + name: "accent1", + description: t`The color of aggregations and breakouts in the graphical query builder.`, + }, + { + name: "accent7", + description: t`Color of filters in the query builder, buttons and links in filter widgets.`, + }, +]; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorPreview/ChartColorPreview.styled.tsx b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorPreview/ChartColorPreview.styled.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b5791b83ec7e52bfdb982ec6fe08bec576a82ac3 --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorPreview/ChartColorPreview.styled.tsx @@ -0,0 +1,30 @@ +import styled from "@emotion/styled"; +import { color } from "metabase/lib/colors"; + +export const TableRoot = styled.div` + display: flex; + flex-direction: column; + flex: 1 1 auto; +`; + +export const TableHeader = styled.div` + display: block; + padding: 1rem 1.5rem; + border: 1px solid ${color("border")}; + border-left: none; + border-top-right-radius: 0.5rem; +`; + +export const TableTitle = styled.div` + color: ${color("text-dark")}; + font-size: 1rem; + font-weight: bold; +`; + +export const TableBody = styled.div` + flex: 1 1 auto; + border: 1px solid ${color("border")}; + border-top: none; + border-left: none; + border-bottom-right-radius: 0.5rem; +`; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorPreview/ChartColorPreview.tsx b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorPreview/ChartColorPreview.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e1c51e0fe46b1655318f5a9768da9cdd83121e99 --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorPreview/ChartColorPreview.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { t } from "ttag"; +import { + TableBody, + TableHeader, + TableRoot, + TableTitle, +} from "./ChartColorPreview.styled"; + +const ChartColorPreview = (): JSX.Element => { + return ( + <TableRoot> + <TableHeader> + <TableTitle>{t`Palette preview`}</TableTitle> + </TableHeader> + <TableBody /> + </TableRoot> + ); +}; + +export default ChartColorPreview; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorPreview/index.ts b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorPreview/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..a5e162cb35af7a188dd0be10a757de2de5aed1a3 --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorPreview/index.ts @@ -0,0 +1 @@ +export { default } from "./ChartColorPreview"; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorSettings/ChartColorSettings.styled.tsx b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorSettings/ChartColorSettings.styled.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0b449ecddc736bc1de2907d1f5a5974719028d76 --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorSettings/ChartColorSettings.styled.tsx @@ -0,0 +1,54 @@ +import styled from "@emotion/styled"; +import { color, darken } from "metabase/lib/colors"; + +export const TableHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.5rem; + border: 1px solid ${color("border")}; + border-top-left-radius: 0.5rem; +`; + +export const TableTitle = styled.div` + color: ${color("text-dark")}; + font-size: 1rem; + font-weight: bold; +`; + +export const TableLink = styled.div` + display: inline-block; + color: ${color("brand")}; + font-weight: bold; + cursor: pointer; + + &:hover { + color: ${darken("brand", 0.12)}; + } +`; + +export const TableBody = styled.div` + border: 1px solid ${color("border")}; + border-top: none; + border-bottom-left-radius: 0.5rem; +`; + +export const TableBodyRow = styled.div` + display: flex; + align-items: center; + + &:not(:first-of-type) { + border-top: 1px solid ${color("border")}; + } +`; + +export const TableBodyCell = styled.div` + flex: 0 0 auto; + width: 12rem; + padding: 1rem 1.5rem; + + &:not(:first-of-type) { + border-left: 1px solid ${color("border")}; + background-color: ${color("bg-light")}; + } +`; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorSettings/ChartColorSettings.tsx b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorSettings/ChartColorSettings.tsx new file mode 100644 index 0000000000000000000000000000000000000000..93fbc7a941fdbf9a21b570633980fecda095ddeb --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorSettings/ChartColorSettings.tsx @@ -0,0 +1,132 @@ +import React, { memo, useCallback, useMemo, useRef } from "react"; +import { t } from "ttag"; +import { flatten, omit, set } from "lodash"; +import ColorPicker from "metabase/core/components/ColorPicker"; +import { getChartColorGroups } from "./utils"; +import { + TableBody, + TableBodyCell, + TableBodyRow, + TableHeader, + TableLink, + TableTitle, +} from "./ChartColorSettings.styled"; + +export interface ChartColorSettingsProps { + colors: Record<string, string>; + originalColors: Record<string, string>; + onChange?: (colors: Record<string, string>) => void; +} + +const ChartColorSettings = ({ + colors, + originalColors, + onChange, +}: ChartColorSettingsProps): JSX.Element => { + const colorsRef = useRef(colors); + colorsRef.current = colors; + + const colorGroups = useMemo(() => { + return getChartColorGroups(); + }, []); + + const handleChange = useCallback( + (colorName: string, color?: string) => { + if (color) { + onChange?.(set({ ...colorsRef.current }, colorName, color)); + } else { + onChange?.(omit({ ...colorsRef.current }, colorName)); + } + }, + [onChange], + ); + + const handleReset = useCallback(() => { + onChange?.(omit({ ...colorsRef.current }, flatten(colorGroups))); + }, [colorGroups, onChange]); + + return ( + <ChartColorTable + colors={colors} + originalColors={originalColors} + colorGroups={colorGroups} + onChange={handleChange} + onReset={handleReset} + /> + ); +}; + +interface ChartColorTable { + colors: Record<string, string>; + originalColors: Record<string, string>; + colorGroups: string[][]; + onChange: (name: string, color?: string) => void; + onReset: () => void; +} + +const ChartColorTable = ({ + colors, + originalColors, + colorGroups, + onChange, + onReset, +}: ChartColorTable): JSX.Element => { + return ( + <div> + <TableHeader> + <TableTitle>{t`Chart colors`}</TableTitle> + <TableLink onClick={onReset}>{t`Reset to default colors`}</TableLink> + </TableHeader> + <TableBody> + {colorGroups.map((colorGroup, index) => ( + <TableBodyRow key={index}> + {colorGroup.map(colorName => ( + <ChartColorCell + key={colorName} + color={colors[colorName]} + originalColor={originalColors[colorName]} + colorName={colorName} + onChange={onChange} + /> + ))} + </TableBodyRow> + ))} + </TableBody> + </div> + ); +}; + +interface ChartColorCellProps { + color?: string; + originalColor: string; + colorName: string; + onChange: (colorName: string, color?: string) => void; +} + +const ChartColorCell = memo(function ChartColorCell({ + color, + originalColor, + colorName, + onChange, +}: ChartColorCellProps) { + const handleChange = useCallback( + (color?: string) => { + onChange(colorName, color); + }, + [colorName, onChange], + ); + + return ( + <TableBodyCell> + <ColorPicker + color={color ?? originalColor} + isBordered + isSelected + isDefault={color == null || color === originalColor} + onChange={handleChange} + /> + </TableBodyCell> + ); +}); + +export default ChartColorSettings; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorSettings/index.ts b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorSettings/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc52d9c0a16722fbfef1ce03ba5b2c7b8265acfa --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorSettings/index.ts @@ -0,0 +1 @@ +export { default } from "./ChartColorSettings"; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorSettings/utils.ts b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorSettings/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a2761a96b37671152fabcfe04d8ae75eb3ec77f --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ChartColorSettings/utils.ts @@ -0,0 +1,7 @@ +export const getChartColorGroups = (): string[][] => [ + ["accent2", "accent2", "accent2"], + ["accent3", "accent3", "accent3"], + ["accent4", "accent4", "accent4"], + ["accent5", "accent5", "accent5"], + ["accent6", "accent6", "accent6"], +]; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSchemeWidget.jsx b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSchemeWidget.jsx deleted file mode 100644 index 1e6d343e5487db1f2277adb54648acddfa4f27d8..0000000000000000000000000000000000000000 --- a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSchemeWidget.jsx +++ /dev/null @@ -1,93 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from "react"; - -import { t } from "ttag"; - -import ColorPicker from "metabase/components/ColorPicker"; -import Icon from "metabase/components/Icon"; -import { humanize } from "metabase/lib/formatting"; - -import { originalColors } from "../lib/whitelabel"; - -const THEMEABLE_COLORS = [ - "brand", - ...Object.keys(originalColors).filter(name => name.startsWith("accent")), -]; - -const getColorDisplayProperties = () => ({ - brand: { - name: t`Primary color`, - description: t`The main color used throughout the app for buttons, links, and the default chart color.`, - }, - accent1: { - name: t`Accent 1`, - description: t`The color of aggregations and breakouts in the graphical query builder.`, - }, - accent2: { - name: t`Accent 2`, - description: t`The color of filters in the query builder and buttons and links in filter widgets.`, - }, - accent3: { - name: t`Additional chart color`, - }, - accent4: { - name: t`Additional chart color`, - }, - accent5: { - name: t`Additional chart color`, - }, - accent6: { - name: t`Additional chart color`, - }, - accent7: { - name: t`Additional chart color`, - }, -}); - -const ColorSchemeWidget = ({ setting, onChange }) => { - const value = setting.value || {}; - const colors = { ...originalColors, ...value }; - - return ( - <div> - <table> - <tbody> - {THEMEABLE_COLORS.map(name => { - const properties = getColorDisplayProperties()[name] || {}; - return ( - <tr key={name}> - <td>{properties.name || humanize(name)}:</td> - <td> - <span className="mx1"> - <ColorPicker - fancy - triggerSize={16} - value={colors[name]} - onChange={color => onChange({ ...value, [name]: color })} - /> - </span> - </td> - <td> - {colors[name] !== originalColors[name] && ( - <Icon - name="close" - className="text-grey-2 text-grey-4-hover cursor-pointer" - onClick={() => onChange({ ...value, [name]: undefined })} - /> - )} - </td> - <td> - <span className="mx2 text-grey-4"> - {properties.description} - </span> - </td> - </tr> - ); - })} - </tbody> - </table> - </div> - ); -}; - -export default ColorSchemeWidget; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettings/ColorSettings.stories.tsx b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettings/ColorSettings.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a79d8c98833a9578e0be37cd4ddec82d51e4251f --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettings/ColorSettings.stories.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { ComponentStory } from "@storybook/react"; +import { color } from "metabase/lib/colors"; +import ColorSettings from "./ColorSettings"; + +export default { + title: "Whitelabel/ColorSettings", + component: ColorSettings, +}; + +const Template: ComponentStory<typeof ColorSettings> = args => { + return <ColorSettings {...args} />; +}; + +export const Default = Template.bind({}); +Default.args = { + initialColors: { + brand: color("brand"), + }, + originalColors: { + brand: color("brand"), + accent1: color("accent1"), + accent7: color("accent7"), + }, +}; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettings/ColorSettings.styled.tsx b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettings/ColorSettings.styled.tsx new file mode 100644 index 0000000000000000000000000000000000000000..94f0753668a24148dd5c90366f020d3e20db5161 --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettings/ColorSettings.styled.tsx @@ -0,0 +1,24 @@ +import styled from "@emotion/styled"; +import { color } from "metabase/lib/colors"; + +export const SettingRoot = styled.div` + flex: 1 1 auto; +`; + +export const SettingTitle = styled.div` + color: ${color("text-medium")}; + font-weight: bold; + margin-bottom: 1rem; +`; + +export const SectionContent = styled.div` + display: flex; +`; + +export const BrandColorSection = styled.div` + margin-top: 1rem; +`; + +export const ChartColorSection = styled.div` + margin-top: 2rem; +`; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettings/ColorSettings.tsx b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettings/ColorSettings.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f1a49f1130da8656ad954feed47a017074d77e82 --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettings/ColorSettings.tsx @@ -0,0 +1,60 @@ +import React, { useCallback, useState } from "react"; +import { t } from "ttag"; +import BrandColorSettings from "../BrandColorSettings"; +import ChartColorSettings from "../ChartColorSettings"; +import ChartColorPreview from "../ChartColorPreview"; +import { + BrandColorSection, + ChartColorSection, + SectionContent, + SettingRoot, + SettingTitle, +} from "./ColorSettings.styled"; + +export interface ColorSettingsProps { + initialColors: Record<string, string> | null; + originalColors: Record<string, string>; + onChange?: (colors: Record<string, string>) => void; +} + +const ColorSettings = ({ + initialColors, + originalColors, + onChange, +}: ColorSettingsProps): JSX.Element => { + const [colors, setColors] = useState(initialColors ?? {}); + + const handleChange = useCallback( + (colors: Record<string, string>) => { + setColors(colors); + onChange?.(colors); + }, + [onChange], + ); + + return ( + <SettingRoot> + <BrandColorSection> + <SettingTitle>{t`User interface colors`}</SettingTitle> + <BrandColorSettings + colors={colors} + originalColors={originalColors} + onChange={handleChange} + /> + </BrandColorSection> + <ChartColorSection> + <SettingTitle>{t`Chart colors`}</SettingTitle> + <SectionContent> + <ChartColorSettings + colors={colors} + originalColors={originalColors} + onChange={handleChange} + /> + <ChartColorPreview /> + </SectionContent> + </ChartColorSection> + </SettingRoot> + ); +}; + +export default ColorSettings; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettings/index.ts b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettings/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5892834370bbc88eed892085c521d900fbb6eea8 --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettings/index.ts @@ -0,0 +1 @@ +export { default } from "./ColorSettings"; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettingsWidget/ColorSettingsWidget.tsx b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettingsWidget/ColorSettingsWidget.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1be0e22045c74de5040c707547851015fa4142eb --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettingsWidget/ColorSettingsWidget.tsx @@ -0,0 +1,40 @@ +import React, { useCallback, useMemo, useRef } from "react"; +import { debounce } from "lodash"; +import { originalColors } from "../../lib/whitelabel"; +import ColorSettings from "../ColorSettings"; +import { ColorSetting } from "./types"; + +export interface ColorSettingsWidget { + setting: ColorSetting; + onChange: (value: Record<string, string>) => void; +} + +const ColorSettingsWidget = ({ + setting, + onChange, +}: ColorSettingsWidget): JSX.Element => { + const onChangeDebounced = useDebounce(onChange, 400); + + return ( + <ColorSettings + initialColors={setting.value} + originalColors={originalColors} + onChange={onChangeDebounced} + /> + ); +}; + +const useDebounce = function<T>(func: (value: T) => void, wait: number) { + const ref = useRef(func); + ref.current = func; + + const callback = useCallback((value: T) => { + return ref.current?.(value); + }, []); + + return useMemo(() => { + return debounce(callback, wait); + }, [callback, wait]); +}; + +export default ColorSettingsWidget; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettingsWidget/index.ts b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettingsWidget/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..adb87563a1797d306f53ac333cab932aed31d79d --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettingsWidget/index.ts @@ -0,0 +1 @@ +export { default } from "./ColorSettingsWidget"; diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettingsWidget/types.ts b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettingsWidget/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a2358a9710913f90d223ae884082a1fdd5d5c65 --- /dev/null +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/components/ColorSettingsWidget/types.ts @@ -0,0 +1,3 @@ +export interface ColorSetting { + value: Record<string, string> | null; +} diff --git a/enterprise/frontend/src/metabase-enterprise/whitelabel/index.js b/enterprise/frontend/src/metabase-enterprise/whitelabel/index.js index cbc2c24337c31388be3cdcdd7f040382c1ce0115..811290759a595a4193e182f004201ecd47e777fb 100644 --- a/enterprise/frontend/src/metabase-enterprise/whitelabel/index.js +++ b/enterprise/frontend/src/metabase-enterprise/whitelabel/index.js @@ -16,7 +16,7 @@ import { } from "metabase-enterprise/settings/selectors"; import MetabaseSettings from "metabase/lib/settings"; -import ColorSchemeWidget from "./components/ColorSchemeWidget"; +import ColorSettingsWidget from "./components/ColorSettingsWidget"; import LogoUpload from "./components/LogoUpload"; import LogoIcon from "./components/LogoIcon"; import { @@ -53,7 +53,7 @@ if (hasPremiumFeature("whitelabel")) { { key: "application-colors", display_name: t`Color Palette`, - widget: ColorSchemeWidget, + widget: ColorSettingsWidget, }, { key: "application-logo-url", diff --git a/frontend/src/metabase/core/components/ColorPicker/ColorPicker.stories.tsx b/frontend/src/metabase/core/components/ColorPicker/ColorPicker.stories.tsx index 6fb56139a2e6be8364f1186eea2ed1117c6396e1..f5e0cdaf37d366dffb69b12740c579ca6bda9865 100644 --- a/frontend/src/metabase/core/components/ColorPicker/ColorPicker.stories.tsx +++ b/frontend/src/metabase/core/components/ColorPicker/ColorPicker.stories.tsx @@ -12,7 +12,7 @@ export default { const Template: ComponentStory<typeof ColorPicker> = args => { const [{ color }, updateArgs] = useArgs(); - const handleChange = (color: string) => { + const handleChange = (color?: string) => { updateArgs({ color }); }; diff --git a/frontend/src/metabase/core/components/ColorPicker/ColorPicker.tsx b/frontend/src/metabase/core/components/ColorPicker/ColorPicker.tsx index 1ac0bf6c18d54dd3dad67a309000e25f7de24c70..49fb2f751e5a82f5c11b6eb5fb0f681af8428c9d 100644 --- a/frontend/src/metabase/core/components/ColorPicker/ColorPicker.tsx +++ b/frontend/src/metabase/core/components/ColorPicker/ColorPicker.tsx @@ -13,8 +13,8 @@ export interface ColorPickerProps extends ColorPickerAttributes { placeholder?: string; isBordered?: boolean; isSelected?: boolean; - isGenerated?: boolean; - onChange?: (color: string) => void; + isDefault?: boolean; + onChange?: (color?: string) => void; } const ColorPicker = forwardRef(function ColorPicker( @@ -23,7 +23,7 @@ const ColorPicker = forwardRef(function ColorPicker( placeholder, isBordered, isSelected, - isGenerated, + isDefault, onChange, ...props }: ColorPickerProps, @@ -40,7 +40,7 @@ const ColorPicker = forwardRef(function ColorPicker( placeholder={placeholder} isBordered={isBordered} isSelected={isSelected} - isGenerated={isGenerated} + isDefault={isDefault} onClick={onClick} onChange={onChange} /> diff --git a/frontend/src/metabase/core/components/ColorPicker/ColorPicker.unit.spec.tsx b/frontend/src/metabase/core/components/ColorPicker/ColorPicker.unit.spec.tsx index f52e6094beb6e719c34095cb3aca855a3c14487f..f60d747fea34adcc06f45c583f7ca6d20d97dac6 100644 --- a/frontend/src/metabase/core/components/ColorPicker/ColorPicker.unit.spec.tsx +++ b/frontend/src/metabase/core/components/ColorPicker/ColorPicker.unit.spec.tsx @@ -6,7 +6,9 @@ import ColorPicker from "./ColorPicker"; const TestColorPicker = () => { const [color, setColor] = useState("white"); - return <ColorPicker color={color} onChange={setColor} />; + const handleChange = (color?: string) => setColor(color ?? "white"); + + return <ColorPicker color={color} onChange={handleChange} />; }; describe("ColorPicker", () => { diff --git a/frontend/src/metabase/core/components/ColorPicker/ColorPickerContent.tsx b/frontend/src/metabase/core/components/ColorPicker/ColorPickerContent.tsx index 4b7adcce72ab7c03672ad742421c2d68e578bdf2..d540e9e43991e0d25f1d18bfc6711bb531edf48b 100644 --- a/frontend/src/metabase/core/components/ColorPicker/ColorPickerContent.tsx +++ b/frontend/src/metabase/core/components/ColorPicker/ColorPickerContent.tsx @@ -11,27 +11,22 @@ export type ColorPickerContentAttributes = Omit< export interface ColorPickerContentProps extends ColorPickerContentAttributes { color?: string; - onChange?: (color: string) => void; + onChange?: (color?: string) => void; } const ColorPickerContent = forwardRef(function ColorPickerContent( { color, onChange, ...props }: ColorPickerContentProps, ref: Ref<HTMLDivElement>, ) { - const handleColorChange = useCallback( - (color?: string) => color && onChange?.(color), - [onChange], - ); - - const handleColorStateChange = useCallback( + const handleChange = useCallback( (state: ColorState) => onChange?.(state.hex), [onChange], ); return ( <ContentContainer {...props} ref={ref}> - <ColorPickerControls color={color} onChange={handleColorStateChange} /> - <ColorInput color={color} fullWidth onChange={handleColorChange} /> + <ColorPickerControls color={color} onChange={handleChange} /> + <ColorInput color={color} fullWidth onChange={onChange} /> </ContentContainer> ); }); diff --git a/frontend/src/metabase/core/components/ColorPicker/ColorPickerTrigger.tsx b/frontend/src/metabase/core/components/ColorPicker/ColorPickerTrigger.tsx index d27b72c6703ca9a20560dde26aad278df8badd39..d1fe8b29d5686e05a9c89e5ed6fbe4686d1d7479 100644 --- a/frontend/src/metabase/core/components/ColorPicker/ColorPickerTrigger.tsx +++ b/frontend/src/metabase/core/components/ColorPicker/ColorPickerTrigger.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, HTMLAttributes, Ref, useCallback } from "react"; +import React, { forwardRef, HTMLAttributes, Ref } from "react"; import ColorPill from "metabase/core/components/ColorPill"; import ColorInput from "metabase/core/components/ColorInput"; import { TriggerContainer } from "./ColorPicker.styled"; @@ -9,8 +9,8 @@ export interface ColorPickerTriggerProps placeholder?: string; isBordered?: boolean; isSelected?: boolean; - isGenerated?: boolean; - onChange?: (color: string) => void; + isDefault?: boolean; + onChange?: (color?: string) => void; } const ColorPickerTrigger = forwardRef(function ColorPickerTrigger( @@ -19,31 +19,27 @@ const ColorPickerTrigger = forwardRef(function ColorPickerTrigger( placeholder, isBordered, isSelected, - isGenerated, + isDefault, onClick, onChange, ...props }: ColorPickerTriggerProps, ref: Ref<HTMLDivElement>, ) { - const handleChange = useCallback( - (color?: string) => color && onChange?.(color), - [onChange], - ); - return ( <TriggerContainer {...props} ref={ref}> <ColorPill color={color} isBordered={isBordered} isSelected={isSelected} - isGenerated={isGenerated} + isDefault={isDefault} onClick={onClick} /> <ColorInput color={color} placeholder={placeholder} - onChange={handleChange} + fullWidth + onChange={onChange} /> </TriggerContainer> ); diff --git a/frontend/src/metabase/core/components/ColorPill/ColorPill.styled.tsx b/frontend/src/metabase/core/components/ColorPill/ColorPill.styled.tsx index e9d3926e31db5bab5ea56d580be4673efd590801..df81ef186936785a6072a8ea2094a833ef077c08 100644 --- a/frontend/src/metabase/core/components/ColorPill/ColorPill.styled.tsx +++ b/frontend/src/metabase/core/components/ColorPill/ColorPill.styled.tsx @@ -4,7 +4,7 @@ import { color } from "metabase/lib/colors"; export interface ColorPillRootProps { isBordered?: boolean; isSelected?: boolean; - isGenerated?: boolean; + isDefault?: boolean; } export const ColorPillRoot = styled.div<ColorPillRootProps>` @@ -14,8 +14,8 @@ export const ColorPillRoot = styled.div<ColorPillRootProps>` padding: ${props => props.isBordered && "0.1875rem"}; border-width: ${props => (props.isBordered ? "0.0625rem" : "0")}; border-color: ${props => - props.isSelected ? color("border") : "transparent"}; - border-style: ${props => (props.isGenerated ? "dashed" : "solid")}; + props.isSelected ? color("text-light") : "transparent"}; + border-style: ${props => (props.isDefault ? "dashed" : "solid")}; border-radius: 50%; cursor: pointer; `; diff --git a/frontend/src/metabase/core/components/ColorPill/ColorPill.tsx b/frontend/src/metabase/core/components/ColorPill/ColorPill.tsx index c61ca1915264a8b7ba4aa790a875fd87c6d56228..e9599ef179547b691d5068aabfc41799244afd30 100644 --- a/frontend/src/metabase/core/components/ColorPill/ColorPill.tsx +++ b/frontend/src/metabase/core/components/ColorPill/ColorPill.tsx @@ -5,7 +5,7 @@ export interface ColorPillProps extends HTMLAttributes<HTMLDivElement> { color: string; isBordered?: boolean; isSelected?: boolean; - isGenerated?: boolean; + isDefault?: boolean; } const ColorPill = forwardRef(function ColorPill( @@ -13,7 +13,7 @@ const ColorPill = forwardRef(function ColorPill( color, isBordered, isSelected, - isGenerated, + isDefault, "aria-label": ariaLabel = color, ...props }: ColorPillProps, @@ -25,7 +25,7 @@ const ColorPill = forwardRef(function ColorPill( ref={ref} isBordered={isBordered} isSelected={isSelected} - isGenerated={isGenerated} + isDefault={isDefault} aria-label={ariaLabel} > <ColorPillContent diff --git a/frontend/test/metabase/scenarios/admin/settings/whitelabel-color-theme.cy.spec.js b/frontend/test/metabase/scenarios/admin/settings/whitelabel-color-theme.cy.spec.js deleted file mode 100644 index fe8ab6791fe676751becaad52d862ccffb7f224f..0000000000000000000000000000000000000000 --- a/frontend/test/metabase/scenarios/admin/settings/whitelabel-color-theme.cy.spec.js +++ /dev/null @@ -1,56 +0,0 @@ -import { restore, describeEE } from "__support__/e2e/cypress"; - -// Define colors that we use for whitelabeling -// If rbg values exist, it's because we explicit test those -const colors = { - primary: { hex: "8B572A", rgb: [139, 87, 42] }, - nav: { hex: "284E07", rgb: [40, 78, 7] }, - accent1: { hex: "417505" }, - accent2: { hex: "7ED321" }, - additional1: { hex: "B8E986" }, - additional2: { hex: "50E3C2" }, - additional3: { hex: "4A90E2" }, - additional4: { hex: "082CBE" }, - additional5: { hex: "F8E71C", rgb: [248, 231, 28] }, -}; - -function changeThemeColor(location, colorhex) { - cy.get("td") - .eq(location) - .click(); - cy.get(`div[title='#${colorhex}']`).click(); - cy.findByText("Done").click(); -} - -describeEE("formatting > whitelabel > color theme", () => { - beforeEach(() => { - restore(); - cy.signInAsAdmin(); - }); - - it("should change the brand color", () => { - cy.visit("/admin/settings/whitelabel"); - cy.intercept("GET", `/api/setting`).as("setting"); - cy.intercept("GET", "/api/session/properties").as("sessionProperties"); - - // brand color - changeThemeColor(1, colors.primary.hex); - cy.wait("@setting"); - - cy.wait("@sessionProperties").then(xhr => { - console.log(xhr.response.body); - console.log(xhr.response.body["application-colors"]); - }); - - cy.signOut(); - - cy.visit("/"); - - // "Remember me" checkbox has to have the branded background color - cy.get('div span[size="16"]').should( - "have.css", - "background-color", - `rgb(${colors.primary.rgb.join(", ")})`, - ); - }); -}); diff --git a/frontend/test/metabase/scenarios/admin/settings/whitelabel.cy.spec.js b/frontend/test/metabase/scenarios/admin/settings/whitelabel.cy.spec.js index 277736ed0b136842cb45603f9003effc0887a2c9..0a00cf8467a7ddc7edcea22f27785039cfafaabc 100644 --- a/frontend/test/metabase/scenarios/admin/settings/whitelabel.cy.spec.js +++ b/frontend/test/metabase/scenarios/admin/settings/whitelabel.cy.spec.js @@ -1,31 +1,4 @@ -import { - restore, - openOrdersTable, - describeEE, - summarize, -} from "__support__/e2e/cypress"; - -// Define colors that we use for whitelabeling -// If rbg values exist, it's because we explicit test those -const colors = { - primary: { hex: "8B572A", rgb: [139, 87, 42] }, - nav: { hex: "284E07", rgb: [40, 78, 7] }, - accent1: { hex: "417505" }, - accent2: { hex: "7ED321" }, - additional1: { hex: "B8E986" }, - additional2: { hex: "50E3C2" }, - additional3: { hex: "4A90E2" }, - additional4: { hex: "082CBE" }, - additional5: { hex: "F8E71C", rgb: [248, 231, 28] }, -}; - -function changeThemeColor(location, colorhex) { - cy.get("td") - .eq(location) - .click(); - cy.get(`div[title='#${colorhex}']`).click(); - cy.findByText("Done").click(); -} +import { describeEE, restore } from "__support__/e2e/cypress"; function checkFavicon() { cy.request("/api/setting/application-favicon-url") @@ -47,47 +20,6 @@ describeEE("formatting > whitelabel", () => { cy.signInAsAdmin(); }); - describe("admin", () => { - it("should be able to set colors using color-picker dialog", () => { - cy.visit("/admin/settings/whitelabel"); - - cy.log("Select color with squares"); - changeThemeColor(1, colors.primary.hex); - - cy.log("Select color by entering rgb value"); - cy.get("td") - .eq(5) - .click(); - cy.get(".sketch-picker") - .find("input") - .eq(1) - .clear() - .type(colors.nav.rgb[0]); - cy.get(".sketch-picker") - .find("input") - .eq(2) - .clear() - .type(colors.nav.rgb[1]); - cy.get(".sketch-picker") - .find("input") - .eq(3) - .clear() - .type(colors.nav.rgb[2]); - cy.findByText("Done").click(); - - cy.log("Select color by typing hex code"); - cy.get("td") - .eq(29) - .click(); - cy.get(".sketch-picker") - .find("input") - .first() - .clear() - .type(colors.additional4.hex); - cy.findByText("Done").click(); - }); - }); - describe("company name", () => { const COMPANY_NAME = "Test Co"; @@ -136,36 +68,6 @@ describeEE("formatting > whitelabel", () => { }); }); - describe("company color theme", () => { - beforeEach(() => { - cy.request("PUT", "/api/setting/application-colors", { - value: { - accent1: `#${colors.accent1.hex}`, - accent2: `#${colors.accent2.hex}`, - accent3: `#${colors.additional1.hex}`, - accent4: `#${colors.additional2.hex}`, - accent5: `#${colors.additional3.hex}`, - accent6: `#${colors.additional4.hex}`, - accent7: `#${colors.additional5.hex}`, - brand: `#${colors.primary.hex}`, - nav: `#${colors.nav.hex}`, - }, - }); - }); - - it.skip("should show color changes reflected in q visualizations (metabase-enterprise #470)", () => { - // *** Test should pass when issue #470 is resolved - cy.signInAsNormalUser(); - openOrdersTable(); - summarize(); - cy.findByText("Price").click(); - cy.findByText("Done").click(); - - cy.get(`div[fill='#${colors.primary.hex};']`); - cy.get(`rect[fill='#509EE3']`).should("not.exist"); - }); - }); - describe("company logo", () => { beforeEach(() => { cy.log("Add a logo");