| /** |
| * 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, useTheme } from '@superset-ui/core'; |
| import { ColumnOption } 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: inline-block; |
| 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; |
| `; |
| |
| 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, |
| ...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; |
| }) => { |
| const theme = useTheme(); |
| const ref = useRef<HTMLDivElement>(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) { |
| // add column_name to fix typescript error |
| const column = { ...savedMetric, column_name: '' }; |
| if (!column.verbose_name) { |
| column.verbose_name = column.metric_name; |
| } |
| return <ColumnOption column={column} />; |
| } |
| return <Tooltip title={label}>{label}</Tooltip>; |
| }; |
| |
| const getOptionControlContent = () => ( |
| <OptionControlContainer |
| isAdhoc={isAdhoc} |
| data-test="option-label" |
| {...props} |
| > |
| <CloseContainer |
| role="button" |
| data-test="remove-control-button" |
| onClick={onRemove} |
| > |
| <Icon name="x-small" color={theme.colors.grayscale.light1} /> |
| </CloseContainer> |
| <Label data-test="control-label"> |
| {isFunction && <Icon name="function" viewBox="0 0 16 11" />} |
| {getLabelContent()} |
| </Label> |
| {isAdhoc && ( |
| <CaretContainer> |
| <Icon name="caret-right" color={theme.colors.grayscale.light1} /> |
| </CaretContainer> |
| )} |
| </OptionControlContainer> |
| ); |
| |
| drag(drop(ref)); |
| return <DragContainer ref={ref}>{getOptionControlContent()}</DragContainer>; |
| }; |