feat(explore): Implement data panel redesign (#19751)

* feat(explore): Redesign of data panel

* Auto calculate chart panel height and width

* Add tests

* Fix e2e tests

* Increase collapsed data panel height
This commit is contained in:
Kamil Gabryjelski 2022-04-19 10:10:40 +02:00 committed by GitHub
parent 34323f9b5f
commit 594523e895
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 496 additions and 361 deletions

View File

@ -121,14 +121,12 @@ describe('Test datatable', () => {
cy.visitChartByName('Daily Totals');
});
it('Data Pane opens and loads results', () => {
cy.get('[data-test="data-tab"]').click();
cy.contains('Results').click();
cy.get('[data-test="row-count-label"]').contains('26 rows retrieved');
cy.contains('View results');
cy.get('.ant-empty-description').should('not.exist');
});
it('Datapane loads view samples', () => {
cy.get('[data-test="data-tab"]').click();
cy.contains('View samples').click();
cy.contains('Samples').click();
cy.get('[data-test="row-count-label"]').contains('1k rows retrieved');
cy.get('.ant-empty-description').should('not.exist');
});

View File

@ -67,41 +67,56 @@ export const CopyButton = styled(Button)`
}
`;
const CopyNode = (
<CopyButton buttonSize="xsmall" aria-label={t('Copy')}>
<i className="fa fa-clipboard" />
</CopyButton>
);
export const CopyToClipboardButton = ({
data,
columns,
}: {
data?: Record<string, any>;
columns?: string[];
}) => (
<CopyToClipboard
text={
data && columns ? prepareCopyToClipboardTabularData(data, columns) : ''
}
wrapped={false}
copyNode={CopyNode}
/>
);
}) => {
const theme = useTheme();
return (
<CopyToClipboard
text={
data && columns ? prepareCopyToClipboardTabularData(data, columns) : ''
}
wrapped={false}
copyNode={
<Icons.CopyOutlined
iconColor={theme.colors.grayscale.base}
iconSize="l"
aria-label={t('Copy')}
role="button"
css={css`
&.anticon > * {
line-height: 0;
}
`}
/>
}
/>
);
};
export const FilterInput = ({
onChangeHandler,
}: {
onChangeHandler(filterText: string): void;
}) => {
const theme = useTheme();
const debouncedChangeHandler = debounce(onChangeHandler, SLOW_DEBOUNCE);
return (
<Input
prefix={<Icons.Search iconColor={theme.colors.grayscale.base} />}
placeholder={t('Search')}
onChange={(event: any) => {
const filterText = event.target.value;
debouncedChangeHandler(filterText);
}}
css={css`
width: 200px;
margin-right: ${theme.gridUnit * 2}px;
`}
/>
);
};

View File

