refactor: Split SliceHeaderControls into smaller files (#31270)
This commit is contained in:
parent
25f4226dbb
commit
1d44662b1d
|
|
@ -40,13 +40,13 @@ import {
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { RootState } from 'src/dashboard/types';
|
import { RootState } from 'src/dashboard/types';
|
||||||
import { Menu } from 'src/components/Menu';
|
import { Menu } from 'src/components/Menu';
|
||||||
|
import { usePermissions } from 'src/hooks/usePermissions';
|
||||||
import { AntdDropdown as Dropdown } from 'src/components/index';
|
import { AntdDropdown as Dropdown } from 'src/components/index';
|
||||||
import { updateDataMask } from 'src/dataMask/actions';
|
import { updateDataMask } from 'src/dataMask/actions';
|
||||||
import { DrillDetailMenuItems } from '../DrillDetail';
|
import { DrillDetailMenuItems } from '../DrillDetail';
|
||||||
import { getMenuAdjustedY } from '../utils';
|
import { getMenuAdjustedY } from '../utils';
|
||||||
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
import { MenuItemTooltip } from '../DisabledMenuItemTooltip';
|
||||||
import { DrillByMenuItems } from '../DrillBy/DrillByMenuItems';
|
import { DrillByMenuItems } from '../DrillBy/DrillByMenuItems';
|
||||||
import { usePermissions } from './usePermissions';
|
|
||||||
|
|
||||||
export enum ContextMenuItem {
|
export enum ContextMenuItem {
|
||||||
CrossFilter,
|
CrossFilter,
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,8 @@ import { useUiConfig } from 'src/components/UiConfigContext';
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import EditableTitle from 'src/components/EditableTitle';
|
import EditableTitle from 'src/components/EditableTitle';
|
||||||
import SliceHeaderControls, {
|
import SliceHeaderControls from 'src/dashboard/components/SliceHeaderControls';
|
||||||
SliceHeaderControlsProps,
|
import { SliceHeaderControlsProps } from 'src/dashboard/components/SliceHeaderControls/types';
|
||||||
} from 'src/dashboard/components/SliceHeaderControls';
|
|
||||||
import FiltersBadge from 'src/dashboard/components/FiltersBadge';
|
import FiltersBadge from 'src/dashboard/components/FiltersBadge';
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
import { RootState } from 'src/dashboard/types';
|
import { RootState } from 'src/dashboard/types';
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,9 @@ import { render, screen } from 'spec/helpers/testing-library';
|
||||||
import { FeatureFlag, VizType } from '@superset-ui/core';
|
import { FeatureFlag, VizType } from '@superset-ui/core';
|
||||||
import mockState from 'spec/fixtures/mockState';
|
import mockState from 'spec/fixtures/mockState';
|
||||||
import { Menu } from 'src/components/Menu';
|
import { Menu } from 'src/components/Menu';
|
||||||
import SliceHeaderControls, {
|
import SliceHeaderControls from '.';
|
||||||
SliceHeaderControlsProps,
|
import { SliceHeaderControlsProps } from './types';
|
||||||
handleDropdownNavigation,
|
import { handleDropdownNavigation } from './utils';
|
||||||
} from '.';
|
|
||||||
|
|
||||||
jest.mock('src/components/Dropdown', () => {
|
jest.mock('src/components/Dropdown', () => {
|
||||||
const original = jest.requireActual('src/components/Dropdown');
|
const original = jest.requireActual('src/components/Dropdown');
|
||||||
|
|
@ -310,13 +309,13 @@ test('Should show "Drill to detail" with `can_explore` & `can_samples` perms', (
|
||||||
(global as any).featureFlags = {
|
(global as any).featureFlags = {
|
||||||
[FeatureFlag.DrillToDetail]: true,
|
[FeatureFlag.DrillToDetail]: true,
|
||||||
};
|
};
|
||||||
const props = {
|
const props = createProps();
|
||||||
...createProps(),
|
|
||||||
supersetCanExplore: true,
|
|
||||||
};
|
|
||||||
props.slice.slice_id = 18;
|
props.slice.slice_id = 18;
|
||||||
renderWrapper(props, {
|
renderWrapper(props, {
|
||||||
Admin: [['can_samples', 'Datasource']],
|
Admin: [
|
||||||
|
['can_samples', 'Datasource'],
|
||||||
|
['can_explore', 'Superset'],
|
||||||
|
],
|
||||||
});
|
});
|
||||||
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
|
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
/**
|
||||||
|
* 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 { ReactChild, useCallback } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { css, t, useTheme } from '@superset-ui/core';
|
||||||
|
import Modal from 'src/components/Modal';
|
||||||
|
import Button from 'src/components/Button';
|
||||||
|
|
||||||
|
export const ViewResultsModalTrigger = ({
|
||||||
|
canExplore,
|
||||||
|
exploreUrl,
|
||||||
|
triggerNode,
|
||||||
|
modalTitle,
|
||||||
|
modalBody,
|
||||||
|
showModal = false,
|
||||||
|
setShowModal,
|
||||||
|
}: {
|
||||||
|
canExplore?: boolean;
|
||||||
|
exploreUrl: string;
|
||||||
|
triggerNode: ReactChild;
|
||||||
|
modalTitle: ReactChild;
|
||||||
|
modalBody: ReactChild;
|
||||||
|
showModal: boolean;
|
||||||
|
setShowModal: (showModal: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
const history = useHistory();
|
||||||
|
const exploreChart = () => history.push(exploreUrl);
|
||||||
|
const theme = useTheme();
|
||||||
|
const openModal = useCallback(() => setShowModal(true), [setShowModal]);
|
||||||
|
const closeModal = useCallback(() => setShowModal(false), [setShowModal]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
data-test="span-modal-trigger"
|
||||||
|
onClick={openModal}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{triggerNode}
|
||||||
|
</span>
|
||||||
|
{(() => (
|
||||||
|
<Modal
|
||||||
|
css={css`
|
||||||
|
.ant-modal-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
show={showModal}
|
||||||
|
onHide={closeModal}
|
||||||
|
closable
|
||||||
|
title={modalTitle}
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
buttonStyle="secondary"
|
||||||
|
buttonSize="small"
|
||||||
|
onClick={exploreChart}
|
||||||
|
disabled={!canExplore}
|
||||||
|
tooltip={
|
||||||
|
!canExplore
|
||||||
|
? t(
|
||||||
|
'You do not have sufficient permissions to edit the chart',
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('Edit chart')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
buttonStyle="primary"
|
||||||
|
buttonSize="small"
|
||||||
|
onClick={closeModal}
|
||||||
|
css={css`
|
||||||
|
margin-left: ${theme.gridUnit * 2}px;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{t('Close')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
responsive
|
||||||
|
resizable
|
||||||
|
resizableConfig={{
|
||||||
|
minHeight: theme.gridUnit * 128,
|
||||||
|
minWidth: theme.gridUnit * 128,
|
||||||
|
defaultSize: {
|
||||||
|
width: 'auto',
|
||||||
|
height: '75vh',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
draggable
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
{modalBody}
|
||||||
|
</Modal>
|
||||||
|
))()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -16,19 +16,9 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import {
|
import { MouseEvent, Key, useState, useRef, RefObject } from 'react';
|
||||||
MouseEvent,
|
|
||||||
Key,
|
|
||||||
KeyboardEvent,
|
|
||||||
ReactChild,
|
|
||||||
useState,
|
|
||||||
useRef,
|
|
||||||
RefObject,
|
|
||||||
useCallback,
|
|
||||||
ReactElement,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { RouteComponentProps, useHistory, withRouter } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import {
|
import {
|
||||||
Behavior,
|
Behavior,
|
||||||
|
|
@ -36,23 +26,12 @@ import {
|
||||||
isFeatureEnabled,
|
isFeatureEnabled,
|
||||||
FeatureFlag,
|
FeatureFlag,
|
||||||
getChartMetadataRegistry,
|
getChartMetadataRegistry,
|
||||||
QueryFormData,
|
|
||||||
styled,
|
styled,
|
||||||
t,
|
t,
|
||||||
useTheme,
|
|
||||||
ensureIsArray,
|
|
||||||
VizType,
|
VizType,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import {
|
import { Menu } from 'src/components/Menu';
|
||||||
MenuItemKeyEnum,
|
|
||||||
Menu,
|
|
||||||
MenuItemChildType,
|
|
||||||
isAntdMenuItem,
|
|
||||||
isAntdMenuItemRef,
|
|
||||||
isSubMenuOrItemType,
|
|
||||||
isAntdMenuSubmenu,
|
|
||||||
} from 'src/components/Menu';
|
|
||||||
import { NoAnimationDropdown } from 'src/components/Dropdown';
|
import { NoAnimationDropdown } from 'src/components/Dropdown';
|
||||||
import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems';
|
import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems';
|
||||||
import downloadAsImage from 'src/utils/downloadAsImage';
|
import downloadAsImage from 'src/utils/downloadAsImage';
|
||||||
|
|
@ -60,28 +39,16 @@ import { getSliceHeaderTooltip } from 'src/dashboard/util/getSliceHeaderTooltip'
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
import ModalTrigger from 'src/components/ModalTrigger';
|
import ModalTrigger from 'src/components/ModalTrigger';
|
||||||
import Button from 'src/components/Button';
|
|
||||||
import ViewQueryModal from 'src/explore/components/controls/ViewQueryModal';
|
import ViewQueryModal from 'src/explore/components/controls/ViewQueryModal';
|
||||||
import { ResultsPaneOnDashboard } from 'src/explore/components/DataTablesPane';
|
import { ResultsPaneOnDashboard } from 'src/explore/components/DataTablesPane';
|
||||||
import Modal from 'src/components/Modal';
|
|
||||||
import { DrillDetailMenuItems } from 'src/components/Chart/DrillDetail';
|
import { DrillDetailMenuItems } from 'src/components/Chart/DrillDetail';
|
||||||
import { LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';
|
import { LOG_ACTIONS_CHART_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';
|
||||||
import { MenuKeys, RootState } from 'src/dashboard/types';
|
import { MenuKeys, RootState } from 'src/dashboard/types';
|
||||||
import { findPermission } from 'src/utils/findPermission';
|
import { usePermissions } from 'src/hooks/usePermissions';
|
||||||
import { useCrossFiltersScopingModal } from '../nativeFilters/FilterBar/CrossFilters/ScopingModal/useCrossFiltersScopingModal';
|
import { useCrossFiltersScopingModal } from '../nativeFilters/FilterBar/CrossFilters/ScopingModal/useCrossFiltersScopingModal';
|
||||||
|
import { handleDropdownNavigation } from './utils';
|
||||||
const ACTION_KEYS = {
|
import { ViewResultsModalTrigger } from './ViewResultsModalTrigger';
|
||||||
enter: 'Enter',
|
import { SliceHeaderControlsProps } from './types';
|
||||||
spacebar: 'Spacebar',
|
|
||||||
space: ' ',
|
|
||||||
};
|
|
||||||
|
|
||||||
const NAV_KEYS = {
|
|
||||||
tab: 'Tab',
|
|
||||||
escape: 'Escape',
|
|
||||||
up: 'ArrowUp',
|
|
||||||
down: 'ArrowDown',
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: replace 3 dots with an icon
|
// TODO: replace 3 dots with an icon
|
||||||
const VerticalDotsContainer = styled.div`
|
const VerticalDotsContainer = styled.div`
|
||||||
|
|
@ -126,51 +93,6 @@ const VerticalDotsTrigger = () => (
|
||||||
</VerticalDotsContainer>
|
</VerticalDotsContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface SliceHeaderControlsProps {
|
|
||||||
slice: {
|
|
||||||
description: string;
|
|
||||||
viz_type: string;
|
|
||||||
slice_name: string;
|
|
||||||
slice_id: number;
|
|
||||||
slice_description: string;
|
|
||||||
datasource: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
componentId: string;
|
|
||||||
dashboardId: number;
|
|
||||||
chartStatus: string;
|
|
||||||
isCached: boolean[];
|
|
||||||
cachedDttm: string[] | null;
|
|
||||||
isExpanded?: boolean;
|
|
||||||
updatedDttm: number | null;
|
|
||||||
isFullSize?: boolean;
|
|
||||||
isDescriptionExpanded?: boolean;
|
|
||||||
formData: QueryFormData;
|
|
||||||
exploreUrl: string;
|
|
||||||
|
|
||||||
forceRefresh: (sliceId: number, dashboardId: number) => void;
|
|
||||||
logExploreChart?: (sliceId: number) => void;
|
|
||||||
logEvent?: (eventName: string, eventData?: object) => void;
|
|
||||||
toggleExpandSlice?: (sliceId: number) => void;
|
|
||||||
exportCSV?: (sliceId: number) => void;
|
|
||||||
exportPivotCSV?: (sliceId: number) => void;
|
|
||||||
exportFullCSV?: (sliceId: number) => void;
|
|
||||||
exportXLSX?: (sliceId: number) => void;
|
|
||||||
exportFullXLSX?: (sliceId: number) => void;
|
|
||||||
handleToggleFullSize: () => void;
|
|
||||||
|
|
||||||
addDangerToast: (message: string) => void;
|
|
||||||
addSuccessToast: (message: string) => void;
|
|
||||||
|
|
||||||
supersetCanExplore?: boolean;
|
|
||||||
supersetCanShare?: boolean;
|
|
||||||
supersetCanCSV?: boolean;
|
|
||||||
|
|
||||||
crossFiltersEnabled?: boolean;
|
|
||||||
}
|
|
||||||
type SliceHeaderControlsPropsWithRouter = SliceHeaderControlsProps &
|
|
||||||
RouteComponentProps;
|
|
||||||
|
|
||||||
const dropdownIconsStyles = css`
|
const dropdownIconsStyles = css`
|
||||||
&&.anticon > .anticon:first-child {
|
&&.anticon > .anticon:first-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
|
@ -178,353 +100,7 @@ const dropdownIconsStyles = css`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/**
|
const SliceHeaderControls = (props: SliceHeaderControlsProps) => {
|
||||||
* A MenuItem can be recognized in the tree by the presence of a ref
|
|
||||||
*
|
|
||||||
* @param children
|
|
||||||
* @param currentKeys
|
|
||||||
* @returns an array of keys
|
|
||||||
*/
|
|
||||||
const extractMenuItemRefs = (child: MenuItemChildType): RefObject<any>[] => {
|
|
||||||
// check that child has props
|
|
||||||
const childProps: Record<string, any> = child?.props;
|
|
||||||
// loop through each prop
|
|
||||||
if (childProps) {
|
|
||||||
const arrayProps = Object.values(childProps);
|
|
||||||
// check if any is of type ref MenuItem
|
|
||||||
const refs = arrayProps.filter(ref => isAntdMenuItemRef(ref));
|
|
||||||
return refs;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Recursively extracts keys from menu items
|
|
||||||
*
|
|
||||||
* @param children
|
|
||||||
* @param currentKeys
|
|
||||||
* @returns an array of keys and their refs
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const extractMenuItemsKeys = (
|
|
||||||
children: MenuItemChildType[],
|
|
||||||
currentKeys?: { key: string; ref?: RefObject<any> }[],
|
|
||||||
): { key: string; ref?: RefObject<any> }[] => {
|
|
||||||
const allKeys = currentKeys || [];
|
|
||||||
const arrayChildren = ensureIsArray(children);
|
|
||||||
|
|
||||||
arrayChildren.forEach((child: MenuItemChildType) => {
|
|
||||||
const isMenuItem = isAntdMenuItem(child);
|
|
||||||
const refs = extractMenuItemRefs(child);
|
|
||||||
// key is immediately available in a standard MenuItem
|
|
||||||
if (isMenuItem) {
|
|
||||||
const { key } = child;
|
|
||||||
if (key) {
|
|
||||||
allKeys.push({
|
|
||||||
key,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// one or more menu items refs are available
|
|
||||||
if (refs.length) {
|
|
||||||
allKeys.push(
|
|
||||||
...refs.map(ref => ({ key: ref.current.props.eventKey, ref })),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// continue to extract keys from nested children
|
|
||||||
if (child?.props?.children) {
|
|
||||||
const childKeys = extractMenuItemsKeys(child.props.children, allKeys);
|
|
||||||
allKeys.push(...childKeys);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return allKeys;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a map of keys and their types for a MenuItem
|
|
||||||
* Individual refs can be given to extract keys from nested items
|
|
||||||
* Refs can be used to control the event handlers of the menu items
|
|
||||||
*
|
|
||||||
* @param itemChildren
|
|
||||||
* @param type
|
|
||||||
* @returns a map of keys and their types
|
|
||||||
*/
|
|
||||||
const extractMenuItemsKeyMap = (
|
|
||||||
children: MenuItemChildType,
|
|
||||||
): Record<string, any> => {
|
|
||||||
const keysMap: Record<string, any> = {};
|
|
||||||
const childrenArray = ensureIsArray(children);
|
|
||||||
|
|
||||||
childrenArray.forEach((child: MenuItemChildType) => {
|
|
||||||
const isMenuItem = isAntdMenuItem(child);
|
|
||||||
const isSubmenu = isAntdMenuSubmenu(child);
|
|
||||||
const menuItemsRefs = extractMenuItemRefs(child);
|
|
||||||
|
|
||||||
// key is immediately available in MenuItem or SubMenu
|
|
||||||
if (isMenuItem || isSubmenu) {
|
|
||||||
const directKey = child?.key;
|
|
||||||
if (directKey) {
|
|
||||||
keysMap[directKey] = {};
|
|
||||||
keysMap[directKey].type = isSubmenu
|
|
||||||
? MenuItemKeyEnum.SubMenu
|
|
||||||
: MenuItemKeyEnum.MenuItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// one or more menu items refs are available
|
|
||||||
if (menuItemsRefs.length) {
|
|
||||||
menuItemsRefs.forEach(ref => {
|
|
||||||
const key = ref.current.props.eventKey;
|
|
||||||
keysMap[key] = {};
|
|
||||||
keysMap[key].type = isSubmenu
|
|
||||||
? MenuItemKeyEnum.SubMenu
|
|
||||||
: MenuItemKeyEnum.MenuItem;
|
|
||||||
keysMap[key].parent = child.key;
|
|
||||||
keysMap[key].ref = ref;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// if it has children must check for the presence of menu items
|
|
||||||
if (child?.props?.children) {
|
|
||||||
const theChildren = child?.props?.children;
|
|
||||||
const childKeys = extractMenuItemsKeys(theChildren);
|
|
||||||
childKeys.forEach(keyMap => {
|
|
||||||
const k = keyMap.key;
|
|
||||||
keysMap[k] = {};
|
|
||||||
keysMap[k].type = MenuItemKeyEnum.SubMenuItem;
|
|
||||||
keysMap[k].parent = child.key;
|
|
||||||
if (keyMap.ref) {
|
|
||||||
keysMap[k].ref = keyMap.ref;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return keysMap;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Determines the next key to select based on the current key and direction
|
|
||||||
*
|
|
||||||
* @param keys
|
|
||||||
* @param keysMap
|
|
||||||
* @param currentKeyIndex
|
|
||||||
* @param direction
|
|
||||||
* @returns the selected key and the open key
|
|
||||||
*/
|
|
||||||
const getNavigationKeys = (
|
|
||||||
keys: string[],
|
|
||||||
keysMap: Record<string, any>,
|
|
||||||
currentKeyIndex: number,
|
|
||||||
direction = 'up',
|
|
||||||
) => {
|
|
||||||
const step = direction === 'up' ? -1 : 1;
|
|
||||||
const skipStep = direction === 'up' ? -2 : 2;
|
|
||||||
const keysLen = direction === 'up' ? 0 : keys.length;
|
|
||||||
const mathFn = direction === 'up' ? Math.max : Math.min;
|
|
||||||
let openKey: string | undefined;
|
|
||||||
let selectedKey = keys[mathFn(currentKeyIndex + step, keysLen)];
|
|
||||||
|
|
||||||
// go to first key if current key is the last
|
|
||||||
if (!selectedKey) {
|
|
||||||
return { selectedKey: keys[0], openKey: undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSubMenu = keysMap[selectedKey]?.type === MenuItemKeyEnum.SubMenu;
|
|
||||||
if (isSubMenu) {
|
|
||||||
// this is a submenu, skip to first submenu item
|
|
||||||
selectedKey = keys[mathFn(currentKeyIndex + skipStep, keysLen)];
|
|
||||||
}
|
|
||||||
// re-evaulate if current selected key is a submenu or submenu item
|
|
||||||
if (!isSubMenuOrItemType(keysMap[selectedKey].type)) {
|
|
||||||
openKey = undefined;
|
|
||||||
} else {
|
|
||||||
const parentKey = keysMap[selectedKey].parent;
|
|
||||||
if (parentKey) {
|
|
||||||
openKey = parentKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { selectedKey, openKey };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const handleDropdownNavigation = (
|
|
||||||
e: KeyboardEvent<HTMLElement>,
|
|
||||||
dropdownIsOpen: boolean,
|
|
||||||
menu: ReactElement,
|
|
||||||
toggleDropdown: () => void,
|
|
||||||
setSelectedKeys: (keys: string[]) => void,
|
|
||||||
setOpenKeys: (keys: string[]) => void,
|
|
||||||
) => {
|
|
||||||
if (e.key === NAV_KEYS.tab && !dropdownIsOpen) {
|
|
||||||
return; // if tab, continue with system tab navigation
|
|
||||||
}
|
|
||||||
const menuProps = menu.props || {};
|
|
||||||
const keysMap = extractMenuItemsKeyMap(menuProps.children);
|
|
||||||
const keys = Object.keys(keysMap);
|
|
||||||
const { selectedKeys = [] } = menuProps;
|
|
||||||
const currentKeyIndex = keys.indexOf(selectedKeys[0]);
|
|
||||||
|
|
||||||
switch (e.key) {
|
|
||||||
// toggle the dropdown on keypress
|
|
||||||
case ACTION_KEYS.enter:
|
|
||||||
case ACTION_KEYS.spacebar:
|
|
||||||
case ACTION_KEYS.space:
|
|
||||||
if (selectedKeys.length) {
|
|
||||||
const currentKey = selectedKeys[0];
|
|
||||||
const currentKeyConf = keysMap[selectedKeys];
|
|
||||||
// when a menu item is selected, then trigger
|
|
||||||
// the menu item's onClick handler
|
|
||||||
menuProps.onClick?.({ key: currentKey, domEvent: e });
|
|
||||||
// trigger click handle on ref
|
|
||||||
if (currentKeyConf?.ref) {
|
|
||||||
const refMenuItemProps = currentKeyConf.ref.current.props;
|
|
||||||
refMenuItemProps.onClick?.({
|
|
||||||
key: currentKey,
|
|
||||||
domEvent: e,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// clear out/deselect keys
|
|
||||||
setSelectedKeys([]);
|
|
||||||
// close submenus
|
|
||||||
setOpenKeys([]);
|
|
||||||
// put focus back on menu trigger
|
|
||||||
e.currentTarget.focus();
|
|
||||||
}
|
|
||||||
// if nothing was selected, or after selecting new menu item,
|
|
||||||
toggleDropdown();
|
|
||||||
break;
|
|
||||||
// select the menu items going down
|
|
||||||
case NAV_KEYS.down:
|
|
||||||
case NAV_KEYS.tab && !e.shiftKey: {
|
|
||||||
const { selectedKey, openKey } = getNavigationKeys(
|
|
||||||
keys,
|
|
||||||
keysMap,
|
|
||||||
currentKeyIndex,
|
|
||||||
'down',
|
|
||||||
);
|
|
||||||
setSelectedKeys([selectedKey]);
|
|
||||||
setOpenKeys(openKey ? [openKey] : []);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// select the menu items going up
|
|
||||||
case NAV_KEYS.up:
|
|
||||||
case NAV_KEYS.tab && e.shiftKey: {
|
|
||||||
const { selectedKey, openKey } = getNavigationKeys(
|
|
||||||
keys,
|
|
||||||
keysMap,
|
|
||||||
currentKeyIndex,
|
|
||||||
'up',
|
|
||||||
);
|
|
||||||
setSelectedKeys([selectedKey]);
|
|
||||||
setOpenKeys(openKey ? [openKey] : []);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case NAV_KEYS.escape:
|
|
||||||
// close dropdown menu
|
|
||||||
toggleDropdown();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ViewResultsModalTrigger = ({
|
|
||||||
canExplore,
|
|
||||||
exploreUrl,
|
|
||||||
triggerNode,
|
|
||||||
modalTitle,
|
|
||||||
modalBody,
|
|
||||||
showModal = false,
|
|
||||||
setShowModal,
|
|
||||||
}: {
|
|
||||||
canExplore?: boolean;
|
|
||||||
exploreUrl: string;
|
|
||||||
triggerNode: ReactChild;
|
|
||||||
modalTitle: ReactChild;
|
|
||||||
modalBody: ReactChild;
|
|
||||||
showModal: boolean;
|
|
||||||
setShowModal: (showModal: boolean) => void;
|
|
||||||
}) => {
|
|
||||||
const history = useHistory();
|
|
||||||
const exploreChart = () => history.push(exploreUrl);
|
|
||||||
const theme = useTheme();
|
|
||||||
const openModal = useCallback(() => setShowModal(true), []);
|
|
||||||
const closeModal = useCallback(() => setShowModal(false), []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
data-test="span-modal-trigger"
|
|
||||||
onClick={openModal}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
{triggerNode}
|
|
||||||
</span>
|
|
||||||
{(() => (
|
|
||||||
<Modal
|
|
||||||
css={css`
|
|
||||||
.ant-modal-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
show={showModal}
|
|
||||||
onHide={closeModal}
|
|
||||||
closable
|
|
||||||
title={modalTitle}
|
|
||||||
footer={
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
buttonStyle="secondary"
|
|
||||||
buttonSize="small"
|
|
||||||
onClick={exploreChart}
|
|
||||||
disabled={!canExplore}
|
|
||||||
tooltip={
|
|
||||||
!canExplore
|
|
||||||
? t(
|
|
||||||
'You do not have sufficient permissions to edit the chart',
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('Edit chart')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
buttonStyle="primary"
|
|
||||||
buttonSize="small"
|
|
||||||
onClick={closeModal}
|
|
||||||
css={css`
|
|
||||||
margin-left: ${theme.gridUnit * 2}px;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{t('Close')}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
responsive
|
|
||||||
resizable
|
|
||||||
resizableConfig={{
|
|
||||||
minHeight: theme.gridUnit * 128,
|
|
||||||
minWidth: theme.gridUnit * 128,
|
|
||||||
defaultSize: {
|
|
||||||
width: 'auto',
|
|
||||||
height: '75vh',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
draggable
|
|
||||||
destroyOnClose
|
|
||||||
>
|
|
||||||
{modalBody}
|
|
||||||
</Modal>
|
|
||||||
))()}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SliceHeaderControls = (props: SliceHeaderControlsPropsWithRouter) => {
|
|
||||||
const [dropdownIsOpen, setDropdownIsOpen] = useState(false);
|
const [dropdownIsOpen, setDropdownIsOpen] = useState(false);
|
||||||
const [tableModalIsOpen, setTableModalIsOpen] = useState(false);
|
const [tableModalIsOpen, setTableModalIsOpen] = useState(false);
|
||||||
const [drillModalIsOpen, setDrillModalIsOpen] = useState(false);
|
const [drillModalIsOpen, setDrillModalIsOpen] = useState(false);
|
||||||
|
|
@ -559,19 +135,7 @@ const SliceHeaderControls = (props: SliceHeaderControlsPropsWithRouter) => {
|
||||||
.get(props.slice.viz_type)
|
.get(props.slice.viz_type)
|
||||||
?.behaviors?.includes(Behavior.InteractiveChart);
|
?.behaviors?.includes(Behavior.InteractiveChart);
|
||||||
const canExplore = props.supersetCanExplore;
|
const canExplore = props.supersetCanExplore;
|
||||||
const canDatasourceSamples = useSelector((state: RootState) =>
|
const { canDrillToDetail, canViewQuery, canViewTable } = usePermissions();
|
||||||
findPermission('can_samples', 'Datasource', state.user?.roles),
|
|
||||||
);
|
|
||||||
const canDrill = useSelector((state: RootState) =>
|
|
||||||
findPermission('can_drill', 'Dashboard', state.user?.roles),
|
|
||||||
);
|
|
||||||
const canDrillToDetail = (canExplore || canDrill) && canDatasourceSamples;
|
|
||||||
const canViewQuery = useSelector((state: RootState) =>
|
|
||||||
findPermission('can_view_query', 'Dashboard', state.user?.roles),
|
|
||||||
);
|
|
||||||
const canViewTable = useSelector((state: RootState) =>
|
|
||||||
findPermission('can_view_chart_as_table', 'Dashboard', state.user?.roles),
|
|
||||||
);
|
|
||||||
const refreshChart = () => {
|
const refreshChart = () => {
|
||||||
if (props.updatedDttm) {
|
if (props.updatedDttm) {
|
||||||
props.forceRefresh(props.slice.slice_id, props.dashboardId);
|
props.forceRefresh(props.slice.slice_id, props.dashboardId);
|
||||||
|
|
@ -965,4 +529,4 @@ const SliceHeaderControls = (props: SliceHeaderControlsPropsWithRouter) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(SliceHeaderControls);
|
export default SliceHeaderControls;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* 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 { QueryFormData } from '@superset-ui/core';
|
||||||
|
|
||||||
|
export interface SliceHeaderControlsProps {
|
||||||
|
slice: {
|
||||||
|
description: string;
|
||||||
|
viz_type: string;
|
||||||
|
slice_name: string;
|
||||||
|
slice_id: number;
|
||||||
|
slice_description: string;
|
||||||
|
datasource: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
componentId: string;
|
||||||
|
dashboardId: number;
|
||||||
|
chartStatus: string;
|
||||||
|
isCached: boolean[];
|
||||||
|
cachedDttm: string[] | null;
|
||||||
|
isExpanded?: boolean;
|
||||||
|
updatedDttm: number | null;
|
||||||
|
isFullSize?: boolean;
|
||||||
|
isDescriptionExpanded?: boolean;
|
||||||
|
formData: QueryFormData;
|
||||||
|
exploreUrl: string;
|
||||||
|
|
||||||
|
forceRefresh: (sliceId: number, dashboardId: number) => void;
|
||||||
|
logExploreChart?: (sliceId: number) => void;
|
||||||
|
logEvent?: (eventName: string, eventData?: object) => void;
|
||||||
|
toggleExpandSlice?: (sliceId: number) => void;
|
||||||
|
exportCSV?: (sliceId: number) => void;
|
||||||
|
exportPivotCSV?: (sliceId: number) => void;
|
||||||
|
exportFullCSV?: (sliceId: number) => void;
|
||||||
|
exportXLSX?: (sliceId: number) => void;
|
||||||
|
exportFullXLSX?: (sliceId: number) => void;
|
||||||
|
handleToggleFullSize: () => void;
|
||||||
|
|
||||||
|
addDangerToast: (message: string) => void;
|
||||||
|
addSuccessToast: (message: string) => void;
|
||||||
|
|
||||||
|
supersetCanExplore?: boolean;
|
||||||
|
supersetCanShare?: boolean;
|
||||||
|
supersetCanCSV?: boolean;
|
||||||
|
|
||||||
|
crossFiltersEnabled?: boolean;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
isAntdMenuItem,
|
||||||
|
isAntdMenuItemRef,
|
||||||
|
isAntdMenuSubmenu,
|
||||||
|
isSubMenuOrItemType,
|
||||||
|
MenuItemChildType,
|
||||||
|
MenuItemKeyEnum,
|
||||||
|
} from 'src/components/Menu';
|
||||||
|
import { KeyboardEvent, ReactElement, RefObject } from 'react';
|
||||||
|
import { ensureIsArray } from '@superset-ui/core';
|
||||||
|
|
||||||
|
const ACTION_KEYS = {
|
||||||
|
enter: 'Enter',
|
||||||
|
spacebar: 'Spacebar',
|
||||||
|
space: ' ',
|
||||||
|
};
|
||||||
|
|
||||||
|
const NAV_KEYS = {
|
||||||
|
tab: 'Tab',
|
||||||
|
escape: 'Escape',
|
||||||
|
up: 'ArrowUp',
|
||||||
|
down: 'ArrowDown',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A MenuItem can be recognized in the tree by the presence of a ref
|
||||||
|
*
|
||||||
|
* @param children
|
||||||
|
* @param currentKeys
|
||||||
|
* @returns an array of keys
|
||||||
|
*/
|
||||||
|
const extractMenuItemRefs = (child: MenuItemChildType): RefObject<any>[] => {
|
||||||
|
// check that child has props
|
||||||
|
const childProps: Record<string, any> = child?.props;
|
||||||
|
// loop through each prop
|
||||||
|
if (childProps) {
|
||||||
|
const arrayProps = Object.values(childProps);
|
||||||
|
// check if any is of type ref MenuItem
|
||||||
|
return arrayProps.filter(ref => isAntdMenuItemRef(ref));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively extracts keys from menu items
|
||||||
|
*
|
||||||
|
* @param children
|
||||||
|
* @param currentKeys
|
||||||
|
* @returns an array of keys and their refs
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const extractMenuItemsKeys = (
|
||||||
|
children: MenuItemChildType[],
|
||||||
|
currentKeys?: { key: string; ref?: RefObject<any> }[],
|
||||||
|
): { key: string; ref?: RefObject<any> }[] => {
|
||||||
|
const allKeys = currentKeys || [];
|
||||||
|
const arrayChildren = ensureIsArray(children);
|
||||||
|
|
||||||
|
arrayChildren.forEach((child: MenuItemChildType) => {
|
||||||
|
const isMenuItem = isAntdMenuItem(child);
|
||||||
|
const refs = extractMenuItemRefs(child);
|
||||||
|
// key is immediately available in a standard MenuItem
|
||||||
|
if (isMenuItem) {
|
||||||
|
const { key } = child;
|
||||||
|
if (key) {
|
||||||
|
allKeys.push({
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// one or more menu items refs are available
|
||||||
|
if (refs.length) {
|
||||||
|
allKeys.push(
|
||||||
|
...refs.map(ref => ({ key: ref.current.props.eventKey, ref })),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// continue to extract keys from nested children
|
||||||
|
if (child?.props?.children) {
|
||||||
|
const childKeys = extractMenuItemsKeys(child.props.children, allKeys);
|
||||||
|
allKeys.push(...childKeys);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return allKeys;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a map of keys and their types for a MenuItem
|
||||||
|
* Individual refs can be given to extract keys from nested items
|
||||||
|
* Refs can be used to control the event handlers of the menu items
|
||||||
|
*
|
||||||
|
* @param itemChildren
|
||||||
|
* @param type
|
||||||
|
* @returns a map of keys and their types
|
||||||
|
*/
|
||||||
|
const extractMenuItemsKeyMap = (
|
||||||
|
children: MenuItemChildType,
|
||||||
|
): Record<string, any> => {
|
||||||
|
const keysMap: Record<string, any> = {};
|
||||||
|
const childrenArray = ensureIsArray(children);
|
||||||
|
|
||||||
|
childrenArray.forEach((child: MenuItemChildType) => {
|
||||||
|
const isMenuItem = isAntdMenuItem(child);
|
||||||
|
const isSubmenu = isAntdMenuSubmenu(child);
|
||||||
|
const menuItemsRefs = extractMenuItemRefs(child);
|
||||||
|
|
||||||
|
// key is immediately available in MenuItem or SubMenu
|
||||||
|
if (isMenuItem || isSubmenu) {
|
||||||
|
const directKey = child?.key;
|
||||||
|
if (directKey) {
|
||||||
|
keysMap[directKey] = {};
|
||||||
|
keysMap[directKey].type = isSubmenu
|
||||||
|
? MenuItemKeyEnum.SubMenu
|
||||||
|
: MenuItemKeyEnum.MenuItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// one or more menu items refs are available
|
||||||
|
if (menuItemsRefs.length) {
|
||||||
|
menuItemsRefs.forEach(ref => {
|
||||||
|
const key = ref.current.props.eventKey;
|
||||||
|
keysMap[key] = {};
|
||||||
|
keysMap[key].type = isSubmenu
|
||||||
|
? MenuItemKeyEnum.SubMenu
|
||||||
|
: MenuItemKeyEnum.MenuItem;
|
||||||
|
keysMap[key].parent = child.key;
|
||||||
|
keysMap[key].ref = ref;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it has children must check for the presence of menu items
|
||||||
|
if (child?.props?.children) {
|
||||||
|
const theChildren = child?.props?.children;
|
||||||
|
const childKeys = extractMenuItemsKeys(theChildren);
|
||||||
|
childKeys.forEach(keyMap => {
|
||||||
|
const k = keyMap.key;
|
||||||
|
keysMap[k] = {};
|
||||||
|
keysMap[k].type = MenuItemKeyEnum.SubMenuItem;
|
||||||
|
keysMap[k].parent = child.key;
|
||||||
|
if (keyMap.ref) {
|
||||||
|
keysMap[k].ref = keyMap.ref;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return keysMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Determines the next key to select based on the current key and direction
|
||||||
|
*
|
||||||
|
* @param keys
|
||||||
|
* @param keysMap
|
||||||
|
* @param currentKeyIndex
|
||||||
|
* @param direction
|
||||||
|
* @returns the selected key and the open key
|
||||||
|
*/
|
||||||
|
const getNavigationKeys = (
|
||||||
|
keys: string[],
|
||||||
|
keysMap: Record<string, any>,
|
||||||
|
currentKeyIndex: number,
|
||||||
|
direction = 'up',
|
||||||
|
) => {
|
||||||
|
const step = direction === 'up' ? -1 : 1;
|
||||||
|
const skipStep = direction === 'up' ? -2 : 2;
|
||||||
|
const keysLen = direction === 'up' ? 0 : keys.length;
|
||||||
|
const mathFn = direction === 'up' ? Math.max : Math.min;
|
||||||
|
let openKey: string | undefined;
|
||||||
|
let selectedKey = keys[mathFn(currentKeyIndex + step, keysLen)];
|
||||||
|
|
||||||
|
// go to first key if current key is the last
|
||||||
|
if (!selectedKey) {
|
||||||
|
return { selectedKey: keys[0], openKey: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSubMenu = keysMap[selectedKey]?.type === MenuItemKeyEnum.SubMenu;
|
||||||
|
if (isSubMenu) {
|
||||||
|
// this is a submenu, skip to first submenu item
|
||||||
|
selectedKey = keys[mathFn(currentKeyIndex + skipStep, keysLen)];
|
||||||
|
}
|
||||||
|
// re-evaulate if current selected key is a submenu or submenu item
|
||||||
|
if (!isSubMenuOrItemType(keysMap[selectedKey].type)) {
|
||||||
|
openKey = undefined;
|
||||||
|
} else {
|
||||||
|
const parentKey = keysMap[selectedKey].parent;
|
||||||
|
if (parentKey) {
|
||||||
|
openKey = parentKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { selectedKey, openKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleDropdownNavigation = (
|
||||||
|
e: KeyboardEvent<HTMLElement>,
|
||||||
|
dropdownIsOpen: boolean,
|
||||||
|
menu: ReactElement,
|
||||||
|
toggleDropdown: () => void,
|
||||||
|
setSelectedKeys: (keys: string[]) => void,
|
||||||
|
setOpenKeys: (keys: string[]) => void,
|
||||||
|
) => {
|
||||||
|
if (e.key === NAV_KEYS.tab && !dropdownIsOpen) {
|
||||||
|
return; // if tab, continue with system tab navigation
|
||||||
|
}
|
||||||
|
const menuProps = menu.props || {};
|
||||||
|
const keysMap = extractMenuItemsKeyMap(menuProps.children);
|
||||||
|
const keys = Object.keys(keysMap);
|
||||||
|
const { selectedKeys = [] } = menuProps;
|
||||||
|
const currentKeyIndex = keys.indexOf(selectedKeys[0]);
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
// toggle the dropdown on keypress
|
||||||
|
case ACTION_KEYS.enter:
|
||||||
|
case ACTION_KEYS.spacebar:
|
||||||
|
case ACTION_KEYS.space:
|
||||||
|
if (selectedKeys.length) {
|
||||||
|
const currentKey = selectedKeys[0];
|
||||||
|
const currentKeyConf = keysMap[selectedKeys];
|
||||||
|
// when a menu item is selected, then trigger
|
||||||
|
// the menu item's onClick handler
|
||||||
|
menuProps.onClick?.({ key: currentKey, domEvent: e });
|
||||||
|
// trigger click handle on ref
|
||||||
|
if (currentKeyConf?.ref) {
|
||||||
|
const refMenuItemProps = currentKeyConf.ref.current.props;
|
||||||
|
refMenuItemProps.onClick?.({
|
||||||
|
key: currentKey,
|
||||||
|
domEvent: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// clear out/deselect keys
|
||||||
|
setSelectedKeys([]);
|
||||||
|
// close submenus
|
||||||
|
setOpenKeys([]);
|
||||||
|
// put focus back on menu trigger
|
||||||
|
e.currentTarget.focus();
|
||||||
|
}
|
||||||
|
// if nothing was selected, or after selecting new menu item,
|
||||||
|
toggleDropdown();
|
||||||
|
break;
|
||||||
|
// select the menu items going down
|
||||||
|
case NAV_KEYS.down:
|
||||||
|
case NAV_KEYS.tab && !e.shiftKey: {
|
||||||
|
const { selectedKey, openKey } = getNavigationKeys(
|
||||||
|
keys,
|
||||||
|
keysMap,
|
||||||
|
currentKeyIndex,
|
||||||
|
'down',
|
||||||
|
);
|
||||||
|
setSelectedKeys([selectedKey]);
|
||||||
|
setOpenKeys(openKey ? [openKey] : []);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// select the menu items going up
|
||||||
|
case NAV_KEYS.up:
|
||||||
|
case NAV_KEYS.tab && e.shiftKey: {
|
||||||
|
const { selectedKey, openKey } = getNavigationKeys(
|
||||||
|
keys,
|
||||||
|
keysMap,
|
||||||
|
currentKeyIndex,
|
||||||
|
'up',
|
||||||
|
);
|
||||||
|
setSelectedKeys([selectedKey]);
|
||||||
|
setOpenKeys(openKey ? [openKey] : []);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case NAV_KEYS.escape:
|
||||||
|
// close dropdown menu
|
||||||
|
toggleDropdown();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -38,6 +38,12 @@ export const usePermissions = () => {
|
||||||
);
|
);
|
||||||
const canDrillBy = (canExplore || canDrill) && canWriteExploreFormData;
|
const canDrillBy = (canExplore || canDrill) && canWriteExploreFormData;
|
||||||
const canDrillToDetail = (canExplore || canDrill) && canDatasourceSamples;
|
const canDrillToDetail = (canExplore || canDrill) && canDatasourceSamples;
|
||||||
|
const canViewQuery = useSelector((state: RootState) =>
|
||||||
|
findPermission('can_view_query', 'Dashboard', state.user?.roles),
|
||||||
|
);
|
||||||
|
const canViewTable = useSelector((state: RootState) =>
|
||||||
|
findPermission('can_view_chart_as_table', 'Dashboard', state.user?.roles),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canExplore,
|
canExplore,
|
||||||
|
|
@ -47,5 +53,7 @@ export const usePermissions = () => {
|
||||||
canDrill,
|
canDrill,
|
||||||
canDrillBy,
|
canDrillBy,
|
||||||
canDrillToDetail,
|
canDrillToDetail,
|
||||||
|
canViewQuery,
|
||||||
|
canViewTable,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue