Skip to content
Snippets Groups Projects
Unverified Commit 3f353cee authored by Alexander Polyankin's avatar Alexander Polyankin Committed by GitHub
Browse files

New UI to change the color palette in Admin (#22926)

parent abe3a3f1
No related branches found
No related tags found
No related merge requests found
Showing
with 606 additions and 95 deletions
......@@ -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 => ({
......
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;
`;
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;
export { default } from "./BrandColorSettings";
export interface ColorOption {
name: string;
description: string;
}
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.`,
},
];
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;
`;
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;
export { default } from "./ChartColorPreview";
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")};
}
`;
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;
export { default } from "./ChartColorSettings";
export const getChartColorGroups = (): string[][] => [
["accent2", "accent2", "accent2"],
["accent3", "accent3", "accent3"],
["accent4", "accent4", "accent4"],
["accent5", "accent5", "accent5"],
["accent6", "accent6", "accent6"],
];
/* 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;
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"),
},
};
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;
`;
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;
export { default } from "./ColorSettings";
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;
export { default } from "./ColorSettingsWidget";
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment