/** * 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 React, { useRef } from 'react'; import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd'; import { styled, t, useTheme } from '@superset-ui/core'; import { MetricOption, InfoTooltipWithTrigger, } from '@superset-ui/chart-controls'; import { Tooltip } from 'src/common/components/Tooltip'; import Icon from 'src/components/Icon'; import { savedMetricType } from 'src/explore/components/controls/MetricControl/types'; const DragContainer = styled.div` margin-bottom: ${({ theme }) => theme.gridUnit}px; :last-child { margin-bottom: 0; } `; const OptionControlContainer = styled.div<{ isAdhoc?: boolean; }>` display: flex; align-items: center; width: 100%; font-size: ${({ theme }) => theme.typography.sizes.s}px; height: ${({ theme }) => theme.gridUnit * 6}px; background-color: ${({ theme }) => theme.colors.grayscale.light3}; border-radius: 3px; cursor: ${({ isAdhoc }) => (isAdhoc ? 'pointer' : 'default')}; `; const Label = styled.div` display: flex; max-width: 100%; overflow: hidden; text-overflow: ellipsis; align-items: center; white-space: nowrap; padding-left: ${({ theme }) => theme.gridUnit}px; svg { margin-right: ${({ theme }) => theme.gridUnit}px; } .option-label { display: inline; } `; const CaretContainer = styled.div` height: 100%; border-left: solid 1px ${({ theme }) => theme.colors.grayscale.dark2}0C; margin-left: auto; `; const CloseContainer = styled.div` height: 100%; width: ${({ theme }) => theme.gridUnit * 6}px; border-right: solid 1px ${({ theme }) => theme.colors.grayscale.dark2}0C; cursor: pointer; `; const StyledInfoTooltipWithTrigger = styled(InfoTooltipWithTrigger)` margin: 0 ${({ theme }) => theme.gridUnit}px; `; export const HeaderContainer = styled.div` display: flex; align-items: center; justify-content: space-between; `; export const LabelsContainer = styled.div` padding: ${({ theme }) => theme.gridUnit}px; border: solid 1px ${({ theme }) => theme.colors.grayscale.light2}; border-radius: ${({ theme }) => theme.gridUnit}px; `; export const AddControlLabel = styled.div` display: flex; align-items: center; width: 100%; height: ${({ theme }) => theme.gridUnit * 6}px; padding-left: ${({ theme }) => theme.gridUnit}px; font-size: ${({ theme }) => theme.typography.sizes.s}px; color: ${({ theme }) => theme.colors.grayscale.light1}; border: dashed 1px ${({ theme }) => theme.colors.grayscale.light2}; border-radius: ${({ theme }) => theme.gridUnit}px; cursor: pointer; :hover { background-color: ${({ theme }) => theme.colors.grayscale.light4}; } :active { background-color: ${({ theme }) => theme.colors.grayscale.light3}; } `; export const AddIconButton = styled.button` display: flex; align-items: center; justify-content: center; height: ${({ theme }) => theme.gridUnit * 4}px; width: ${({ theme }) => theme.gridUnit * 4}px; padding: 0; background-color: ${({ theme }) => theme.colors.primary.dark1}; border: none; border-radius: 2px; :disabled { cursor: not-allowed; background-color: ${({ theme }) => theme.colors.grayscale.light1}; } `; interface DragItem { index: number; type: string; } export const OptionControlLabel = ({ label, savedMetric, onRemove, onMoveLabel, onDropLabel, isAdhoc, isFunction, type, index, isExtra, ...props }: { label: string | React.ReactNode; savedMetric?: savedMetricType; onRemove: () => void; onMoveLabel: (dragIndex: number, hoverIndex: number) => void; onDropLabel: () => void; isAdhoc?: boolean; isFunction?: boolean; isDraggable?: boolean; type: string; index: number; isExtra?: boolean; }) => { const theme = useTheme(); const ref = useRef(null); const [, drop] = useDrop({ accept: type, drop() { onDropLabel?.(); }, hover(item: DragItem, monitor: DropTargetMonitor) { if (!ref.current) { return; } const dragIndex = item.index; 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.index = hoverIndex; }, }); const [, drag] = useDrag({ item: { type, index }, collect: monitor => ({ isDragging: monitor.isDragging(), }), }); const getLabelContent = () => { if (savedMetric?.metric_name) { return ; } return {label}; }; const getOptionControlContent = () => ( {isExtra && ( )} {isAdhoc && ( )} ); drag(drop(ref)); return {getOptionControlContent()}; };