Skip to content
Snippets Groups Projects
Unverified Commit e8dc5ec5 authored by Phoomparin Mano's avatar Phoomparin Mano Committed by GitHub
Browse files

Migrate smart scalar and night mode to support Mantine theming (#42347)

* migrate smart scalar to css modules

* lighten theme color

* adjust margin

* map space 1 to xs

* use c prop instead of color

* use rem unit

* use rem unit

* add y margin

* return null when no children

* comment on text variation

* cleanup unused components and add dark mode todo

* remove todo comment

* support night mode in embedding

* lift dashboard theme to redux

* make theme non-nullable with default

* use component prop instead of title tag

* optional display theme

* restore tooltip state

* add default mock state

* remove theme from embed

* replace handleActions with createReducer
parent f8474255
No related branches found
No related tags found
No related merge requests found
Showing
with 167 additions and 145 deletions
......@@ -32,6 +32,9 @@ export interface MetabaseColors {
/** Text color on light elements. Should be a darker color for readability. */
"text-light"?: string;
/** Lighter variation of dark text on light elements. */
"text-medium"?: string;
}
export type MetabaseColor = keyof MetabaseColors;
......
import type { DisplayTheme } from "metabase/public/lib/types";
import type {
Dashboard,
DashboardId,
......@@ -112,4 +113,6 @@ export interface DashboardState {
toastDashboardId: number | null;
};
tabDeletions: Record<TabDeletionId, TabDeletion>;
theme: DisplayTheme | null;
}
......@@ -30,5 +30,6 @@ export const createMockDashboardState = (
toastDashboardId: null,
},
tabDeletions: {},
theme: "light",
...opts,
});
......@@ -11,3 +11,4 @@ export * from "./sharing";
export * from "./ui";
export * from "./actions";
export * from "./tabs";
export * from "./theme";
import { createAction } from "@reduxjs/toolkit";
import type { DisplayTheme } from "metabase/public/lib/types";
export const SET_DISPLAY_THEME = "metabase/dashboard/SET_DISPLAY_THEME";
export const setDisplayTheme = createAction<DisplayTheme>(SET_DISPLAY_THEME);
......@@ -39,6 +39,7 @@ export const INITIAL_DASHBOARD_STATE: DashboardState = {
toastDashboardId: null,
},
tabDeletions: {},
theme: "light",
};
export const DASHBOARD_SLOW_TIMEOUT = 15 * 1000;
......
......@@ -5,18 +5,29 @@ import { replace } from "react-router-redux";
import screenfull from "screenfull";
import HideS from "metabase/css/core/hide.module.css";
import { setDisplayTheme } from "metabase/dashboard/actions";
import { getDisplayTheme } from "metabase/dashboard/selectors";
import * as MetabaseAnalytics from "metabase/lib/analytics";
import { parseHashOptions, stringifyHashOptions } from "metabase/lib/browser";
const TICK_PERIOD = 1; // seconds
const mapStateToProps = (state, props) => ({
theme: getDisplayTheme(state),
});
const mapDispatchToProps = { replace, setDisplayTheme };
/* This contains some state for dashboard controls on both private and embedded dashboards.
* It should probably be in Redux?
*
* @deprecated HOCs are deprecated
*/
export const DashboardControls = ComposedComponent =>
connect(null, { replace })(
connect(
mapStateToProps,
mapDispatchToProps,
)(
class extends Component {
static displayName =
"DashboardControls[" +
......@@ -25,10 +36,7 @@ export const DashboardControls = ComposedComponent =>
state = {
isFullscreen: false,
theme: null,
refreshPeriod: null,
hideParameters: null,
};
......@@ -71,9 +79,10 @@ export const DashboardControls = ComposedComponent =>
? null
: options.refresh,
);
this.setTheme(options.theme);
this.setFullscreen(options.fullscreen);
this.setHideParameters(options.hide_parameters);
this.props.setDisplayTheme(options.theme);
};
syncUrlHashToState() {
......@@ -82,7 +91,7 @@ export const DashboardControls = ComposedComponent =>
const { refresh, fullscreen, theme } = parseHashOptions(location.hash);
this.setRefreshPeriod(refresh);
this.setFullscreen(fullscreen);
this.setTheme(theme);
this.props.setDisplayTheme(theme);
}
syncStateToUrlHash = () => {
......@@ -98,7 +107,7 @@ export const DashboardControls = ComposedComponent =>
};
setValue("refresh", this.state.refreshPeriod);
setValue("fullscreen", this.state.isFullscreen);
setValue("theme", this.state.theme);
setValue("theme", this.props.theme);
delete options.night; // DEPRECATED: options.night
......@@ -143,12 +152,7 @@ export const DashboardControls = ComposedComponent =>
// Preserve existing behavior, while keeping state in a new `theme` key
setNightMode = isNightMode => {
const theme = isNightMode ? "night" : null;
this.setState({ theme });
};
setTheme = theme => {
this.setState({ theme });
this.props.setDisplayTheme(isNightMode ? "night" : null);
};
setFullscreen = async (isFullscreen, browserFullscreen = true) => {
......@@ -236,8 +240,8 @@ export const DashboardControls = ComposedComponent =>
<ComposedComponent
{...this.props}
{...this.state}
isNightMode={this.state.theme === "night"}
hasNightModeToggle={this.state.theme !== "transparent"}
isNightMode={this.props.theme === "night"}
hasNightModeToggle={this.props.theme !== "transparent"}
setRefreshElapsedHook={this.setRefreshElapsedHook}
loadDashboardParams={this.loadDashboardParams}
onNightModeChange={this.setNightMode}
......
import { createReducer } from "@reduxjs/toolkit";
import { assoc, dissoc, assocIn, updateIn, chain, merge } from "icepick";
import produce from "immer";
import reduceReducers from "reduce-reducers";
......@@ -45,6 +46,7 @@ import {
SHOW_AUTO_APPLY_FILTERS_TOAST,
tabsReducer,
FETCH_CARD_DATA_PENDING,
SET_DISPLAY_THEME,
fetchDashboard,
} from "./actions";
import { INITIAL_DASHBOARD_STATE } from "./constants";
......@@ -515,6 +517,10 @@ const autoApplyFilters = handleActions(
INITIAL_DASHBOARD_STATE.autoApplyFilters,
);
const theme = createReducer(INITIAL_DASHBOARD_STATE.theme, builder => {
builder.addCase(SET_DISPLAY_THEME, (state, { payload }) => payload || null);
});
export const dashboardReducers = reduceReducers(
INITIAL_DASHBOARD_STATE,
combineReducers({
......@@ -536,6 +542,7 @@ export const dashboardReducers = reduceReducers(
// Combined reducer needs to init state for every slice
selectedTabId: (state = INITIAL_DASHBOARD_STATE.selectedTabId) => state,
tabDeletions: (state = INITIAL_DASHBOARD_STATE.tabDeletions) => state,
theme,
}),
tabsReducer,
);
......@@ -48,6 +48,7 @@ describe("dashboard reducers", () => {
toastDashboardId: null,
},
tabDeletions: {},
theme: "light",
});
});
......
......@@ -464,3 +464,10 @@ export const getParameterMappingsBeforeEditing = createSelector(
return map;
},
);
export const getDisplayTheme = (state: State) => state.dashboard.theme;
export const getIsNightMode = createSelector(
[getDisplayTheme],
theme => theme === "night",
);
......@@ -3,6 +3,7 @@ import { trackSchemaEvent } from "metabase/lib/analytics";
import type {
EmbeddingDisplayOptions,
DisplayTheme,
EmbedResource,
EmbedResourceType,
} from "./types";
......@@ -13,7 +14,7 @@ const SCHEMA_VERSION = "1-0-1";
type Appearance = {
titled: boolean;
bordered: boolean;
theme: "light" | "night" | "transparent";
theme: DisplayTheme;
font: "instance" | "custom";
hide_download_button: boolean | null;
};
......
import type { Card, Dashboard } from "metabase-types/api";
export type DisplayTheme = "light" | "night" | "transparent";
export type EmbedModalStep = "application" | "legalese" | null;
export type EmbedResource = (Card | Dashboard) & {
......@@ -25,7 +27,7 @@ export type EmbeddingParametersValues = Record<string, string>;
export type EmbeddingDisplayOptions = {
font: null | string;
theme: "light" | "night" | "transparent";
theme: DisplayTheme;
bordered: boolean;
titled: boolean;
hide_download_button: boolean | null;
......
......@@ -7,11 +7,14 @@ import { t, jt } from "ttag";
import { Ellipsified } from "metabase/core/components/Ellipsified";
import Tooltip from "metabase/core/components/Tooltip";
import DashboardS from "metabase/css/dashboard.module.css";
import { color } from "metabase/lib/colors";
import { getIsNightMode } from "metabase/dashboard/selectors";
import { color, lighten } from "metabase/lib/colors";
import { formatValue } from "metabase/lib/formatting/value";
import { measureTextWidth } from "metabase/lib/measure-text";
import { useSelector } from "metabase/lib/redux";
import { isEmpty } from "metabase/lib/validate";
import EmbedFrameS from "metabase/public/components/EmbedFrame/EmbedFrame.module.css";
import { Box, Flex, Title, Text, useMantineTheme } from "metabase/ui";
import ScalarValue, {
ScalarWrapper,
} from "metabase/visualizations/components/ScalarValue";
......@@ -28,18 +31,7 @@ import {
import { ScalarContainer } from "../Scalar/Scalar.styled";
import { SmartScalarComparisonWidget } from "./SettingsComponents/SmartScalarSettingsWidgets";
import {
PreviousValueDetails,
VariationContainer,
PreviousValueWrapper,
PreviousValueNumber,
Separator,
Variation,
VariationIcon,
VariationContainerTooltip,
VariationValue,
ScalarPeriodContent,
} from "./SmartScalar.styled";
import { VariationIcon, VariationValue } from "./SmartScalar.styled";
import { computeTrend, CHANGE_TYPE_OPTIONS } from "./compute";
import {
DASHCARD_HEADER_HEIGHT,
......@@ -137,27 +129,30 @@ export function SmartScalar({
/>
</span>
</ScalarContainer>
{isPeriodVisible(innerHeight) && (
<ScalarPeriod lines={1} period={display.date} />
)}
{isPeriodVisible(innerHeight) && <ScalarPeriod period={display.date} />}
{comparisons.map((comparison, index) => (
<PreviousValueWrapper key={index} data-testid="scalar-previous-value">
<Box maw="100%" key={index} data-testid="scalar-previous-value">
<PreviousValueComparison
comparison={comparison}
fontFamily={fontFamily}
formatOptions={formatOptions}
width={width}
/>
</PreviousValueWrapper>
</Box>
))}
</ScalarWrapper>
);
}
function ScalarPeriod({ lines = 2, period, onClick }) {
function ScalarPeriod({ period, onClick }) {
return (
<ScalarTitleContainer data-testid="scalar-period" lines={lines}>
<ScalarPeriodContent
<ScalarTitleContainer data-testid="scalar-period" lines={1}>
<Text
component="h3"
ta="center"
style={{ overflow: "hidden", cursor: onClick && "pointer" }}
fw={700}
size="0.875rem"
className={cx(
DashboardS.fullscreenNormalText,
DashboardS.fullscreenNightText,
......@@ -165,14 +160,36 @@ function ScalarPeriod({ lines = 2, period, onClick }) {
)}
onClick={onClick}
>
<Ellipsified tooltip={period} lines={lines} placement="bottom">
<Ellipsified tooltip={period} lines={1} placement="bottom">
{period}
</Ellipsified>
</ScalarPeriodContent>
</Text>
</ScalarTitleContainer>
);
}
const Separator = ({ inTooltip }) => {
const theme = useMantineTheme();
const isNightMode = useSelector(getIsNightMode);
const separatorColor =
isNightMode || inTooltip
? lighten(theme.fn.themeColor("text-medium"), 0.15)
: lighten(theme.fn.themeColor("text-light"), 0.25);
return (
<Text
display="inline-block"
mx="0.2rem"
style={{ transform: "scale(0.7)" }}
c={separatorColor}
span
>
{""}
</Text>
);
};
function PreviousValueComparison({
comparison,
width,
......@@ -191,6 +208,9 @@ function PreviousValueComparison({
display,
} = comparison;
const theme = useMantineTheme();
const isNightMode = useSelector(getIsNightMode);
const fittedChangeDisplay =
changeType === CHANGE_TYPE_OPTIONS.CHANGED.CHANGE_TYPE
? formatChangeAutoPrecision(percentChange, {
......@@ -199,13 +219,13 @@ function PreviousValueComparison({
width: getChangeWidth(width),
})
: display.percentChange;
const separator = <Separator></Separator>;
const availableComparisonWidth =
width -
4 * SPACING -
ICON_SIZE -
ICON_MARGIN_RIGHT -
measureTextWidth(innerText(separator), {
measureTextWidth(innerText(<Separator />), {
size: fontSize,
family: fontFamily,
weight: 700,
......@@ -223,21 +243,35 @@ function PreviousValueComparison({
: []),
"",
];
const detailCandidates = valueCandidates.map(valueStr => {
const getDetailCandidate = (valueStr, { inTooltip } = {}) => {
if (isEmpty(valueStr)) {
return comparisonDescStr;
}
const descColor =
isNightMode || inTooltip
? lighten(theme.fn.themeColor("text-medium"), 0.45)
: "text-light";
if (isEmpty(comparisonDescStr)) {
return (
<PreviousValueNumber key={valueStr}>{valueStr}</PreviousValueNumber>
<Text key={valueStr} c={descColor} span>
{valueStr}
</Text>
);
}
return jt`${comparisonDescStr}: ${(
<PreviousValueNumber key="value-str">{valueStr}</PreviousValueNumber>
<Text key="value-str" c={descColor} span>
{valueStr}
</Text>
)}`;
});
};
const detailCandidates = valueCandidates.map(valueStr =>
getDetailCandidate(valueStr),
);
const fullDetailDisplay = detailCandidates[0];
const fittedDetailDisplay = detailCandidates.find(
e =>
......@@ -248,36 +282,66 @@ function PreviousValueComparison({
}) <= availableComparisonWidth,
);
const VariationPercent = ({ iconSize, children }) => (
<Variation color={changeColor}>
{changeArrowIconName && (
<VariationIcon name={changeArrowIconName} size={iconSize} />
)}
<VariationValue showTooltip={false}>{children}</VariationValue>
</Variation>
);
const VariationDetails = ({ children }) =>
children ? (
<PreviousValueDetails>
{separator}
const tooltipFullDetailDisplay = getDetailCandidate(valueCandidates[0], {
inTooltip: true,
});
const VariationPercent = ({ inTooltip, iconSize, children }) => {
const noChangeColor =
inTooltip || isNightMode
? lighten(theme.fn.themeColor("text-medium"), 0.3)
: "text-light";
return (
<Flex align="center" maw="100%" c={changeColor ?? noChangeColor}>
{changeArrowIconName && (
<VariationIcon name={changeArrowIconName} size={iconSize} />
)}
<VariationValue showTooltip={false}>{children}</VariationValue>
</Flex>
);
};
const VariationDetails = ({ inTooltip, children }) => {
if (!children) {
return null;
}
const detailColor =
isNightMode || inTooltip
? lighten(theme.fn.themeColor("text-light"), 0.25)
: "text-medium";
return (
<Title order={4} c={detailColor} style={{ whiteSpace: "pre" }}>
<Separator inTooltip={inTooltip} />
{children}
</PreviousValueDetails>
) : null;
</Title>
);
};
return (
<Tooltip
isEnabled={fullDetailDisplay !== fittedDetailDisplay}
placement="bottom"
tooltip={
<VariationContainerTooltip className="variation-container-tooltip">
<VariationPercent iconSize={TOOLTIP_ICON_SIZE}>
<Flex align="center">
<VariationPercent iconSize={TOOLTIP_ICON_SIZE} inTooltip>
{display.percentChange}
</VariationPercent>
<VariationDetails>{fullDetailDisplay}</VariationDetails>
</VariationContainerTooltip>
<VariationDetails inTooltip>
{tooltipFullDetailDisplay}
</VariationDetails>
</Flex>
}
>
<VariationContainer
<Flex
wrap="wrap"
align="center"
justify="center"
mx="sm"
my="xs"
lh="1.2rem"
className={cx(
DashboardS.fullscreenNormalText,
DashboardS.fullscreenNightText,
......@@ -288,7 +352,7 @@ function PreviousValueComparison({
{fittedChangeDisplay}
</VariationPercent>
<VariationDetails>{fittedDetailDisplay}</VariationDetails>
</VariationContainer>
</Flex>
</Tooltip>
);
}
......
import styled from "@emotion/styled";
import { Ellipsified } from "metabase/core/components/Ellipsified";
import DashboardS from "metabase/css/dashboard.module.css";
import { color, lighten } from "metabase/lib/colors";
import { isEmpty } from "metabase/lib/validate";
import { space } from "metabase/styled-components/theme";
import { Icon } from "metabase/ui";
export const Variation = styled.div`
color: ${props => (isEmpty(props.color) ? color("text-light") : props.color)};
display: flex;
align-items: center;
max-width: 100%;
.${DashboardS.Dashboard}.${DashboardS.DashboardNight}.${DashboardS.DashboardFullscreen}
.${DashboardS.fullscreenNightText}
&,
.variation-container-tooltip & {
color: ${props =>
isEmpty(props.color) ? lighten("text-medium", 0.3) : props.color};
}
`;
export const VariationIcon = styled(Icon)`
display: flex;
align-items: center;
......@@ -33,64 +15,3 @@ export const VariationIcon = styled(Icon)`
export const VariationValue = styled(Ellipsified)`
font-weight: 900;
`;
export const VariationContainerTooltip = styled.div`
display: flex;
align-items: center;
`;
export const PreviousValueWrapper = styled.div`
max-width: 100%;
`;
export const VariationContainer = styled.div`
align-items: center;
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: ${space(0)} ${space(1)};
line-height: 1.2rem;
`;
export const Separator = styled.span`
display: inline-block;
transform: scale(0.7);
margin: 0 0.2rem;
color: ${lighten("text-light", 0.25)};
.${DashboardS.Dashboard}.${DashboardS.DashboardNight}.${DashboardS.DashboardFullscreen}
.${DashboardS.fullscreenNightText}
&,
.variation-container-tooltip & {
color: ${lighten("text-medium", 0.15)};
}
`;
export const PreviousValueDetails = styled.h4`
color: ${color("text-medium")};
white-space: pre;
.${DashboardS.Dashboard}.${DashboardS.DashboardNight}.${DashboardS.DashboardFullscreen}
.${DashboardS.fullscreenNightText}
&,
.variation-container-tooltip & {
color: ${lighten("text-light", 0.25)};
}
`;
export const PreviousValueNumber = styled.span`
color: ${color("text-light")};
.${DashboardS.Dashboard}.${DashboardS.DashboardNight}.${DashboardS.DashboardFullscreen}
.${DashboardS.fullscreenNightText}
&,
.variation-container-tooltip & {
color: ${lighten("text-medium", 0.45)};
}
`;
export const ScalarPeriodContent = styled.h3`
text-align: center;
overflow: hidden;
cursor: ${props => props.onClick && "pointer"};
font-size: 0.875rem;
`;
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