blob: 5b2d9d7001bf32979e02729c080028306ba5181e [file] [log] [blame]
/**
* 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 { useRef, ReactNode } from 'react';
import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd';
import { styled, t, useTheme, keyframes, css } from '@superset-ui/core';
import { InfoTooltip, Icons, Tooltip } from '@superset-ui/core/components';
import { savedMetricType } from 'src/explore/components/controls/MetricControl/types';
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
import { StyledMetricOption } from '../../optionRenderers';
export const DragContainer = styled.div`
margin-bottom: ${({ theme }) => theme.sizeUnit}px;
:last-child {
margin-bottom: 0;
}
`;
export const OptionControlContainer = styled.div<{
withCaret?: boolean;
}>`
display: flex;
align-items: center;
width: 100%;
font-size: ${({ theme }) => theme.fontSizeSM}px;
height: ${({ theme }) => theme.sizeUnit * 6}px;
background-color: ${({ theme }) => theme.colorBgLayout};
border-radius: 3px;
cursor: ${({ withCaret }) => (withCaret ? 'pointer' : 'default')};
:hover {
background-color: ${({ theme }) => theme.colorPrimaryBgHover};
}
`;
export const Label = styled.div`
${({ theme }) => `
display: flex;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
align-items: center;
white-space: nowrap;
padding-left: ${theme.sizeUnit}px;
svg {
margin-right: ${theme.sizeUnit}px;
}
.type-label {
margin-right: ${theme.sizeUnit * 2}px;
margin-left: ${theme.sizeUnit}px;
font-weight: ${theme.fontWeightNormal};
width: auto;
}
.option-label {
display: inline;
}
`}
`;
const LabelText = styled.span`
overflow: hidden;
text-overflow: ellipsis;
`;
export const CaretContainer = styled.div`
height: 100%;
border-left: solid 1px ${({ theme }) => theme.colorSplit};
margin-left: auto;
`;
export const CloseContainer = styled.div`
height: auto;
width: ${({ theme }) => theme.sizeUnit * 6}px;
border-right: solid 1px ${({ theme }) => theme.colorBorder};
cursor: pointer;
`;
const StyledInfoTooltip = styled(InfoTooltip)`
margin: 0 ${({ theme }) => theme.sizeUnit}px;
`;
export const HeaderContainer = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`;
export const LabelsContainer = styled.div`
padding: ${({ theme }) => theme.sizeUnit}px;
border: solid 1px ${({ theme }) => theme.colorSplit};
border-radius: ${({ theme }) => theme.borderRadius}px;
`;
const borderPulse = keyframes`
0% {
right: 100%;
}
50% {
left: 4px;
}
90% {
right: 4px;
}
100% {
left: 100%;
}
`;
export const DndLabelsContainer = styled.div<{
canDrop?: boolean;
isOver?: boolean;
isDragging?: boolean;
isLoading?: boolean;
}>`
${({ theme, isLoading, canDrop, isDragging, isOver }) => `
position: relative;
padding: ${theme.sizeUnit}px;
border: ${
!isLoading && isDragging
? `dashed 1px ${canDrop ? theme.colorSplit : theme.colorErrorBgHover}`
: `solid 1px ${
isLoading && isDragging
? theme.colorWarningBgHover
: theme.colorBorder
}`
};
border-radius: ${theme.borderRadius}px;
&:before,
&:after {
content: ' ';
position: absolute;
border-radius: ${theme.borderRadius}px;
}
&:before {
display: ${isDragging || isLoading ? 'block' : 'none'};
background-color: ${canDrop ? theme.colorPrimary : theme.colorErrorBgHover};
z-index: 10;
opacity: 10%;
top: 1px;
right: 1px;
bottom: 1px;
left: 1px;
}
&:after {
display: ${isLoading || (canDrop && isOver) ? 'block' : 'none'};
background-color: ${
isLoading ? theme.colorFillTertiary : theme.colorPrimary
};
z-index: 11;
opacity: 35%;
top: ${-theme.sizeUnit}px;
right: ${-theme.sizeUnit}px;
bottom: ${-theme.sizeUnit}px;
left: ${-theme.sizeUnit}px;
cursor: ${isLoading ? 'wait' : 'auto'};
}
`}
&:before {
${({ theme, isLoading }) =>
isLoading &&
css`
animation: ${borderPulse} 2s ease-in infinite;
background: linear-gradient(currentColor 0 0) 0 100%/0% 3px no-repeat;
background-size: 100% ${theme.sizeUnit / 2}px;
top: auto;
right: ${theme.sizeUnit}px;
left: ${theme.sizeUnit}px;
bottom: -${theme.sizeUnit / 2}px;
height: ${theme.sizeUnit / 2}px;
`};
}
`;
export const AddControlLabel = styled.div<{
cancelHover?: boolean;
}>`
display: flex;
align-items: center;
width: 100%;
height: ${({ theme }) => theme.sizeUnit * 6}px;
padding-left: ${({ theme }) => theme.sizeUnit}px;
font-size: ${({ theme }) => theme.fontSizeSM}px;
color: ${({ theme }) => theme.colorTextSecondary};
border: dashed 1px ${({ theme }) => theme.colorSplit};
border-radius: ${({ theme }) => theme.borderRadius}px;
cursor: ${({ cancelHover }) => (cancelHover ? 'inherit' : 'pointer')};
:hover {
background-color: ${({ cancelHover, theme }) =>
cancelHover ? 'inherit' : theme.colorFillSecondary};
}
:active {
background-color: ${({ cancelHover, theme }) =>
cancelHover ? 'inherit' : theme.colorFillTertiary};
}
svg {
margin-right: ${({ theme }) => theme.sizeUnit}px;
}
`;
export const AddIconButton = styled.button`
display: flex;
align-items: center;
justify-content: center;
height: ${({ theme }) => theme.sizeUnit * 4}px;
width: ${({ theme }) => theme.sizeUnit * 4}px;
padding: 0;
background-color: ${({ theme }) => theme.colorPrimaryText};
border: none;
border-radius: 2px;
cursor: pointer;
:disabled {
cursor: not-allowed;
background-color: ${({ theme }) => theme.colorBgContainerDisabled};
}
`;
interface DragItem {
dragIndex: number;
type: string;
}
export const OptionControlLabel = ({
label,
savedMetric,
adhocMetric,
onRemove,
onMoveLabel,
onDropLabel,
withCaret,
isFunction,
type,
index,
isExtra,
datasourceWarningMessage,
tooltipTitle,
multi = true,
...props
}: {
label: string | ReactNode;
savedMetric?: savedMetricType;
adhocMetric?: AdhocMetric;
onRemove: () => void;
onMoveLabel: (dragIndex: number, hoverIndex: number) => void;
onDropLabel: () => void;
withCaret?: boolean;
isFunction?: boolean;
isDraggable?: boolean;
type: string;
index: number;
isExtra?: boolean;
datasourceWarningMessage?: string;
tooltipTitle?: string;
multi?: boolean;
}) => {
const theme = useTheme();
const ref = useRef<HTMLDivElement>(null);
const labelRef = useRef<HTMLDivElement>(null);
const hasMetricName = savedMetric?.metric_name;
const [, drop] = useDrop({
accept: type,
drop() {
if (!multi) {
return;
}
onDropLabel?.();
},
hover(item: DragItem, monitor: DropTargetMonitor) {
if (!multi) {
return;
}
if (!ref.current) {
return;
}
const { dragIndex } = item;
const hoverIndex = index;
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return;
}
// Determine rectangle on screen
const hoverBoundingRect = ref.current?.getBoundingClientRect();
// Get vertical middle
const hoverMiddleY =
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get pixels to the top
const hoverClientY = clientOffset?.y
? clientOffset?.y - hoverBoundingRect.top
: 0;
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
// Time to actually perform the action
onMoveLabel?.(dragIndex, hoverIndex);
// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
// eslint-disable-next-line no-param-reassign
item.dragIndex = hoverIndex;
},
});
const [{ isDragging }, drag] = useDrag({
item: {
type,
dragIndex: index,
value: savedMetric?.metric_name ? savedMetric : adhocMetric,
},
collect: monitor => ({
isDragging: monitor.isDragging(),
}),
});
const getLabelContent = () => {
const shouldShowTooltip =
(!isDragging &&
typeof label === 'string' &&
tooltipTitle &&
label &&
tooltipTitle !== label) ||
(!isDragging &&
labelRef &&
labelRef.current &&
labelRef.current.scrollWidth > labelRef.current.clientWidth);
if (savedMetric && hasMetricName) {
return (
<StyledMetricOption
metric={savedMetric}
labelRef={labelRef}
shouldShowTooltip={!isDragging}
/>
);
}
if (!shouldShowTooltip) {
return <LabelText ref={labelRef}>{label}</LabelText>;
}
return (
<Tooltip title={tooltipTitle || label}>
<LabelText ref={labelRef}>{label}</LabelText>
</Tooltip>
);
};
const getOptionControlContent = () => (
<OptionControlContainer
withCaret={withCaret}
data-test="option-label"
{...props}
css={css`
text-align: center;
`}
>
<CloseContainer
role="button"
data-test="remove-control-button"
onClick={onRemove}
>
<Icons.CloseOutlined
iconSize="m"
iconColor={theme.colorIcon}
css={css`
vertical-align: sub;
`}
/>
</CloseContainer>
<Label data-test="control-label">
{isFunction && <Icons.FunctionOutlined iconSize="m" />}
{getLabelContent()}
</Label>
{(!!datasourceWarningMessage || isExtra) && (
<StyledInfoTooltip
type="warning"
placement="top"
tooltip={
datasourceWarningMessage ||
t(`
This filter was inherited from the dashboard's context.
It won't be saved when saving the chart.
`)
}
/>
)}
{withCaret && (
<CaretContainer>
<Icons.RightOutlined
iconSize="m"
css={css`
margin: ${theme.sizeUnit}px;
`}
iconColor={theme.colorIcon}
/>
</CaretContainer>
)}
</OptionControlContainer>
);
drag(drop(ref));
return <DragContainer ref={ref}>{getOptionControlContent()}</DragContainer>;
};