mirror of https://github.com/apache/superset.git
feat: Responsive UI for Big Number with Time Comparison (#27375)
This commit is contained in:
parent
2c00cc534c
commit
5de2530e3c
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { createRef, useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { css, styled, t, useTheme } from '@superset-ui/core';
|
||||
import { Tooltip } from '@superset-ui/chart-controls';
|
||||
import {
|
||||
|
@ -24,24 +24,33 @@ import {
|
|||
PopKPIComparisonValueStyleProps,
|
||||
PopKPIProps,
|
||||
} from './types';
|
||||
import { useOverflowDetection } from './useOverflowDetection';
|
||||
|
||||
const NumbersContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const ComparisonValue = styled.div<PopKPIComparisonValueStyleProps>`
|
||||
${({ theme, subheaderFontSize }) => `
|
||||
font-weight: ${theme.typography.weights.light};
|
||||
width: 33%;
|
||||
display: table-cell;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: ${subheaderFontSize || 20}px;
|
||||
text-align: center;
|
||||
flex: 1 1 0px;
|
||||
`}
|
||||
`;
|
||||
|
||||
const SymbolWrapper = styled.div<PopKPIComparisonSymbolStyleProps>`
|
||||
const SymbolWrapper = styled.span<PopKPIComparisonSymbolStyleProps>`
|
||||
${({ theme, backgroundColor, textColor }) => `
|
||||
background-color: ${backgroundColor};
|
||||
color: ${textColor};
|
||||
padding: ${theme.gridUnit}px ${theme.gridUnit * 2}px;
|
||||
border-radius: ${theme.gridUnit * 2}px;
|
||||
display: inline-block;
|
||||
margin-right: ${theme.gridUnit}px;
|
||||
`}
|
||||
`;
|
||||
|
@ -61,25 +70,23 @@ export default function PopKPI(props: PopKPIProps) {
|
|||
comparatorText,
|
||||
} = props;
|
||||
|
||||
const rootElem = createRef<HTMLDivElement>();
|
||||
const theme = useTheme();
|
||||
|
||||
const flexGap = theme.gridUnit * 5;
|
||||
const wrapperDivStyles = css`
|
||||
font-family: ${theme.typography.families.sansSerif};
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: ${theme.gridUnit * 4}px;
|
||||
border-radius: ${theme.gridUnit * 2}px;
|
||||
align-items: center;
|
||||
height: ${height}px;
|
||||
width: ${width}px;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const bigValueContainerStyles = css`
|
||||
font-size: ${headerFontSize || 60}px;
|
||||
font-weight: ${theme.typography.weights.normal};
|
||||
text-align: center;
|
||||
margin-bottom: ${theme.gridUnit * 4}px;
|
||||
`;
|
||||
|
||||
const getArrowIndicatorColor = () => {
|
||||
|
@ -135,29 +142,59 @@ export default function PopKPI(props: PopKPIProps) {
|
|||
tooltipText: t('Percentage difference between the time periods'),
|
||||
},
|
||||
],
|
||||
[prevNumber, valueDifference, percentDifferenceFormattedString],
|
||||
[
|
||||
comparatorText,
|
||||
prevNumber,
|
||||
valueDifference,
|
||||
percentDifferenceFormattedString,
|
||||
],
|
||||
);
|
||||
|
||||
const { isOverflowing, symbolContainerRef, wrapperRef } =
|
||||
useOverflowDetection(flexGap);
|
||||
|
||||
return (
|
||||
<div ref={rootElem} css={wrapperDivStyles}>
|
||||
<div css={bigValueContainerStyles}>
|
||||
{bigNumber}
|
||||
{percentDifferenceNumber !== 0 && (
|
||||
<span css={arrowIndicatorStyle}>
|
||||
{percentDifferenceNumber > 0 ? '↑' : '↓'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
css={css`
|
||||
width: 100%;
|
||||
display: table;
|
||||
`}
|
||||
<div css={wrapperDivStyles} ref={wrapperRef}>
|
||||
<NumbersContainer
|
||||
css={
|
||||
isOverflowing &&
|
||||
css`
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
align-items: flex-start;
|
||||
`
|
||||
}
|
||||
>
|
||||
<div css={bigValueContainerStyles}>
|
||||
{bigNumber}
|
||||
{percentDifferenceNumber !== 0 && (
|
||||
<span css={arrowIndicatorStyle}>
|
||||
{percentDifferenceNumber > 0 ? '↑' : '↓'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
css={css`
|
||||
display: table-row;
|
||||
`}
|
||||
css={[
|
||||
css`
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: ${flexGap}px;
|
||||
min-width: 0;
|
||||
flex-shrink: 1;
|
||||
`,
|
||||
isOverflowing
|
||||
? css`
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: fit-content;
|
||||
`
|
||||
: css`
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
`,
|
||||
]}
|
||||
ref={symbolContainerRef}
|
||||
>
|
||||
{SYMBOLS_WITH_VALUES.map((symbol_with_value, index) => (
|
||||
<ComparisonValue
|
||||
|
@ -182,7 +219,7 @@ export default function PopKPI(props: PopKPIProps) {
|
|||
</ComparisonValue>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</NumbersContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -103,12 +103,18 @@ const config: ControlPanelConfig = {
|
|||
controlSetRows: [
|
||||
['y_axis_format'],
|
||||
['currency_format'],
|
||||
[headerFontSize],
|
||||
[
|
||||
{
|
||||
...headerFontSize,
|
||||
config: { ...headerFontSize.config, default: 0.2 },
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
...subheaderFontSize,
|
||||
config: {
|
||||
...subheaderFontSize.config,
|
||||
default: 0.125,
|
||||
label: t('Comparison font size'),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
export const useOverflowDetection = (flexGap: number) => {
|
||||
const symbolContainerRef = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const [isOverflowing, setIsOverflowing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let obs: ResizeObserver;
|
||||
const symbolContainerElem = symbolContainerRef.current;
|
||||
const wrapperElem = wrapperRef.current;
|
||||
if (symbolContainerElem && wrapperElem) {
|
||||
const symbolContainerChildrenElems = Array.from(
|
||||
symbolContainerElem.children,
|
||||
);
|
||||
obs = new ResizeObserver(
|
||||
debounce(() => {
|
||||
const totalChildrenWidth = symbolContainerChildrenElems.reduce(
|
||||
(acc, element) =>
|
||||
// take symbol container's child's scroll width to account for the container growing with display: flex
|
||||
acc + (element.firstElementChild?.scrollWidth ?? 0),
|
||||
0,
|
||||
);
|
||||
if (
|
||||
totalChildrenWidth +
|
||||
flexGap * Math.max(symbolContainerChildrenElems.length - 1, 0) >
|
||||
wrapperElem.clientWidth
|
||||
) {
|
||||
setIsOverflowing(true);
|
||||
} else {
|
||||
setIsOverflowing(false);
|
||||
}
|
||||
}, 500),
|
||||
);
|
||||
obs.observe(document.body);
|
||||
symbolContainerChildrenElems.forEach(elem => {
|
||||
obs.observe(elem);
|
||||
});
|
||||
}
|
||||
return () => obs?.disconnect();
|
||||
}, [flexGap]);
|
||||
|
||||
return { isOverflowing, symbolContainerRef, wrapperRef };
|
||||
};
|
Loading…
Reference in New Issue