mirror of
https://github.com/apache/superset.git
synced 2024-09-06 13:57:40 -04:00
feat(explore): Redesign of Run/Save buttons (#19558)
* feat(explore): Move save button to header, run button to bottom of control panel * Make the tabs sticky * Add error icon to Data tab * Show message when creating chart and all controls are filled correctly * Add tests and storybook * Fix tests * Disable save button when control have errors * Fix types * Apply code review comments * Replace styled with css * Remove unused import
This commit is contained in:
parent
32239b04aa
commit
c8304a2821
@ -18,7 +18,7 @@
|
||||
*/
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { styled, logging, t } from '@superset-ui/core';
|
||||
import { styled, logging, t, ensureIsArray } from '@superset-ui/core';
|
||||
|
||||
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||
import { PLACEHOLDER_DATASOURCE } from 'src/dashboard/constants';
|
||||
@ -288,6 +288,23 @@ class Chart extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!isLoading &&
|
||||
!chartAlert &&
|
||||
isFaded &&
|
||||
ensureIsArray(queriesResponse).length === 0
|
||||
) {
|
||||
return (
|
||||
<EmptyStateBig
|
||||
title={t('Your chart is ready to go!')}
|
||||
description={t(
|
||||
'Click on "Create chart" button in the control panel on the left to preview a visualization',
|
||||
)}
|
||||
image="chart.svg"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
onError={this.handleRenderContainerFailure}
|
||||
|
@ -30,7 +30,7 @@ import {
|
||||
ControlPanelsContainerProps,
|
||||
} from 'src/explore/components/ControlPanelsContainer';
|
||||
|
||||
describe('ControlPanelsContainer2', () => {
|
||||
describe('ControlPanelsContainer', () => {
|
||||
beforeAll(() => {
|
||||
getChartControlPanelRegistry().registerValue('table', {
|
||||
controlPanelSections: [
|
||||
@ -90,6 +90,10 @@ describe('ControlPanelsContainer2', () => {
|
||||
form_data: getFormDataFromControls(controls),
|
||||
isDatasourceMetaLoading: false,
|
||||
exploreState: {},
|
||||
chart: {
|
||||
queriesResponse: null,
|
||||
chartStatus: 'success',
|
||||
},
|
||||
} as ControlPanelsContainerProps;
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@
|
||||
*/
|
||||
/* eslint camelcase: 0 */
|
||||
import React, {
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
@ -33,6 +34,7 @@ import {
|
||||
QueryFormData,
|
||||
DatasourceType,
|
||||
css,
|
||||
SupersetTheme,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelSectionConfig,
|
||||
@ -54,10 +56,12 @@ import { getSectionsToRender } from 'src/explore/controlUtils';
|
||||
import { ExploreActions } from 'src/explore/actions/exploreActions';
|
||||
import { ExplorePageState } from 'src/explore/reducers/getInitialState';
|
||||
import { ChartState } from 'src/explore/types';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
|
||||
import ControlRow from './ControlRow';
|
||||
import Control from './Control';
|
||||
import { ControlPanelAlert } from './ControlPanelAlert';
|
||||
import { RunQueryButton } from './RunQueryButton';
|
||||
|
||||
export type ControlPanelsContainerProps = {
|
||||
exploreState: ExplorePageState['explore'];
|
||||
@ -67,6 +71,11 @@ export type ControlPanelsContainerProps = {
|
||||
controls: Record<string, ControlState>;
|
||||
form_data: QueryFormData;
|
||||
isDatasourceMetaLoading: boolean;
|
||||
errorMessage: ReactNode;
|
||||
onQuery: () => void;
|
||||
onStop: () => void;
|
||||
canStopQuery: boolean;
|
||||
chartIsStale: boolean;
|
||||
};
|
||||
|
||||
export type ExpandedControlPanelSectionConfig = Omit<
|
||||
@ -76,13 +85,33 @@ export type ExpandedControlPanelSectionConfig = Omit<
|
||||
controlSetRows: ExpandedControlItem[][];
|
||||
};
|
||||
|
||||
const actionButtonsContainerStyles = (theme: SupersetTheme) => css`
|
||||
display: flex;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: ${theme.gridUnit * 4}px;
|
||||
background: linear-gradient(
|
||||
transparent,
|
||||
${theme.colors.grayscale.light5} ${theme.opacity.mediumLight}
|
||||
);
|
||||
|
||||
& > button {
|
||||
min-width: 156px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Styles = styled.div`
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
overflow-x: visible;
|
||||
|
||||
// Resizable add overflow-y: auto as a style to this div
|
||||
// To override it, we need to use !important
|
||||
overflow: visible !important;
|
||||
#controlSections {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
.nav-tabs {
|
||||
@ -105,15 +134,22 @@ const Styles = styled.div`
|
||||
`;
|
||||
|
||||
const ControlPanelsTabs = styled(Tabs)`
|
||||
.ant-tabs-nav-list {
|
||||
width: ${({ fullWidth }) => (fullWidth ? '100%' : '50%')};
|
||||
}
|
||||
.ant-tabs-content-holder {
|
||||
overflow: visible;
|
||||
}
|
||||
.ant-tabs-tabpane {
|
||||
${({ theme, fullWidth }) => css`
|
||||
height: 100%;
|
||||
}
|
||||
overflow: visible;
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.ant-tabs-nav-list {
|
||||
width: ${fullWidth ? '100%' : '50%'};
|
||||
}
|
||||
.ant-tabs-tabpane {
|
||||
height: 100%;
|
||||
}
|
||||
.ant-tabs-content-holder {
|
||||
padding-top: ${theme.gridUnit * 4}px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const isTimeSection = (section: ControlPanelSectionConfig): boolean =>
|
||||
@ -350,7 +386,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
||||
box-shadow: none;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: ${theme.gridUnit * 10}px;
|
||||
padding-bottom: ${theme.gridUnit * 16}px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
@ -432,6 +468,32 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
||||
[handleClearFormClick, handleContinueClick, hasControlsTransferred],
|
||||
);
|
||||
|
||||
const dataTabTitle = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<span>{t('Data')}</span>
|
||||
{props.errorMessage && (
|
||||
<span
|
||||
css={(theme: SupersetTheme) => css`
|
||||
font-size: ${theme.typography.sizes.xs}px;
|
||||
margin-left: ${theme.gridUnit * 2}px;
|
||||
`}
|
||||
>
|
||||
{' '}
|
||||
<Tooltip
|
||||
id="query-error-tooltip"
|
||||
placement="right"
|
||||
title={props.errorMessage}
|
||||
>
|
||||
<i className="fa fa-exclamation-circle text-danger fa-lg" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
[props.errorMessage],
|
||||
);
|
||||
|
||||
const controlPanelRegistry = getChartControlPanelRegistry();
|
||||
if (
|
||||
!controlPanelRegistry.has(props.form_data.viz_type) &&
|
||||
@ -448,8 +510,9 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
||||
id="controlSections"
|
||||
data-test="control-tabs"
|
||||
fullWidth={showCustomizeTab}
|
||||
allowOverflow={false}
|
||||
>
|
||||
<Tabs.TabPane key="query" tab={t('Data')}>
|
||||
<Tabs.TabPane key="query" tab={dataTabTitle}>
|
||||
<Collapse
|
||||
bordered
|
||||
defaultActiveKey={expandedQuerySections}
|
||||
@ -475,6 +538,17 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
||||
</Tabs.TabPane>
|
||||
)}
|
||||
</ControlPanelsTabs>
|
||||
<div css={actionButtonsContainerStyles}>
|
||||
<RunQueryButton
|
||||
onQuery={props.onQuery}
|
||||
onStop={props.onStop}
|
||||
errorMessage={props.errorMessage}
|
||||
loading={props.chart.chartStatus === 'loading'}
|
||||
isNewChart={!props.chart.queriesResponse}
|
||||
canStopQuery={props.canStopQuery}
|
||||
chartIsStale={props.chartIsStale}
|
||||
/>
|
||||
</div>
|
||||
</Styles>
|
||||
);
|
||||
};
|
||||
|
@ -86,8 +86,8 @@ const MenuItemWithCheckboxContainer = styled.div`
|
||||
|
||||
const MenuTrigger = styled(Button)`
|
||||
${({ theme }) => css`
|
||||
width: ${theme.gridUnit * 6}px;
|
||||
height: ${theme.gridUnit * 6}px;
|
||||
width: ${theme.gridUnit * 8}px;
|
||||
height: ${theme.gridUnit * 8}px;
|
||||
padding: 0;
|
||||
border: 1px solid ${theme.colors.primary.dark2};
|
||||
|
||||
@ -425,7 +425,7 @@ const ExploreAdditionalActionsMenu = ({
|
||||
>
|
||||
<Icons.MoreHoriz
|
||||
iconColor={theme.colors.primary.dark2}
|
||||
iconSize={theme.typography.sizes.m}
|
||||
iconSize="l"
|
||||
/>
|
||||
</MenuTrigger>
|
||||
</AntdDropdown>
|
||||
|
@ -90,6 +90,7 @@ const createProps = () => ({
|
||||
user: {
|
||||
userId: 1,
|
||||
},
|
||||
onSaveChart: jest.fn(),
|
||||
});
|
||||
|
||||
test('Cancelling changes to the properties should reset previous properties', () => {
|
||||
@ -115,3 +116,17 @@ test('Cancelling changes to the properties should reset previous properties', ()
|
||||
|
||||
expect(screen.getByDisplayValue(prevChartName)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Save chart', () => {
|
||||
const props = createProps();
|
||||
render(<ExploreHeader {...props} />, { useRedux: true });
|
||||
userEvent.click(screen.getByText('Save'));
|
||||
expect(props.onSaveChart).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Save disabled', () => {
|
||||
const props = createProps();
|
||||
render(<ExploreHeader {...props} saveDisabled />, { useRedux: true });
|
||||
userEvent.click(screen.getByText('Save'));
|
||||
expect(props.onSaveChart).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -24,7 +24,6 @@ import {
|
||||
CategoricalColorNamespace,
|
||||
css,
|
||||
SupersetClient,
|
||||
styled,
|
||||
t,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
@ -36,9 +35,12 @@ import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
|
||||
import { chartPropShape } from 'src/dashboard/util/propShapes';
|
||||
import AlteredSliceTag from 'src/components/AlteredSliceTag';
|
||||
import FaveStar from 'src/components/FaveStar';
|
||||
import Button from 'src/components/Button';
|
||||
import Icons from 'src/components/Icons';
|
||||
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
||||
import { sliceUpdated } from 'src/explore/actions/exploreActions';
|
||||
import CertifiedBadge from 'src/components/CertifiedBadge';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import ExploreAdditionalActionsMenu from '../ExploreAdditionalActionsMenu';
|
||||
import { ChartEditableTitle } from './ChartEditableTitle';
|
||||
|
||||
@ -55,60 +57,58 @@ const propTypes = {
|
||||
ownState: PropTypes.object,
|
||||
timeout: PropTypes.number,
|
||||
chart: chartPropShape,
|
||||
saveDisabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
${({ theme }) => css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
|
||||
span[role='button'] {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.title-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
margin-right: ${theme.gridUnit * 12}px;
|
||||
}
|
||||
|
||||
.right-button-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> .btn-group {
|
||||
flex: 0 0 auto;
|
||||
margin-left: ${theme.gridUnit}px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-button {
|
||||
color: ${theme.colors.grayscale.base};
|
||||
margin: 0 ${theme.gridUnit * 1.5}px 0 ${theme.gridUnit}px;
|
||||
}
|
||||
`}
|
||||
const saveButtonStyles = theme => css`
|
||||
color: ${theme.colors.primary.dark2};
|
||||
& > span[role='img'] {
|
||||
margin-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButtons = styled.span`
|
||||
${({ theme }) => css`
|
||||
const headerStyles = theme => css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
|
||||
span[role='button'] {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.title-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: ${theme.gridUnit * 2}px;
|
||||
min-width: 0;
|
||||
margin-right: ${theme.gridUnit * 12}px;
|
||||
}
|
||||
|
||||
& .fave-unfave-icon {
|
||||
padding: 0 ${theme.gridUnit}px;
|
||||
.right-button-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
const buttonsStyles = theme => css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: ${theme.gridUnit * 2}px;
|
||||
|
||||
& .fave-unfave-icon {
|
||||
padding: 0 ${theme.gridUnit}px;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
const saveButtonContainerStyles = theme => css`
|
||||
margin-right: ${theme.gridUnit * 2}px;
|
||||
`;
|
||||
|
||||
export class ExploreChartHeader extends React.PureComponent {
|
||||
@ -231,11 +231,13 @@ export class ExploreChartHeader extends React.PureComponent {
|
||||
isStarred,
|
||||
sliceUpdated,
|
||||
sliceName,
|
||||
onSaveChart,
|
||||
saveDisabled,
|
||||
} = this.props;
|
||||
const { latestQueryFormData, sliceFormData } = chart;
|
||||
const oldSliceName = slice?.slice_name;
|
||||
return (
|
||||
<StyledHeader id="slice-header">
|
||||
<div id="slice-header" css={headerStyles}>
|
||||
<div className="title-panel">
|
||||
<ChartEditableTitle
|
||||
title={sliceName}
|
||||
@ -248,7 +250,7 @@ export class ExploreChartHeader extends React.PureComponent {
|
||||
placeholder={t('Add the name of the chart')}
|
||||
/>
|
||||
{slice && (
|
||||
<StyledButtons>
|
||||
<span css={buttonsStyles}>
|
||||
{slice.certified_by && (
|
||||
<CertifiedBadge
|
||||
certifiedBy={slice.certified_by}
|
||||
@ -279,10 +281,31 @@ export class ExploreChartHeader extends React.PureComponent {
|
||||
currentFormData={{ ...formData, chartTitle: sliceName }}
|
||||
/>
|
||||
)}
|
||||
</StyledButtons>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="right-button-panel">
|
||||
<Tooltip
|
||||
title={
|
||||
saveDisabled
|
||||
? t('Add required control values to save chart')
|
||||
: null
|
||||
}
|
||||
>
|
||||
{/* needed to wrap button in a div - antd tooltip doesn't work with disabled button */}
|
||||
<div css={saveButtonContainerStyles}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
onClick={onSaveChart}
|
||||
disabled={saveDisabled}
|
||||
data-test="query-save-button"
|
||||
css={saveButtonStyles}
|
||||
>
|
||||
<Icons.SaveOutlined iconSize="l" />
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<ExploreAdditionalActionsMenu
|
||||
onOpenInEditor={actions.redirectSQLLab}
|
||||
onOpenPropertiesModal={this.openPropertiesModal}
|
||||
@ -292,7 +315,7 @@ export class ExploreChartHeader extends React.PureComponent {
|
||||
canAddReports={this.canAddReports()}
|
||||
/>
|
||||
</div>
|
||||
</StyledHeader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { getChartControlPanelRegistry } from '@superset-ui/core';
|
||||
import { MemoryRouter, Route } from 'react-router-dom';
|
||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
@ -111,13 +112,17 @@ test('generates a different form_data param when one is provided and is mounting
|
||||
});
|
||||
|
||||
test('reuses the same form_data param when updating', async () => {
|
||||
getChartControlPanelRegistry().registerValue('table', {
|
||||
controlPanelSections: [],
|
||||
});
|
||||
const replaceState = jest.spyOn(window.history, 'replaceState');
|
||||
const pushState = jest.spyOn(window.history, 'pushState');
|
||||
await waitFor(() => renderWithRouter());
|
||||
expect(replaceState.mock.calls.length).toBe(1);
|
||||
userEvent.click(screen.getByText('Run'));
|
||||
userEvent.click(screen.getByText('Update chart'));
|
||||
await waitFor(() => expect(pushState.mock.calls.length).toBe(1));
|
||||
expect(replaceState.mock.calls[0]).toEqual(pushState.mock.calls[0]);
|
||||
replaceState.mockRestore();
|
||||
pushState.mockRestore();
|
||||
getChartControlPanelRegistry().remove('table');
|
||||
});
|
||||
|
@ -48,7 +48,6 @@ import { useTabId } from 'src/hooks/useTabId';
|
||||
import ExploreChartPanel from '../ExploreChartPanel';
|
||||
import ConnectedControlPanelsContainer from '../ControlPanelsContainer';
|
||||
import SaveModal from '../SaveModal';
|
||||
import QueryAndSaveBtns from '../QueryAndSaveBtns';
|
||||
import DataSourcePanel from '../DatasourcePanel';
|
||||
import { mountExploreUrl } from '../../exploreUtils';
|
||||
import { areObjectsEqual } from '../../../reduxUtils';
|
||||
@ -477,8 +476,7 @@ function ExploreViewContainer(props) {
|
||||
props.actions.logEvent(LOG_ACTIONS_CHANGE_EXPLORE_CONTROLS);
|
||||
}
|
||||
|
||||
function renderErrorMessage() {
|
||||
// Returns an error message as a node if any errors are in the store
|
||||
const errorMessage = useMemo(() => {
|
||||
const controlsWithErrors = Object.values(props.controls).filter(
|
||||
control =>
|
||||
control.validationErrors && control.validationErrors.length > 0,
|
||||
@ -512,7 +510,7 @@ function ExploreViewContainer(props) {
|
||||
errorMessage = <div style={{ textAlign: 'left' }}>{errors}</div>;
|
||||
}
|
||||
return errorMessage;
|
||||
}
|
||||
}, [props.controls]);
|
||||
|
||||
function renderChartContainer() {
|
||||
return (
|
||||
@ -520,7 +518,7 @@ function ExploreViewContainer(props) {
|
||||
width={width}
|
||||
height={height}
|
||||
{...props}
|
||||
errorMessage={renderErrorMessage()}
|
||||
errorMessage={errorMessage}
|
||||
refreshOverlayVisible={chartIsStale}
|
||||
onQuery={onQuery}
|
||||
/>
|
||||
@ -558,6 +556,8 @@ function ExploreViewContainer(props) {
|
||||
chart={props.chart}
|
||||
user={props.user}
|
||||
reports={props.reports}
|
||||
onSaveChart={toggleModal}
|
||||
saveDisabled={errorMessage || props.chart.chartStatus === 'loading'}
|
||||
/>
|
||||
</ExploreHeaderContainer>
|
||||
<ExplorePanelContainer id="explore-container">
|
||||
@ -669,16 +669,6 @@ function ExploreViewContainer(props) {
|
||||
enable={{ right: true }}
|
||||
className="col-sm-3 explore-column controls-column"
|
||||
>
|
||||
<QueryAndSaveBtns
|
||||
canAdd={!!(props.can_add || props.can_overwrite)}
|
||||
onQuery={onQuery}
|
||||
onSave={toggleModal}
|
||||
onStop={onStop}
|
||||
loading={props.chart.chartStatus === 'loading'}
|
||||
chartIsStale={chartIsStale}
|
||||
errorMessage={renderErrorMessage()}
|
||||
datasourceType={props.datasource_type}
|
||||
/>
|
||||
<ConnectedControlPanelsContainer
|
||||
exploreState={props.exploreState}
|
||||
actions={props.actions}
|
||||
@ -687,6 +677,11 @@ function ExploreViewContainer(props) {
|
||||
chart={props.chart}
|
||||
datasource_type={props.datasource_type}
|
||||
isDatasourceMetaLoading={props.isDatasourceMetaLoading}
|
||||
onQuery={onQuery}
|
||||
onStop={onStop}
|
||||
canStopQuery={props.can_add || props.can_overwrite}
|
||||
errorMessage={errorMessage}
|
||||
chartIsStale={chartIsStale}
|
||||
/>
|
||||
</Resizable>
|
||||
<div
|
||||
|
@ -1,60 +0,0 @@
|
||||
/**
|
||||
* 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 from 'react';
|
||||
import { styledMount as mount } from 'spec/helpers/theming';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import QueryAndSaveButtons from 'src/explore/components/QueryAndSaveBtns';
|
||||
import Button from 'src/components/Button';
|
||||
|
||||
describe('QueryAndSaveButtons', () => {
|
||||
const defaultProps = {
|
||||
canAdd: true,
|
||||
onQuery: sinon.spy(),
|
||||
};
|
||||
|
||||
// It must render
|
||||
it('renders', () => {
|
||||
expect(
|
||||
React.isValidElement(<QueryAndSaveButtons {...defaultProps} />),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
// Test the output
|
||||
describe('output', () => {
|
||||
const wrapper = mount(<QueryAndSaveButtons {...defaultProps} />);
|
||||
|
||||
it('renders 2 buttons', () => {
|
||||
expect(wrapper.find(Button)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('renders buttons with correct text', () => {
|
||||
expect(wrapper.find(Button).at(0).text().trim()).toBe('Run');
|
||||
expect(wrapper.find(Button).at(1).text().trim()).toBe('Save');
|
||||
});
|
||||
|
||||
it('calls onQuery when query button is clicked', () => {
|
||||
const queryButton = wrapper
|
||||
.find('[data-test="run-query-button"]')
|
||||
.hostNodes();
|
||||
queryButton.simulate('click');
|
||||
expect(defaultProps.onQuery.called).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,124 +0,0 @@
|
||||
/**
|
||||
* 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 from 'react';
|
||||
import ButtonGroup from 'src/components/ButtonGroup';
|
||||
import { t, useTheme } from '@superset-ui/core';
|
||||
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import Button, { ButtonStyle, OnClickHandler } from 'src/components/Button';
|
||||
|
||||
export type QueryAndSaveBtnsProps = {
|
||||
canAdd: boolean;
|
||||
onQuery: OnClickHandler;
|
||||
onSave: OnClickHandler;
|
||||
onStop: OnClickHandler;
|
||||
loading?: boolean;
|
||||
chartIsStale?: boolean;
|
||||
errorMessage: React.ReactElement | undefined;
|
||||
};
|
||||
|
||||
export default function QueryAndSaveBtns(props: QueryAndSaveBtnsProps) {
|
||||
const {
|
||||
canAdd,
|
||||
onQuery = () => {},
|
||||
onSave = () => {},
|
||||
onStop = () => {},
|
||||
loading,
|
||||
chartIsStale,
|
||||
errorMessage,
|
||||
} = props;
|
||||
let qryButtonStyle: ButtonStyle = 'tertiary';
|
||||
if (errorMessage) {
|
||||
qryButtonStyle = 'danger';
|
||||
} else if (chartIsStale) {
|
||||
qryButtonStyle = 'primary';
|
||||
}
|
||||
|
||||
const saveButtonDisabled = errorMessage ? true : loading;
|
||||
const qryOrStopButton = loading ? (
|
||||
<Button
|
||||
onClick={onStop}
|
||||
buttonStyle="warning"
|
||||
buttonSize="small"
|
||||
disabled={!canAdd}
|
||||
>
|
||||
<i className="fa fa-stop-circle-o" /> {t('Stop')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
buttonSize="small"
|
||||
onClick={onQuery}
|
||||
buttonStyle={qryButtonStyle}
|
||||
disabled={!!errorMessage}
|
||||
data-test="run-query-button"
|
||||
>
|
||||
<i className="fa fa-bolt" /> {t('Run')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingTop: theme.gridUnit * 2,
|
||||
paddingRight: theme.gridUnit * 2,
|
||||
paddingBottom: 0,
|
||||
paddingLeft: theme.gridUnit * 4,
|
||||
'& button': {
|
||||
width: 100,
|
||||
},
|
||||
'.errMsg': {
|
||||
marginLeft: theme.gridUnit * 4,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ButtonGroup className="query-and-save">
|
||||
{qryOrStopButton}
|
||||
<Button
|
||||
buttonStyle="tertiary"
|
||||
buttonSize="small"
|
||||
data-target="#save_modal"
|
||||
data-toggle="modal"
|
||||
disabled={saveButtonDisabled}
|
||||
onClick={onSave}
|
||||
data-test="query-save-button"
|
||||
>
|
||||
<i className="fa fa-plus-circle" /> {t('Save')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
{errorMessage && (
|
||||
<span className="errMsg">
|
||||
{' '}
|
||||
<Tooltip
|
||||
id="query-error-tooltip"
|
||||
placement="right"
|
||||
title={errorMessage}
|
||||
>
|
||||
<i className="fa fa-exclamation-circle text-danger fa-lg" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -17,29 +17,31 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import QueryAndSaveBtns, { QueryAndSaveBtnsProps } from './QueryAndSaveBtns';
|
||||
import { RunQueryButton, RunQueryButtonProps } from '.';
|
||||
|
||||
export default {
|
||||
title: 'QueryAndSaveBtns',
|
||||
component: QueryAndSaveBtns,
|
||||
title: 'RunQueryButton',
|
||||
component: RunQueryButton,
|
||||
};
|
||||
|
||||
export const InteractiveQueryAndSaveBtnsProps = (
|
||||
args: QueryAndSaveBtnsProps,
|
||||
) => <QueryAndSaveBtns {...args} />;
|
||||
export const InteractiveRunQueryButtonProps = (args: RunQueryButtonProps) => (
|
||||
<RunQueryButton {...args} />
|
||||
);
|
||||
|
||||
InteractiveQueryAndSaveBtnsProps.args = {
|
||||
canAdd: true,
|
||||
InteractiveRunQueryButtonProps.args = {
|
||||
canStopQuery: true,
|
||||
loading: false,
|
||||
errorMessage: null,
|
||||
isNewChart: false,
|
||||
chartIsStale: true,
|
||||
};
|
||||
|
||||
InteractiveQueryAndSaveBtnsProps.argTypes = {
|
||||
InteractiveRunQueryButtonProps.argTypes = {
|
||||
onQuery: { action: 'onQuery' },
|
||||
onSave: { action: 'onSave' },
|
||||
onStop: { action: 'onStop' },
|
||||
};
|
||||
|
||||
InteractiveQueryAndSaveBtnsProps.story = {
|
||||
InteractiveRunQueryButtonProps.story = {
|
||||
parameters: {
|
||||
knobs: {
|
||||
disable: true,
|
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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 from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import { RunQueryButton } from './index';
|
||||
|
||||
const createProps = (overrides: Record<string, any> = {}) => ({
|
||||
loading: false,
|
||||
onQuery: jest.fn(),
|
||||
onStop: jest.fn(),
|
||||
errorMessage: null,
|
||||
isNewChart: false,
|
||||
canStopQuery: true,
|
||||
chartIsStale: true,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test('renders update chart button', () => {
|
||||
const props = createProps();
|
||||
render(<RunQueryButton {...props} />);
|
||||
expect(screen.getByText('Update chart')).toBeVisible();
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
expect(props.onQuery).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('renders create chart button', () => {
|
||||
const props = createProps({ isNewChart: true });
|
||||
render(<RunQueryButton {...props} />);
|
||||
expect(screen.getByText('Create chart')).toBeVisible();
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
expect(props.onQuery).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('renders disabled button', () => {
|
||||
const props = createProps({ errorMessage: 'error' });
|
||||
render(<RunQueryButton {...props} />);
|
||||
expect(screen.getByText('Update chart')).toBeVisible();
|
||||
expect(screen.getByRole('button')).toBeDisabled();
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
expect(props.onQuery).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('renders query running button', () => {
|
||||
const props = createProps({ loading: true });
|
||||
render(<RunQueryButton {...props} />);
|
||||
expect(screen.getByText('Stop')).toBeVisible();
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
expect(props.onStop).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('renders query running button disabled', () => {
|
||||
const props = createProps({ loading: true, canStopQuery: false });
|
||||
render(<RunQueryButton {...props} />);
|
||||
expect(screen.getByText('Stop')).toBeVisible();
|
||||
expect(screen.getByRole('button')).toBeDisabled();
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
expect(props.onStop).not.toHaveBeenCalled();
|
||||
});
|
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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, { ReactNode } from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import Button from 'src/components/Button';
|
||||
|
||||
export type RunQueryButtonProps = {
|
||||
loading: boolean;
|
||||
onQuery: () => void;
|
||||
onStop: () => void;
|
||||
errorMessage: ReactNode;
|
||||
isNewChart: boolean;
|
||||
canStopQuery: boolean;
|
||||
chartIsStale: boolean;
|
||||
};
|
||||
|
||||
export const RunQueryButton = ({
|
||||
loading,
|
||||
onQuery,
|
||||
onStop,
|
||||
errorMessage,
|
||||
isNewChart,
|
||||
canStopQuery,
|
||||
chartIsStale,
|
||||
}: RunQueryButtonProps) =>
|
||||
loading ? (
|
||||
<Button onClick={onStop} buttonStyle="warning" disabled={!canStopQuery}>
|
||||
<i className="fa fa-stop-circle-o" /> {t('Stop')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={onQuery}
|
||||
buttonStyle={chartIsStale ? 'primary' : 'secondary'}
|
||||
disabled={!!errorMessage}
|
||||
data-test="run-query-button"
|
||||
>
|
||||
{isNewChart ? t('Create chart') : t('Update chart')}
|
||||
</Button>
|
||||
);
|
Loading…
Reference in New Issue
Block a user