@ -21,7 +21,11 @@ import React from 'react';
import userEvent from '@testing-library/user-event';
import fetchMock from 'fetch-mock';
import * as copyUtils from 'src/utils/copy';
import { render, screen } from 'spec/helpers/testing-library';
import {
render,
screen,
waitForElementToBeRemoved,
} from 'spec/helpers/testing-library';
import { DataTablesPane } from '.';
const createProps = () => ({
@ -50,7 +54,6 @@ const createProps = () => ({
sort_y_axis: 'alpha_asc',
extra_form_data: {},
},
tableSectionHeight: 156.9,
chartStatus: 'rendered',
onCollapseChange: jest.fn(),
queriesResponse: [
@ -60,91 +63,162 @@ const createProps = () => ({
],
});
test('Rendering DataTablesPane correctly', () => {
const props = createProps();
render(<DataTablesPane {...props} />, { useRedux: true });
expect(screen.getByTestId('some-purposeful-instance')).toBeVisible();
expect(screen.getByRole('tablist')).toBeVisible();
expect(screen.getByRole('tab', { name: 'right Data' })).toBeVisible();
expect(screen.getByRole('img', { name: 'right' })).toBeVisible();
});
test('Should show tabs', async () => {
const props = createProps();
render(<DataTablesPane {...props} />, { useRedux: true });
expect(screen.queryByText('View results')).not.toBeInTheDocument();
expect(screen.queryByText('View samples')).not.toBeInTheDocument();
userEvent.click(await screen.findByText('Data'));
expect(await screen.findByText('View results')).toBeVisible();
expect(screen.getByText('View samples')).toBeVisible();
});
test('Should show tabs: View results', async () => {
const props = createProps();
render(<DataTablesPane {...props} />, {
useRedux: true,
describe('DataTablesPane', () => {
// Collapsed/expanded state depends on local storage
// We need to clear it manually - otherwise initial state would depend on the order of tests
beforeEach(() => {
localStorage.clear();
});
userEvent.click(await screen.findByText('Data'));
userEvent.click(await screen.findByText('View results'));
expect(screen.getByText('0 rows retrieved')).toBeVisible();
});
test('Should show tabs: View samples', async () => {
const props = createProps();
render(<DataTablesPane {...props} />, {
useRedux: true,
afterAll(() => {
localStorage.clear();
});
userEvent.click(await screen.findByText('Data'));
expect(screen.queryByText('0 rows retrieved')).not.toBeInTheDocument();
userEvent.click(await screen.findByText('View samples'));
expect(await screen.findByText('0 rows retrieved')).toBeVisible();
});
test('Should copy data table content correctly', async () => {
fetchMock.post(
'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A456%7D',
{
result: [
{
data: [{ __timestamp: 1230768000000, genre: 'Action' }],
colnames: ['__timestamp', 'genre'],
coltypes: [2, 1],
},
],
},
);
const copyToClipboardSpy = jest.spyOn(copyUtils, 'default');
const props = createProps();
render(
<DataTablesPane
{...{
...props,
chartStatus: 'success',
queriesResponse: [
test('Rendering DataTablesPane correctly', () => {
const props = createProps();
render(<DataTablesPane {...props} />, { useRedux: true });
expect(screen.getByText('Results')).toBeVisible();
expect(screen.getByText('Samples')).toBeVisible();
expect(screen.getByLabelText('Expand data panel')).toBeVisible();
});
test('Collapse/Expand buttons', async () => {
const props = createProps();
render(<DataTablesPane {...props} />, {
useRedux: true,
});
expect(
screen.queryByLabelText('Collapse data panel'),
).not.toBeInTheDocument();
userEvent.click(screen.getByLabelText('Expand data panel'));
expect(await screen.findByLabelText('Collapse data panel')).toBeVisible();
expect(
screen.queryByLabelText('Expand data panel'),
).not.toBeInTheDocument();
});
test('Should show tabs: View results', async () => {
const props = createProps();
render(<DataTablesPane {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByText('Results'));
expect(await screen.findByText('0 rows retrieved')).toBeVisible();
expect(await screen.findByLabelText('Collapse data panel')).toBeVisible();
localStorage.clear();
});
test('Should show tabs: View samples', async () => {
const props = createProps();
render(<DataTablesPane {...props} />, {
useRedux: true,
});
userEvent.click(screen.getByText('Samples'));
expect(await screen.findByText('0 rows retrieved')).toBeVisible();
expect(await screen.findByLabelText('Collapse data panel')).toBeVisible();
});
test('Should copy data table content correctly', async () => {
fetchMock.post(
'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A456%7D',
{
result: [
{
data: [{ __timestamp: 1230768000000, genre: 'Action' }],
colnames: ['__timestamp', 'genre'],
coltypes: [2, 1],
},
],
}}
/>,
{
useRedux: true,
initialState: {
explore: {
timeFormattedColumns: {
'34__table': ['__timestamp'],
},
);
const copyToClipboardSpy = jest.spyOn(copyUtils, 'default');
const props = createProps();
render(
<DataTablesPane
{...{
...props,
chartStatus: 'success',
queriesResponse: [
{
colnames: ['__timestamp', 'genre'],
coltypes: [2, 1],
},
],
}}
/>,
{
useRedux: true,
initialState: {
explore: {
timeFormattedColumns: {
'34__table': ['__timestamp'],
},
},
},
},
},
);
userEvent.click(await screen.findByText('Data'));
expect(await screen.findByText('1 rows retrieved')).toBeVisible();
);
userEvent.click(screen.getByText('Results'));
expect(await screen.findByText('1 rows retrieved')).toBeVisible();
userEvent.click(screen.getByRole('button', { name: 'Copy' }));
expect(copyToClipboardSpy).toHaveBeenCalledWith(
'2009-01-01 00:00:00\tAction\n',
);
fetchMock.done();
userEvent.click(screen.getByLabelText('Copy'));
expect(copyToClipboardSpy).toHaveBeenCalledWith(
'2009-01-01 00:00:00\tAction\n',
);
copyToClipboardSpy.mockRestore();
fetchMock.restore();
});
test('Search table', async () => {
fetchMock.post(
'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A456%7D',
{
result: [
{
data: [
{ __timestamp: 1230768000000, genre: 'Action' },
{ __timestamp: 1230768000010, genre: 'Horror' },
],
colnames: ['__timestamp', 'genre'],
coltypes: [2, 1],
},
],
},
);
const props = createProps();
render(
<DataTablesPane
{...{
...props,
chartStatus: 'success',
queriesResponse: [
{
colnames: ['__timestamp', 'genre'],
coltypes: [2, 1],
},
],
}}
/>,
{
useRedux: true,
initialState: {
explore: {
timeFormattedColumns: {
'34__table': ['__timestamp'],
},
},
},
},
);
userEvent.click(screen.getByText('Results'));
expect(await screen.findByText('2 rows retrieved')).toBeVisible();
expect(screen.getByText('Action')).toBeVisible();
expect(screen.getByText('Horror')).toBeVisible();
userEvent.type(screen.getByPlaceholderText('Search'), 'hor');
await waitForElementToBeRemoved(() => screen.queryByText('Action'));
expect(screen.getByText('Horror')).toBeVisible();
expect(screen.queryByText('Action')).not.toBeInTheDocument();
fetchMock.restore();
});
});

View File

@ -16,15 +16,23 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useState,
MouseEvent,
} from 'react';
import {
css,
ensureIsArray,
GenericDataType,
JsonObject,
styled,
t,
useTheme,
} from '@superset-ui/core';
import Collapse from 'src/components/Collapse';
import Icons from 'src/components/Icons';
import Tabs from 'src/components/Tabs';
import Loading from 'src/components/Loading';
import { EmptyStateMedium } from 'src/components/EmptyState';
@ -58,53 +66,58 @@ const getDefaultDataTablesState = (value: any) => ({
const DATA_TABLE_PAGE_SIZE = 50;
const DATAPANEL_KEY = 'data';
const TableControlsWrapper = styled.div`
display: flex;
align-items: center;
${({ theme }) => `
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: ${theme.gridUnit * 2}px;
span {
flex-shrink: 0;
}
span {
flex-shrink: 0;
}
`}
`;
const SouthPane = styled.div`
position: relative;
background-color: ${({ theme }) => theme.colors.grayscale.light5};
z-index: 5;
overflow: hidden;
`;
${({ theme }) => `
position: relative;
background-color: ${theme.colors.grayscale.light5};
z-index: 5;
overflow: hidden;
const TabsWrapper = styled.div<{ contentHeight: number }>`
height: ${({ contentHeight }) => contentHeight}px;
overflow: hidden;
.ant-tabs {
height: 100%;
}
.table-condensed {
height: 100%;
overflow: auto;
}
`;
.ant-tabs-content-holder {
height: 100%;
}
const CollapseWrapper = styled.div`
height: 100%;
.ant-tabs-content {
height: 100%;
}
.collapse-inner {
height: 100%;
.ant-collapse-item {
.ant-tabs-tabpane {
display: flex;
flex-direction: column;
height: 100%;
.ant-collapse-content {
height: calc(100% - ${({ theme }) => theme.gridUnit * 8}px);
.table-condensed {
height: 100%;
overflow: auto;
margin-bottom: ${theme.gridUnit * 4}px;
.ant-collapse-content-box {
padding-top: 0;
height: 100%;
.table {
margin-bottom: ${theme.gridUnit * 2}px;
}
}
.pagination-container > ul[role='navigation'] {
margin-top: 0;
}
}
}
`}
`;
const Error = styled.pre`
@ -117,7 +130,6 @@ interface DataTableProps {
datasource: string | undefined;
filterText: string;
data: object[] | undefined;
timeFormattedColumns: string[] | undefined;
isLoading: boolean;
error: string | undefined;
errorMessage: React.ReactElement | undefined;
@ -130,12 +142,12 @@ const DataTable = ({
datasource,
filterText,
data,
timeFormattedColumns,
isLoading,
error,
errorMessage,
type,
}: DataTableProps) => {
const timeFormattedColumns = useTimeFormattedColumns(datasource);
// this is to preserve the order of the columns, even if there are integer values,
// while also only grabbing the first column's keys
const columns = useTableColumns(
@ -185,9 +197,42 @@ const DataTable = ({
return null;
};
const TableControls = ({
data,
datasourceId,
onInputChange,
columnNames,
isLoading,
}: {
data: Record<string, any>[];
datasourceId?: string;
onInputChange: (input: string) => void;
columnNames: string[];
isLoading: boolean;
}) => {
const timeFormattedColumns = useTimeFormattedColumns(datasourceId);
const formattedData = useMemo(
() => applyFormattingToTabularData(data, timeFormattedColumns),
[data, timeFormattedColumns],
);
return (
<TableControlsWrapper>
<FilterInput onChangeHandler={onInputChange} />
<div
css={css`
display: flex;
align-items: center;
`}
>
<RowCount data={data} loading={isLoading} />
<CopyToClipboardButton data={formattedData} columns={columnNames} />
</div>
</TableControlsWrapper>
);
};
export const DataTablesPane = ({
queryFormData,
tableSectionHeight,
onCollapseChange,
chartStatus,
ownState,
@ -195,19 +240,19 @@ export const DataTablesPane = ({
queriesResponse,
}: {
queryFormData: Record<string, any>;
tableSectionHeight: number;
chartStatus: string;
ownState?: JsonObject;
onCollapseChange: (openPanelName: string) => void;
onCollapseChange: (isOpen: boolean) => void;
errorMessage?: JSX.Element;
queriesResponse: Record<string, any>;
}) => {
const theme = useTheme();
const [data, setData] = useState(getDefaultDataTablesState(undefined));
const [isLoading, setIsLoading] = useState(getDefaultDataTablesState(true));
const [columnNames, setColumnNames] = useState(getDefaultDataTablesState([]));
const [columnTypes, setColumnTypes] = useState(getDefaultDataTablesState([]));
const [error, setError] = useState(getDefaultDataTablesState(''));
const [filterText, setFilterText] = useState('');
const [filterText, setFilterText] = useState(getDefaultDataTablesState(''));
const [activeTabKey, setActiveTabKey] = useState<string>(
RESULT_TYPES.results,
);
@ -218,24 +263,6 @@ export const DataTablesPane = ({
getItem(LocalStorageKeys.is_datapanel_open, false),
);
const timeFormattedColumns = useTimeFormattedColumns(
queryFormData?.datasource,
);
const formattedData = useMemo(
() => ({
[RESULT_TYPES.results]: applyFormattingToTabularData(
data[RESULT_TYPES.results],
timeFormattedColumns,
),
[RESULT_TYPES.samples]: applyFormattingToTabularData(
data[RESULT_TYPES.samples],
timeFormattedColumns,
),
}),
[data, timeFormattedColumns],
);
const getData = useCallback(
(resultType: 'samples' | 'results') => {
setIsLoading(prevIsLoading => ({
@ -381,81 +408,121 @@ export const DataTablesPane = ({
errorMessage,
]);
const TableControls = (
<TableControlsWrapper>
<RowCount data={data[activeTabKey]} loading={isLoading[activeTabKey]} />
<CopyToClipboardButton
data={formattedData[activeTabKey]}
columns={columnNames[activeTabKey]}
/>
<FilterInput onChangeHandler={setFilterText} />
</TableControlsWrapper>
const handleCollapseChange = useCallback(
(isOpen: boolean) => {
onCollapseChange(isOpen);
setPanelOpen(isOpen);
},
[onCollapseChange],
);
const handleCollapseChange = (openPanelName: string) => {
onCollapseChange(openPanelName);
setPanelOpen(!!openPanelName);
};
const handleTabClick = useCallback(
(tabKey: string, e: MouseEvent) => {
if (!panelOpen) {
handleCollapseChange(true);
} else if (tabKey === activeTabKey) {
e.preventDefault();
handleCollapseChange(false);
}
setActiveTabKey(tabKey);
},
[activeTabKey, handleCollapseChange, panelOpen],
);
const CollapseButton = useMemo(() => {
const caretIcon = panelOpen ? (
<Icons.CaretUp
iconColor={theme.colors.grayscale.base}
aria-label={t('Collapse data panel')}
/>
) : (
<Icons.CaretDown
iconColor={theme.colors.grayscale.base}
aria-label={t('Expand data panel')}
/>
);
return (
<TableControlsWrapper>
{panelOpen ? (
<span
role="button"
tabIndex={0}
onClick={() => handleCollapseChange(false)}
>
{caretIcon}
</span>
) : (
<span
role="button"
tabIndex={0}
onClick={() => handleCollapseChange(true)}
>
{caretIcon}
</span>
)}
</TableControlsWrapper>
);
}, [handleCollapseChange, panelOpen, theme.colors.grayscale.base]);
return (
<SouthPane data-test="some-purposeful-instance">
<TabsWrapper contentHeight={tableSectionHeight}>
<CollapseWrapper data-test="data-tab">
<Collapse
accordion
bordered={false}
defaultActiveKey={panelOpen ? DATAPANEL_KEY : undefined}
onChange={handleCollapseChange}
bold
ghost
className="collapse-inner"
>
<Collapse.Panel header={t('Data')} key={DATAPANEL_KEY}>
<Tabs
fullWidth={false}
tabBarExtraContent={TableControls}
activeKey={activeTabKey}
onChange={setActiveTabKey}
>
<Tabs.TabPane
tab={t('View results')}
key={RESULT_TYPES.results}
>
<DataTable
isLoading={isLoading[RESULT_TYPES.results]}
data={data[RESULT_TYPES.results]}
datasource={queryFormData?.datasource}
timeFormattedColumns={timeFormattedColumns}
columnNames={columnNames[RESULT_TYPES.results]}
columnTypes={columnTypes[RESULT_TYPES.results]}
filterText={filterText}
error={error[RESULT_TYPES.results]}
errorMessage={errorMessage}
type={RESULT_TYPES.results}
/>
</Tabs.TabPane>
<Tabs.TabPane
tab={t('View samples')}
key={RESULT_TYPES.samples}
>
<DataTable
isLoading={isLoading[RESULT_TYPES.samples]}
data={data[RESULT_TYPES.samples]}
datasource={queryFormData?.datasource}
timeFormattedColumns={timeFormattedColumns}
columnNames={columnNames[RESULT_TYPES.samples]}
columnTypes={columnTypes[RESULT_TYPES.samples]}
filterText={filterText}
error={error[RESULT_TYPES.samples]}
errorMessage={errorMessage}
type={RESULT_TYPES.samples}
/>
</Tabs.TabPane>
</Tabs>
</Collapse.Panel>
</Collapse>
</CollapseWrapper>
</TabsWrapper>
<Tabs
fullWidth={false}
tabBarExtraContent={CollapseButton}
activeKey={panelOpen ? activeTabKey : ''}
onTabClick={handleTabClick}
>
<Tabs.TabPane tab={t('Results')} key={RESULT_TYPES.results}>
<TableControls
data={data[RESULT_TYPES.results]}
columnNames={columnNames[RESULT_TYPES.results]}
datasourceId={queryFormData?.datasource}
onInputChange={input =>
setFilterText(prevState => ({
...prevState,
[RESULT_TYPES.results]: input,
}))
}
isLoading={isLoading[RESULT_TYPES.results]}
/>
<DataTable
isLoading={isLoading[RESULT_TYPES.results]}
data={data[RESULT_TYPES.results]}
datasource={queryFormData?.datasource}
columnNames={columnNames[RESULT_TYPES.results]}
columnTypes={columnTypes[RESULT_TYPES.results]}
filterText={filterText[RESULT_TYPES.results]}
error={error[RESULT_TYPES.results]}
errorMessage={errorMessage}
type={RESULT_TYPES.results}
/>
</Tabs.TabPane>
<Tabs.TabPane tab={t('Samples')} key={RESULT_TYPES.samples}>
<TableControls
data={data[RESULT_TYPES.samples]}
columnNames={columnNames[RESULT_TYPES.samples]}
datasourceId={queryFormData?.datasource}
onInputChange={input =>
setFilterText(prevState => ({
...prevState,
[RESULT_TYPES.samples]: input,
}))
}
isLoading={isLoading[RESULT_TYPES.samples]}
/>
<DataTable
isLoading={isLoading[RESULT_TYPES.samples]}
data={data[RESULT_TYPES.samples]}
datasource={queryFormData?.datasource}
columnNames={columnNames[RESULT_TYPES.samples]}
columnTypes={columnTypes[RESULT_TYPES.samples]}
filterText={filterText[RESULT_TYPES.samples]}
error={error[RESULT_TYPES.samples]}
errorMessage={errorMessage}
type={RESULT_TYPES.samples}
/>
</Tabs.TabPane>
</Tabs>
</SouthPane>
);
};

View File

@ -19,7 +19,7 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import Split from 'react-split';
import { styled, SupersetClient, useTheme } from '@superset-ui/core';
import { css, styled, SupersetClient, useTheme } from '@superset-ui/core';
import { useResizeDetector } from 'react-resize-detector';
import { chartPropShape } from 'src/dashboard/util/propShapes';
import ChartContainer from 'src/components/Chart/ChartContainer';
@ -41,8 +41,6 @@ const propTypes = {
dashboardId: PropTypes.number,
column_formats: PropTypes.object,
containerId: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
width: PropTypes.string.isRequired,
isStarred: PropTypes.bool.isRequired,
slice: PropTypes.object,
sliceName: PropTypes.string,
@ -61,11 +59,8 @@ const propTypes = {
const GUTTER_SIZE_FACTOR = 1.25;
const CHART_PANEL_PADDING_HORIZ = 30;
const CHART_PANEL_PADDING_VERTICAL = 15;
const INITIAL_SIZES = [90, 10];
const MIN_SIZES = [300, 50];
const INITIAL_SIZES = [100, 0];
const MIN_SIZES = [300, 65];
const DEFAULT_SOUTH_PANE_HEIGHT_PERCENT = 40;
const Styles = styled.div`
@ -109,28 +104,42 @@ const Styles = styled.div`
}
`;
const ExploreChartPanel = props => {
const ExploreChartPanel = ({
chart,
slice,
vizType,
ownState,
triggerRender,
force,
datasource,
errorMessage,
form_data: formData,
onQuery,
refreshOverlayVisible,
actions,
timeout,
standalone,
}) => {
const theme = useTheme();
const gutterMargin = theme.gridUnit * GUTTER_SIZE_FACTOR;
const gutterHeight = theme.gridUnit * GUTTER_SIZE_FACTOR;
const { width: chartPanelWidth, ref: chartPanelRef } = useResizeDetector({
const {
width: chartPanelWidth,
height: chartPanelHeight,
ref: chartPanelRef,
} = useResizeDetector({
refreshMode: 'debounce',
refreshRate: 300,
});
const { height: pillsHeight, ref: pillsRef } = useResizeDetector({
refreshMode: 'debounce',
refreshRate: 1000,
});
const [splitSizes, setSplitSizes] = useState(
getItem(LocalStorageKeys.chart_split_sizes, INITIAL_SIZES),
);
const { slice } = props;
const updateQueryContext = useCallback(
async function fetchChartData() {
if (slice && slice.query_context === null) {
const queryContext = buildV1ChartDataPayload({
formData: slice.form_data,
force: props.force,
force,
resultFormat: 'json',
resultType: 'full',
setDataMask: null,
@ -154,34 +163,6 @@ const ExploreChartPanel = props => {
updateQueryContext();
}, [updateQueryContext]);
const calcSectionHeight = useCallback(
percent => {
let containerHeight = parseInt(props.height, 10);
if (pillsHeight) {
containerHeight -= pillsHeight;
}
return (
(containerHeight * percent) / 100 - (gutterHeight / 2 + gutterMargin)
);
},
[gutterHeight, gutterMargin, pillsHeight, props.height, props.standalone],
);
const [tableSectionHeight, setTableSectionHeight] = useState(
calcSectionHeight(INITIAL_SIZES[1]),
);
const recalcPanelSizes = useCallback(
([, southPercent]) => {
setTableSectionHeight(calcSectionHeight(southPercent));
},
[calcSectionHeight],
);
useEffect(() => {
recalcPanelSizes(splitSizes);
}, [recalcPanelSizes, splitSizes]);
useEffect(() => {
setItem(LocalStorageKeys.chart_split_sizes, splitSizes);
}, [splitSizes]);
@ -191,19 +172,19 @@ const ExploreChartPanel = props => {
};
const refreshCachedQuery = () => {
props.actions.postChartFormData(
props.form_data,
actions.postChartFormData(
formData,
true,
props.timeout,
props.chart.id,
timeout,
chart.id,
undefined,
props.ownState,
ownState,
);
};
const onCollapseChange = openPanelName => {
const onCollapseChange = useCallback(isOpen => {
let splitSizes;
if (!openPanelName) {
if (!isOpen) {
splitSizes = INITIAL_SIZES;
} else {
splitSizes = [
@ -212,53 +193,84 @@ const ExploreChartPanel = props => {
];
}
setSplitSizes(splitSizes);
};
const renderChart = useCallback(() => {
const { chart, vizType } = props;
const newHeight =
vizType === 'filter_box'
? calcSectionHeight(100) - CHART_PANEL_PADDING_VERTICAL
: calcSectionHeight(splitSizes[0]) - CHART_PANEL_PADDING_VERTICAL;
const chartWidth = chartPanelWidth - CHART_PANEL_PADDING_HORIZ;
return (
chartWidth > 0 && (
<ChartContainer
width={Math.floor(chartWidth)}
height={newHeight}
ownState={props.ownState}
annotationData={chart.annotationData}
chartAlert={chart.chartAlert}
chartStackTrace={chart.chartStackTrace}
chartId={chart.id}
chartStatus={chart.chartStatus}
triggerRender={props.triggerRender}
force={props.force}
datasource={props.datasource}
errorMessage={props.errorMessage}
formData={props.form_data}
onQuery={props.onQuery}
queriesResponse={chart.queriesResponse}
refreshOverlayVisible={props.refreshOverlayVisible}
setControlValue={props.actions.setControlValue}
timeout={props.timeout}
triggerQuery={chart.triggerQuery}
vizType={props.vizType}
/>
)
);
}, [calcSectionHeight, chartPanelWidth, props, splitSizes]);
}, []);
const renderChart = useCallback(
() => (
<div
css={css`
min-height: 0;
flex: 1;
`}
ref={chartPanelRef}
>
{chartPanelWidth && chartPanelHeight && (
<ChartContainer
width={Math.floor(chartPanelWidth)}
height={chartPanelHeight}
ownState={ownState}
annotationData={chart.annotationData}
chartAlert={chart.chartAlert}
chartStackTrace={chart.chartStackTrace}
chartId={chart.id}
chartStatus={chart.chartStatus}
triggerRender={triggerRender}
force={force}
datasource={datasource}
errorMessage={errorMessage}
formData={formData}
onQuery={onQuery}
queriesResponse={chart.queriesResponse}
refreshOverlayVisible={refreshOverlayVisible}
setControlValue={actions.setControlValue}
timeout={timeout}
triggerQuery={chart.triggerQuery}
vizType={vizType}
/>
)}
</div>
),
[
actions.setControlValue,
chart.annotationData,
chart.chartAlert,
chart.chartStackTrace,
chart.chartStatus,
chart.id,
chart.queriesResponse,
chart.triggerQuery,
chartPanelHeight,
chartPanelRef,
chartPanelWidth,
datasource,
errorMessage,
force,
formData,
onQuery,
ownState,
refreshOverlayVisible,
timeout,
triggerRender,
vizType,
],
);
const panelBody = useMemo(
() => (
<div className="panel-body" ref={chartPanelRef}>
<div
className="panel-body"
css={css`
display: flex;
flex-direction: column;
`}
>
<ChartPills
queriesResponse={props.chart.queriesResponse}
chartStatus={props.chart.chartStatus}
chartUpdateStartTime={props.chart.chartUpdateStartTime}
chartUpdateEndTime={props.chart.chartUpdateEndTime}
queriesResponse={chart.queriesResponse}
chartStatus={chart.chartStatus}
chartUpdateStartTime={chart.chartUpdateStartTime}
chartUpdateEndTime={chart.chartUpdateEndTime}
refreshCachedQuery={refreshCachedQuery}
rowLimit={props.form_data?.row_limit}
ref={pillsRef}
rowLimit={formData?.row_limit}
/>
{renderChart()}
</div>
@ -266,14 +278,9 @@ const ExploreChartPanel = props => {
[chartPanelRef, renderChart],
);
const standaloneChartBody = useMemo(
() => <div ref={chartPanelRef}>{renderChart()}</div>,
[chartPanelRef, renderChart],
);
const standaloneChartBody = useMemo(() => renderChart(), [renderChart]);
const [queryFormData, setQueryFormData] = useState(
props.chart.latestQueryFormData,
);
const [queryFormData, setQueryFormData] = useState(chart.latestQueryFormData);
useEffect(() => {
// only update when `latestQueryFormData` changes AND `triggerRender`
@ -281,13 +288,13 @@ const ExploreChartPanel = props => {
// as this can trigger a query downstream based on incomplete form data.
// (`latestQueryFormData` is only updated when a a valid request has been
// triggered).
if (!props.triggerRender) {
setQueryFormData(props.chart.latestQueryFormData);
if (!triggerRender) {
setQueryFormData(chart.latestQueryFormData);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.chart.latestQueryFormData]);
}, [chart.latestQueryFormData]);
if (props.standalone) {
if (standalone) {
// dom manipulation hack to get rid of the boostrap theme's body background
const standaloneClass = 'background-transparent';
const bodyClasses = document.body.className.split(' ');
@ -302,8 +309,8 @@ const ExploreChartPanel = props => {
});
return (
<Styles className="panel panel-default chart-container" ref={chartPanelRef}>
{props.vizType === 'filter_box' ? (
<Styles className="panel panel-default chart-container">
{vizType === 'filter_box' ? (
panelBody
) : (
<Split
@ -313,16 +320,16 @@ const ExploreChartPanel = props => {
gutterSize={gutterHeight}
onDragEnd={onDragEnd}
elementStyle={elementStyle}
expandToMin
>
{panelBody}
<DataTablesPane
ownState={props.ownState}
ownState={ownState}
queryFormData={queryFormData}
tableSectionHeight={tableSectionHeight}
onCollapseChange={onCollapseChange}
chartStatus={props.chart.chartStatus}
errorMessage={props.errorMessage}
queriesResponse={props.chart.queriesResponse}
chartStatus={chart.chartStatus}
errorMessage={errorMessage}
queriesResponse={chart.queriesResponse}
/>
</Split>
)}

View File

@ -63,8 +63,6 @@ import ConnectedExploreChartHeader from '../ExploreChartHeader';
const propTypes = {
...ExploreChartPanel.propTypes,
height: PropTypes.string,
width: PropTypes.string,
actions: PropTypes.object.isRequired,
datasource_type: PropTypes.string.isRequired,
dashboardId: PropTypes.number,
@ -135,6 +133,7 @@ const ExplorePanelContainer = styled.div`
flex: 1;
min-width: ${theme.gridUnit * 128}px;
border-left: 1px solid ${theme.colors.grayscale.light2};
padding: 0 ${theme.gridUnit * 4}px;
.panel {
margin-bottom: 0;
}
@ -172,23 +171,6 @@ const ExplorePanelContainer = styled.div`
`};
`;
const getWindowSize = () => ({
height: window.innerHeight,
width: window.innerWidth,
});
function useWindowSize({ delayMs = 250 } = {}) {
const [size, setSize] = useState(getWindowSize());
useEffect(() => {
const onWindowResize = debounce(() => setSize(getWindowSize()), delayMs);
window.addEventListener('resize', onWindowResize);
return () => window.removeEventListener('resize', onWindowResize);
}, []);
return size;
}
const updateHistory = debounce(
async (formData, datasetId, isReplace, standalone, force, title, tabId) => {
const payload = { ...formData };
@ -246,7 +228,6 @@ function ExploreViewContainer(props) {
const [lastQueriedControls, setLastQueriedControls] = useState(
props.controls,
);
const windowSize = useWindowSize();
const [showingModal, setShowingModal] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(false);
@ -254,11 +235,6 @@ function ExploreViewContainer(props) {
const tabId = useTabId();
const theme = useTheme();
const width = `${windowSize.width}px`;
const navHeight = props.standalone ? 0 : 120;
const height = props.forcedHeight
? `${props.forcedHeight}px`
: `${windowSize.height - navHeight}px`;
const defaultSidebarsWidth = {
controls_width: 320,
@ -515,8 +491,6 @@ function ExploreViewContainer(props) {
function renderChartContainer() {
return (
<ExploreChartPanel
width={width}
height={height}
{...props}
errorMessage={errorMessage}
refreshOverlayVisible={chartIsStale}