mirror of https://github.com/apache/superset.git
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:
parent
34323f9b5f
commit
594523e895
|
@ -121,14 +121,12 @@ describe('Test datatable', () => {
|
||||||
cy.visitChartByName('Daily Totals');
|
cy.visitChartByName('Daily Totals');
|
||||||
});
|
});
|
||||||
it('Data Pane opens and loads results', () => {
|
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.get('[data-test="row-count-label"]').contains('26 rows retrieved');
|
||||||
cy.contains('View results');
|
|
||||||
cy.get('.ant-empty-description').should('not.exist');
|
cy.get('.ant-empty-description').should('not.exist');
|
||||||
});
|
});
|
||||||
it('Datapane loads view samples', () => {
|
it('Datapane loads view samples', () => {
|
||||||
cy.get('[data-test="data-tab"]').click();
|
cy.contains('Samples').click();
|
||||||
cy.contains('View samples').click();
|
|
||||||
cy.get('[data-test="row-count-label"]').contains('1k rows retrieved');
|
cy.get('[data-test="row-count-label"]').contains('1k rows retrieved');
|
||||||
cy.get('.ant-empty-description').should('not.exist');
|
cy.get('.ant-empty-description').should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 = ({
|
export const CopyToClipboardButton = ({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
}: {
|
}: {
|
||||||
data?: Record<string, any>;
|
data?: Record<string, any>;
|
||||||
columns?: string[];
|
columns?: string[];
|
||||||
}) => (
|
}) => {
|
||||||
<CopyToClipboard
|
const theme = useTheme();
|
||||||
text={
|
return (
|
||||||
data && columns ? prepareCopyToClipboardTabularData(data, columns) : ''
|
<CopyToClipboard
|
||||||
}
|
text={
|
||||||
wrapped={false}
|
data && columns ? prepareCopyToClipboardTabularData(data, columns) : ''
|
||||||
copyNode={CopyNode}
|
}
|
||||||
/>
|
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 = ({
|
export const FilterInput = ({
|
||||||
onChangeHandler,
|
onChangeHandler,
|
||||||
}: {
|
}: {
|
||||||
onChangeHandler(filterText: string): void;
|
onChangeHandler(filterText: string): void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
const debouncedChangeHandler = debounce(onChangeHandler, SLOW_DEBOUNCE);
|
const debouncedChangeHandler = debounce(onChangeHandler, SLOW_DEBOUNCE);
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
|
prefix={<Icons.Search iconColor={theme.colors.grayscale.base} />}
|
||||||
placeholder={t('Search')}
|
placeholder={t('Search')}
|
||||||
onChange={(event: any) => {
|
onChange={(event: any) => {
|
||||||
const filterText = event.target.value;
|
const filterText = event.target.value;
|
||||||
debouncedChangeHandler(filterText);
|
debouncedChangeHandler(filterText);
|
||||||
}}
|
}}
|
||||||
|
css={css`
|
||||||
|
width: 200px;
|
||||||
|
margin-right: ${theme.gridUnit * 2}px;
|
||||||
|
`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,7 +21,11 @@ import React from 'react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
import * as copyUtils from 'src/utils/copy';
|
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 '.';
|
import { DataTablesPane } from '.';
|
||||||
|
|
||||||
const createProps = () => ({
|
const createProps = () => ({
|
||||||
|
@ -50,7 +54,6 @@ const createProps = () => ({
|
||||||
sort_y_axis: 'alpha_asc',
|
sort_y_axis: 'alpha_asc',
|
||||||
extra_form_data: {},
|
extra_form_data: {},
|
||||||
},
|
},
|
||||||
tableSectionHeight: 156.9,
|
|
||||||
chartStatus: 'rendered',
|
chartStatus: 'rendered',
|
||||||
onCollapseChange: jest.fn(),
|
onCollapseChange: jest.fn(),
|
||||||
queriesResponse: [
|
queriesResponse: [
|
||||||
|
@ -60,91 +63,162 @@ const createProps = () => ({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Rendering DataTablesPane correctly', () => {
|
describe('DataTablesPane', () => {
|
||||||
const props = createProps();
|
// Collapsed/expanded state depends on local storage
|
||||||
render(<DataTablesPane {...props} />, { useRedux: true });
|
// We need to clear it manually - otherwise initial state would depend on the order of tests
|
||||||
expect(screen.getByTestId('some-purposeful-instance')).toBeVisible();
|
beforeEach(() => {
|
||||||
expect(screen.getByRole('tablist')).toBeVisible();
|
localStorage.clear();
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
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 () => {
|
afterAll(() => {
|
||||||
const props = createProps();
|
localStorage.clear();
|
||||||
render(<DataTablesPane {...props} />, {
|
|
||||||
useRedux: true,
|
|
||||||
});
|
});
|
||||||
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 () => {
|
test('Rendering DataTablesPane correctly', () => {
|
||||||
fetchMock.post(
|
const props = createProps();
|
||||||
'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A456%7D',
|
render(<DataTablesPane {...props} />, { useRedux: true });
|
||||||
{
|
expect(screen.getByText('Results')).toBeVisible();
|
||||||
result: [
|
expect(screen.getByText('Samples')).toBeVisible();
|
||||||
{
|
expect(screen.getByLabelText('Expand data panel')).toBeVisible();
|
||||||
data: [{ __timestamp: 1230768000000, genre: 'Action' }],
|
});
|
||||||
colnames: ['__timestamp', 'genre'],
|
|
||||||
coltypes: [2, 1],
|
test('Collapse/Expand buttons', async () => {
|
||||||
},
|
const props = createProps();
|
||||||
],
|
render(<DataTablesPane {...props} />, {
|
||||||
},
|
useRedux: true,
|
||||||
);
|
});
|
||||||
const copyToClipboardSpy = jest.spyOn(copyUtils, 'default');
|
expect(
|
||||||
const props = createProps();
|
screen.queryByLabelText('Collapse data panel'),
|
||||||
render(
|
).not.toBeInTheDocument();
|
||||||
<DataTablesPane
|
userEvent.click(screen.getByLabelText('Expand data panel'));
|
||||||
{...{
|
expect(await screen.findByLabelText('Collapse data panel')).toBeVisible();
|
||||||
...props,
|
expect(
|
||||||
chartStatus: 'success',
|
screen.queryByLabelText('Expand data panel'),
|
||||||
queriesResponse: [
|
).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'],
|
colnames: ['__timestamp', 'genre'],
|
||||||
coltypes: [2, 1],
|
coltypes: [2, 1],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}}
|
},
|
||||||
/>,
|
);
|
||||||
{
|
const copyToClipboardSpy = jest.spyOn(copyUtils, 'default');
|
||||||
useRedux: true,
|
const props = createProps();
|
||||||
initialState: {
|
render(
|
||||||
explore: {
|
<DataTablesPane
|
||||||
timeFormattedColumns: {
|
{...{
|
||||||
'34__table': ['__timestamp'],
|
...props,
|
||||||
|
chartStatus: 'success',
|
||||||
|
queriesResponse: [
|
||||||
|
{
|
||||||
|
colnames: ['__timestamp', 'genre'],
|
||||||
|
coltypes: [2, 1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
useRedux: true,
|
||||||
|
initialState: {
|
||||||
|
explore: {
|
||||||
|
timeFormattedColumns: {
|
||||||
|
'34__table': ['__timestamp'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
);
|
||||||
);
|
userEvent.click(screen.getByText('Results'));
|
||||||
userEvent.click(await screen.findByText('Data'));
|
expect(await screen.findByText('1 rows retrieved')).toBeVisible();
|
||||||
expect(await screen.findByText('1 rows retrieved')).toBeVisible();
|
|
||||||
|
|
||||||
userEvent.click(screen.getByRole('button', { name: 'Copy' }));
|
userEvent.click(screen.getByLabelText('Copy'));
|
||||||
expect(copyToClipboardSpy).toHaveBeenCalledWith(
|
expect(copyToClipboardSpy).toHaveBeenCalledWith(
|
||||||
'2009-01-01 00:00:00\tAction\n',
|
'2009-01-01 00:00:00\tAction\n',
|
||||||
);
|
);
|
||||||
fetchMock.done();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,15 +16,23 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
MouseEvent,
|
||||||
|
} from 'react';
|
||||||
import {
|
import {
|
||||||
|
css,
|
||||||
ensureIsArray,
|
ensureIsArray,
|
||||||
GenericDataType,
|
GenericDataType,
|
||||||
JsonObject,
|
JsonObject,
|
||||||
styled,
|
styled,
|
||||||
t,
|
t,
|
||||||
|
useTheme,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import Collapse from 'src/components/Collapse';
|
import Icons from 'src/components/Icons';
|
||||||
import Tabs from 'src/components/Tabs';
|
import Tabs from 'src/components/Tabs';
|
||||||
import Loading from 'src/components/Loading';
|
import Loading from 'src/components/Loading';
|
||||||
import { EmptyStateMedium } from 'src/components/EmptyState';
|
import { EmptyStateMedium } from 'src/components/EmptyState';
|
||||||
|
@ -58,53 +66,58 @@ const getDefaultDataTablesState = (value: any) => ({
|
||||||
|
|
||||||
const DATA_TABLE_PAGE_SIZE = 50;
|
const DATA_TABLE_PAGE_SIZE = 50;
|
||||||
|
|
||||||
const DATAPANEL_KEY = 'data';
|
|
||||||
|
|
||||||
const TableControlsWrapper = styled.div`
|
const TableControlsWrapper = styled.div`
|
||||||
display: flex;
|
${({ theme }) => `
|
||||||
align-items: center;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: ${theme.gridUnit * 2}px;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SouthPane = styled.div`
|
const SouthPane = styled.div`
|
||||||
position: relative;
|
${({ theme }) => `
|
||||||
background-color: ${({ theme }) => theme.colors.grayscale.light5};
|
position: relative;
|
||||||
z-index: 5;
|
background-color: ${theme.colors.grayscale.light5};
|
||||||
overflow: hidden;
|
z-index: 5;
|
||||||
`;
|
overflow: hidden;
|
||||||
|
|
||||||
const TabsWrapper = styled.div<{ contentHeight: number }>`
|
.ant-tabs {
|
||||||
height: ${({ contentHeight }) => contentHeight}px;
|
height: 100%;
|
||||||
overflow: hidden;
|
}
|
||||||
|
|
||||||
.table-condensed {
|
.ant-tabs-content-holder {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
}
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CollapseWrapper = styled.div`
|
.ant-tabs-content {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.collapse-inner {
|
.ant-tabs-tabpane {
|
||||||
height: 100%;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
.ant-collapse-item {
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.ant-collapse-content {
|
.table-condensed {
|
||||||
height: calc(100% - ${({ theme }) => theme.gridUnit * 8}px);
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
margin-bottom: ${theme.gridUnit * 4}px;
|
||||||
|
|
||||||
.ant-collapse-content-box {
|
.table {
|
||||||
padding-top: 0;
|
margin-bottom: ${theme.gridUnit * 2}px;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination-container > ul[role='navigation'] {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Error = styled.pre`
|
const Error = styled.pre`
|
||||||
|
@ -117,7 +130,6 @@ interface DataTableProps {
|
||||||
datasource: string | undefined;
|
datasource: string | undefined;
|
||||||
filterText: string;
|
filterText: string;
|
||||||
data: object[] | undefined;
|
data: object[] | undefined;
|
||||||
timeFormattedColumns: string[] | undefined;
|
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
errorMessage: React.ReactElement | undefined;
|
errorMessage: React.ReactElement | undefined;
|
||||||
|
@ -130,12 +142,12 @@ const DataTable = ({
|
||||||
datasource,
|
datasource,
|
||||||
filterText,
|
filterText,
|
||||||
data,
|
data,
|
||||||
timeFormattedColumns,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
type,
|
type,
|
||||||
}: DataTableProps) => {
|
}: DataTableProps) => {
|
||||||
|
const timeFormattedColumns = useTimeFormattedColumns(datasource);
|
||||||
// this is to preserve the order of the columns, even if there are integer values,
|
// this is to preserve the order of the columns, even if there are integer values,
|
||||||
// while also only grabbing the first column's keys
|
// while also only grabbing the first column's keys
|
||||||
const columns = useTableColumns(
|
const columns = useTableColumns(
|
||||||
|
@ -185,9 +197,42 @@ const DataTable = ({
|
||||||
return null;
|
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 = ({
|
export const DataTablesPane = ({
|
||||||
queryFormData,
|
queryFormData,
|
||||||
tableSectionHeight,
|
|
||||||
onCollapseChange,
|
onCollapseChange,
|
||||||
chartStatus,
|
chartStatus,
|
||||||
ownState,
|
ownState,
|
||||||
|
@ -195,19 +240,19 @@ export const DataTablesPane = ({
|
||||||
queriesResponse,
|
queriesResponse,
|
||||||
}: {
|
}: {
|
||||||
queryFormData: Record<string, any>;
|
queryFormData: Record<string, any>;
|
||||||
tableSectionHeight: number;
|
|
||||||
chartStatus: string;
|
chartStatus: string;
|
||||||
ownState?: JsonObject;
|
ownState?: JsonObject;
|
||||||
onCollapseChange: (openPanelName: string) => void;
|
onCollapseChange: (isOpen: boolean) => void;
|
||||||
errorMessage?: JSX.Element;
|
errorMessage?: JSX.Element;
|
||||||
queriesResponse: Record<string, any>;
|
queriesResponse: Record<string, any>;
|
||||||
}) => {
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
const [data, setData] = useState(getDefaultDataTablesState(undefined));
|
const [data, setData] = useState(getDefaultDataTablesState(undefined));
|
||||||
const [isLoading, setIsLoading] = useState(getDefaultDataTablesState(true));
|
const [isLoading, setIsLoading] = useState(getDefaultDataTablesState(true));
|
||||||
const [columnNames, setColumnNames] = useState(getDefaultDataTablesState([]));
|
const [columnNames, setColumnNames] = useState(getDefaultDataTablesState([]));
|
||||||
const [columnTypes, setColumnTypes] = useState(getDefaultDataTablesState([]));
|
const [columnTypes, setColumnTypes] = useState(getDefaultDataTablesState([]));
|
||||||
const [error, setError] = useState(getDefaultDataTablesState(''));
|
const [error, setError] = useState(getDefaultDataTablesState(''));
|
||||||
const [filterText, setFilterText] = useState('');
|
const [filterText, setFilterText] = useState(getDefaultDataTablesState(''));
|
||||||
const [activeTabKey, setActiveTabKey] = useState<string>(
|
const [activeTabKey, setActiveTabKey] = useState<string>(
|
||||||
RESULT_TYPES.results,
|
RESULT_TYPES.results,
|
||||||
);
|
);
|
||||||
|
@ -218,24 +263,6 @@ export const DataTablesPane = ({
|
||||||
getItem(LocalStorageKeys.is_datapanel_open, false),
|
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(
|
const getData = useCallback(
|
||||||
(resultType: 'samples' | 'results') => {
|
(resultType: 'samples' | 'results') => {
|
||||||
setIsLoading(prevIsLoading => ({
|
setIsLoading(prevIsLoading => ({
|
||||||
|
@ -381,81 +408,121 @@ export const DataTablesPane = ({
|
||||||
errorMessage,
|
errorMessage,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const TableControls = (
|
const handleCollapseChange = useCallback(
|
||||||
<TableControlsWrapper>
|
(isOpen: boolean) => {
|
||||||
<RowCount data={data[activeTabKey]} loading={isLoading[activeTabKey]} />
|
onCollapseChange(isOpen);
|
||||||
<CopyToClipboardButton
|
setPanelOpen(isOpen);
|
||||||
data={formattedData[activeTabKey]}
|
},
|
||||||
columns={columnNames[activeTabKey]}
|
[onCollapseChange],
|
||||||
/>
|
|
||||||
<FilterInput onChangeHandler={setFilterText} />
|
|
||||||
</TableControlsWrapper>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCollapseChange = (openPanelName: string) => {
|
const handleTabClick = useCallback(
|
||||||
onCollapseChange(openPanelName);
|
(tabKey: string, e: MouseEvent) => {
|
||||||
setPanelOpen(!!openPanelName);
|
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 (
|
return (
|
||||||
<SouthPane data-test="some-purposeful-instance">
|
<SouthPane data-test="some-purposeful-instance">
|
||||||
<TabsWrapper contentHeight={tableSectionHeight}>
|
<Tabs
|
||||||
<CollapseWrapper data-test="data-tab">
|
fullWidth={false}
|
||||||
<Collapse
|
tabBarExtraContent={CollapseButton}
|
||||||
accordion
|
activeKey={panelOpen ? activeTabKey : ''}
|
||||||
bordered={false}
|
onTabClick={handleTabClick}
|
||||||
defaultActiveKey={panelOpen ? DATAPANEL_KEY : undefined}
|
>
|
||||||
onChange={handleCollapseChange}
|
<Tabs.TabPane tab={t('Results')} key={RESULT_TYPES.results}>
|
||||||
bold
|
<TableControls
|
||||||
ghost
|
data={data[RESULT_TYPES.results]}
|
||||||
className="collapse-inner"
|
columnNames={columnNames[RESULT_TYPES.results]}
|
||||||
>
|
datasourceId={queryFormData?.datasource}
|
||||||
<Collapse.Panel header={t('Data')} key={DATAPANEL_KEY}>
|
onInputChange={input =>
|
||||||
<Tabs
|
setFilterText(prevState => ({
|
||||||
fullWidth={false}
|
...prevState,
|
||||||
tabBarExtraContent={TableControls}
|
[RESULT_TYPES.results]: input,
|
||||||
activeKey={activeTabKey}
|
}))
|
||||||
onChange={setActiveTabKey}
|
}
|
||||||
>
|
isLoading={isLoading[RESULT_TYPES.results]}
|
||||||
<Tabs.TabPane
|
/>
|
||||||
tab={t('View results')}
|
<DataTable
|
||||||
key={RESULT_TYPES.results}
|
isLoading={isLoading[RESULT_TYPES.results]}
|
||||||
>
|
data={data[RESULT_TYPES.results]}
|
||||||
<DataTable
|
datasource={queryFormData?.datasource}
|
||||||
isLoading={isLoading[RESULT_TYPES.results]}
|
columnNames={columnNames[RESULT_TYPES.results]}
|
||||||
data={data[RESULT_TYPES.results]}
|
columnTypes={columnTypes[RESULT_TYPES.results]}
|
||||||
datasource={queryFormData?.datasource}
|
filterText={filterText[RESULT_TYPES.results]}
|
||||||
timeFormattedColumns={timeFormattedColumns}
|
error={error[RESULT_TYPES.results]}
|
||||||
columnNames={columnNames[RESULT_TYPES.results]}
|
errorMessage={errorMessage}
|
||||||
columnTypes={columnTypes[RESULT_TYPES.results]}
|
type={RESULT_TYPES.results}
|
||||||
filterText={filterText}
|
/>
|
||||||
error={error[RESULT_TYPES.results]}
|
</Tabs.TabPane>
|
||||||
errorMessage={errorMessage}
|
<Tabs.TabPane tab={t('Samples')} key={RESULT_TYPES.samples}>
|
||||||
type={RESULT_TYPES.results}
|
<TableControls
|
||||||
/>
|
data={data[RESULT_TYPES.samples]}
|
||||||
</Tabs.TabPane>
|
columnNames={columnNames[RESULT_TYPES.samples]}
|
||||||
<Tabs.TabPane
|
datasourceId={queryFormData?.datasource}
|
||||||
tab={t('View samples')}
|
onInputChange={input =>
|
||||||
key={RESULT_TYPES.samples}
|
setFilterText(prevState => ({
|
||||||
>
|
...prevState,
|
||||||
<DataTable
|
[RESULT_TYPES.samples]: input,
|
||||||
isLoading={isLoading[RESULT_TYPES.samples]}
|
}))
|
||||||
data={data[RESULT_TYPES.samples]}
|
}
|
||||||
datasource={queryFormData?.datasource}
|
isLoading={isLoading[RESULT_TYPES.samples]}
|
||||||
timeFormattedColumns={timeFormattedColumns}
|
/>
|
||||||
columnNames={columnNames[RESULT_TYPES.samples]}
|
<DataTable
|
||||||
columnTypes={columnTypes[RESULT_TYPES.samples]}
|
isLoading={isLoading[RESULT_TYPES.samples]}
|
||||||
filterText={filterText}
|
data={data[RESULT_TYPES.samples]}
|
||||||
error={error[RESULT_TYPES.samples]}
|
datasource={queryFormData?.datasource}
|
||||||
errorMessage={errorMessage}
|
columnNames={columnNames[RESULT_TYPES.samples]}
|
||||||
type={RESULT_TYPES.samples}
|
columnTypes={columnTypes[RESULT_TYPES.samples]}
|
||||||
/>
|
filterText={filterText[RESULT_TYPES.samples]}
|
||||||
</Tabs.TabPane>
|
error={error[RESULT_TYPES.samples]}
|
||||||
</Tabs>
|
errorMessage={errorMessage}
|
||||||
</Collapse.Panel>
|
type={RESULT_TYPES.samples}
|
||||||
</Collapse>
|
/>
|
||||||
</CollapseWrapper>
|
</Tabs.TabPane>
|
||||||
</TabsWrapper>
|
</Tabs>
|
||||||
</SouthPane>
|
</SouthPane>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Split from 'react-split';
|
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 { useResizeDetector } from 'react-resize-detector';
|
||||||
import { chartPropShape } from 'src/dashboard/util/propShapes';
|
import { chartPropShape } from 'src/dashboard/util/propShapes';
|
||||||
import ChartContainer from 'src/components/Chart/ChartContainer';
|
import ChartContainer from 'src/components/Chart/ChartContainer';
|
||||||
|
@ -41,8 +41,6 @@ const propTypes = {
|
||||||
dashboardId: PropTypes.number,
|
dashboardId: PropTypes.number,
|
||||||
column_formats: PropTypes.object,
|
column_formats: PropTypes.object,
|
||||||
containerId: PropTypes.string.isRequired,
|
containerId: PropTypes.string.isRequired,
|
||||||
height: PropTypes.string.isRequired,
|
|
||||||
width: PropTypes.string.isRequired,
|
|
||||||
isStarred: PropTypes.bool.isRequired,
|
isStarred: PropTypes.bool.isRequired,
|
||||||
slice: PropTypes.object,
|
slice: PropTypes.object,
|
||||||
sliceName: PropTypes.string,
|
sliceName: PropTypes.string,
|
||||||
|
@ -61,11 +59,8 @@ const propTypes = {
|
||||||
|
|
||||||
const GUTTER_SIZE_FACTOR = 1.25;
|
const GUTTER_SIZE_FACTOR = 1.25;
|
||||||
|
|
||||||
const CHART_PANEL_PADDING_HORIZ = 30;
|
const INITIAL_SIZES = [100, 0];
|
||||||
const CHART_PANEL_PADDING_VERTICAL = 15;
|
const MIN_SIZES = [300, 65];
|
||||||
|
|
||||||
const INITIAL_SIZES = [90, 10];
|
|
||||||
const MIN_SIZES = [300, 50];
|
|
||||||
const DEFAULT_SOUTH_PANE_HEIGHT_PERCENT = 40;
|
const DEFAULT_SOUTH_PANE_HEIGHT_PERCENT = 40;
|
||||||
|
|
||||||
const Styles = styled.div`
|
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 theme = useTheme();
|
||||||
const gutterMargin = theme.gridUnit * GUTTER_SIZE_FACTOR;
|
const gutterMargin = theme.gridUnit * GUTTER_SIZE_FACTOR;
|
||||||
const gutterHeight = 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',
|
refreshMode: 'debounce',
|
||||||
refreshRate: 300,
|
refreshRate: 300,
|
||||||
});
|
});
|
||||||
const { height: pillsHeight, ref: pillsRef } = useResizeDetector({
|
|
||||||
refreshMode: 'debounce',
|
|
||||||
refreshRate: 1000,
|
|
||||||
});
|
|
||||||
const [splitSizes, setSplitSizes] = useState(
|
const [splitSizes, setSplitSizes] = useState(
|
||||||
getItem(LocalStorageKeys.chart_split_sizes, INITIAL_SIZES),
|
getItem(LocalStorageKeys.chart_split_sizes, INITIAL_SIZES),
|
||||||
);
|
);
|
||||||
const { slice } = props;
|
|
||||||
const updateQueryContext = useCallback(
|
const updateQueryContext = useCallback(
|
||||||
async function fetchChartData() {
|
async function fetchChartData() {
|
||||||
if (slice && slice.query_context === null) {
|
if (slice && slice.query_context === null) {
|
||||||
const queryContext = buildV1ChartDataPayload({
|
const queryContext = buildV1ChartDataPayload({
|
||||||
formData: slice.form_data,
|
formData: slice.form_data,
|
||||||
force: props.force,
|
force,
|
||||||
resultFormat: 'json',
|
resultFormat: 'json',
|
||||||
resultType: 'full',
|
resultType: 'full',
|
||||||
setDataMask: null,
|
setDataMask: null,
|
||||||
|
@ -154,34 +163,6 @@ const ExploreChartPanel = props => {
|
||||||
updateQueryContext();
|
updateQueryContext();
|
||||||
}, [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(() => {
|
useEffect(() => {
|
||||||
setItem(LocalStorageKeys.chart_split_sizes, splitSizes);
|
setItem(LocalStorageKeys.chart_split_sizes, splitSizes);
|
||||||
}, [splitSizes]);
|
}, [splitSizes]);
|
||||||
|
@ -191,19 +172,19 @@ const ExploreChartPanel = props => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshCachedQuery = () => {
|
const refreshCachedQuery = () => {
|
||||||
props.actions.postChartFormData(
|
actions.postChartFormData(
|
||||||
props.form_data,
|
formData,
|
||||||
true,
|
true,
|
||||||
props.timeout,
|
timeout,
|
||||||
props.chart.id,
|
chart.id,
|
||||||
undefined,
|
undefined,
|
||||||
props.ownState,
|
ownState,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCollapseChange = openPanelName => {
|
const onCollapseChange = useCallback(isOpen => {
|
||||||
let splitSizes;
|
let splitSizes;
|
||||||
if (!openPanelName) {
|
if (!isOpen) {
|
||||||
splitSizes = INITIAL_SIZES;
|
splitSizes = INITIAL_SIZES;
|
||||||
} else {
|
} else {
|
||||||
splitSizes = [
|
splitSizes = [
|
||||||
|
@ -212,53 +193,84 @@ const ExploreChartPanel = props => {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
setSplitSizes(splitSizes);
|
setSplitSizes(splitSizes);
|
||||||
};
|
}, []);
|
||||||
const renderChart = useCallback(() => {
|
|
||||||
const { chart, vizType } = props;
|
const renderChart = useCallback(
|
||||||
const newHeight =
|
() => (
|
||||||
vizType === 'filter_box'
|
<div
|
||||||
? calcSectionHeight(100) - CHART_PANEL_PADDING_VERTICAL
|
css={css`
|
||||||
: calcSectionHeight(splitSizes[0]) - CHART_PANEL_PADDING_VERTICAL;
|
min-height: 0;
|
||||||
const chartWidth = chartPanelWidth - CHART_PANEL_PADDING_HORIZ;
|
flex: 1;
|
||||||
return (
|
`}
|
||||||
chartWidth > 0 && (
|
ref={chartPanelRef}
|
||||||
<ChartContainer
|
>
|
||||||
width={Math.floor(chartWidth)}
|
{chartPanelWidth && chartPanelHeight && (
|
||||||
height={newHeight}
|
<ChartContainer
|
||||||
ownState={props.ownState}
|
width={Math.floor(chartPanelWidth)}
|
||||||
annotationData={chart.annotationData}
|
height={chartPanelHeight}
|
||||||
chartAlert={chart.chartAlert}
|
ownState={ownState}
|
||||||
chartStackTrace={chart.chartStackTrace}
|
annotationData={chart.annotationData}
|
||||||
chartId={chart.id}
|
chartAlert={chart.chartAlert}
|
||||||
chartStatus={chart.chartStatus}
|
chartStackTrace={chart.chartStackTrace}
|
||||||
triggerRender={props.triggerRender}
|
chartId={chart.id}
|
||||||
force={props.force}
|
chartStatus={chart.chartStatus}
|
||||||
datasource={props.datasource}
|
triggerRender={triggerRender}
|
||||||
errorMessage={props.errorMessage}
|
force={force}
|
||||||
formData={props.form_data}
|
datasource={datasource}
|
||||||
onQuery={props.onQuery}
|
errorMessage={errorMessage}
|
||||||
queriesResponse={chart.queriesResponse}
|
formData={formData}
|
||||||
refreshOverlayVisible={props.refreshOverlayVisible}
|
onQuery={onQuery}
|
||||||
setControlValue={props.actions.setControlValue}
|
queriesResponse={chart.queriesResponse}
|
||||||
timeout={props.timeout}
|
refreshOverlayVisible={refreshOverlayVisible}
|
||||||
triggerQuery={chart.triggerQuery}
|
setControlValue={actions.setControlValue}
|
||||||
vizType={props.vizType}
|
timeout={timeout}
|
||||||
/>
|
triggerQuery={chart.triggerQuery}
|
||||||
)
|
vizType={vizType}
|
||||||
);
|
/>
|
||||||
}, [calcSectionHeight, chartPanelWidth, props, splitSizes]);
|
)}
|
||||||
|
</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(
|
const panelBody = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<div className="panel-body" ref={chartPanelRef}>
|
<div
|
||||||
|
className="panel-body"
|
||||||
|
css={css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`}
|
||||||
|
>
|
||||||
<ChartPills
|
<ChartPills
|
||||||
queriesResponse={props.chart.queriesResponse}
|
queriesResponse={chart.queriesResponse}
|
||||||
chartStatus={props.chart.chartStatus}
|
chartStatus={chart.chartStatus}
|
||||||
chartUpdateStartTime={props.chart.chartUpdateStartTime}
|
chartUpdateStartTime={chart.chartUpdateStartTime}
|
||||||
chartUpdateEndTime={props.chart.chartUpdateEndTime}
|
chartUpdateEndTime={chart.chartUpdateEndTime}
|
||||||
refreshCachedQuery={refreshCachedQuery}
|
refreshCachedQuery={refreshCachedQuery}
|
||||||
rowLimit={props.form_data?.row_limit}
|
rowLimit={formData?.row_limit}
|
||||||
ref={pillsRef}
|
|
||||||
/>
|
/>
|
||||||
{renderChart()}
|
{renderChart()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -266,14 +278,9 @@ const ExploreChartPanel = props => {
|
||||||
[chartPanelRef, renderChart],
|
[chartPanelRef, renderChart],
|
||||||
);
|
);
|
||||||
|
|
||||||
const standaloneChartBody = useMemo(
|
const standaloneChartBody = useMemo(() => renderChart(), [renderChart]);
|
||||||
() => <div ref={chartPanelRef}>{renderChart()}</div>,
|
|
||||||
[chartPanelRef, renderChart],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [queryFormData, setQueryFormData] = useState(
|
const [queryFormData, setQueryFormData] = useState(chart.latestQueryFormData);
|
||||||
props.chart.latestQueryFormData,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// only update when `latestQueryFormData` changes AND `triggerRender`
|
// 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.
|
// as this can trigger a query downstream based on incomplete form data.
|
||||||
// (`latestQueryFormData` is only updated when a a valid request has been
|
// (`latestQueryFormData` is only updated when a a valid request has been
|
||||||
// triggered).
|
// triggered).
|
||||||
if (!props.triggerRender) {
|
if (!triggerRender) {
|
||||||
setQueryFormData(props.chart.latestQueryFormData);
|
setQueryFormData(chart.latestQueryFormData);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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
|
// dom manipulation hack to get rid of the boostrap theme's body background
|
||||||
const standaloneClass = 'background-transparent';
|
const standaloneClass = 'background-transparent';
|
||||||
const bodyClasses = document.body.className.split(' ');
|
const bodyClasses = document.body.className.split(' ');
|
||||||
|
@ -302,8 +309,8 @@ const ExploreChartPanel = props => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Styles className="panel panel-default chart-container" ref={chartPanelRef}>
|
<Styles className="panel panel-default chart-container">
|
||||||
{props.vizType === 'filter_box' ? (
|
{vizType === 'filter_box' ? (
|
||||||
panelBody
|
panelBody
|
||||||
) : (
|
) : (
|
||||||
<Split
|
<Split
|
||||||
|
@ -313,16 +320,16 @@ const ExploreChartPanel = props => {
|
||||||
gutterSize={gutterHeight}
|
gutterSize={gutterHeight}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
elementStyle={elementStyle}
|
elementStyle={elementStyle}
|
||||||
|
expandToMin
|
||||||
>
|
>
|
||||||
{panelBody}
|
{panelBody}
|
||||||
<DataTablesPane
|
<DataTablesPane
|
||||||
ownState={props.ownState}
|
ownState={ownState}
|
||||||
queryFormData={queryFormData}
|
queryFormData={queryFormData}
|
||||||
tableSectionHeight={tableSectionHeight}
|
|
||||||
onCollapseChange={onCollapseChange}
|
onCollapseChange={onCollapseChange}
|
||||||
chartStatus={props.chart.chartStatus}
|
chartStatus={chart.chartStatus}
|
||||||
errorMessage={props.errorMessage}
|
errorMessage={errorMessage}
|
||||||
queriesResponse={props.chart.queriesResponse}
|
queriesResponse={chart.queriesResponse}
|
||||||
/>
|
/>
|
||||||
</Split>
|
</Split>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -63,8 +63,6 @@ import ConnectedExploreChartHeader from '../ExploreChartHeader';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
...ExploreChartPanel.propTypes,
|
...ExploreChartPanel.propTypes,
|
||||||
height: PropTypes.string,
|
|
||||||
width: PropTypes.string,
|
|
||||||
actions: PropTypes.object.isRequired,
|
actions: PropTypes.object.isRequired,
|
||||||
datasource_type: PropTypes.string.isRequired,
|
datasource_type: PropTypes.string.isRequired,
|
||||||
dashboardId: PropTypes.number,
|
dashboardId: PropTypes.number,
|
||||||
|
@ -135,6 +133,7 @@ const ExplorePanelContainer = styled.div`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: ${theme.gridUnit * 128}px;
|
min-width: ${theme.gridUnit * 128}px;
|
||||||
border-left: 1px solid ${theme.colors.grayscale.light2};
|
border-left: 1px solid ${theme.colors.grayscale.light2};
|
||||||
|
padding: 0 ${theme.gridUnit * 4}px;
|
||||||
.panel {
|
.panel {
|
||||||
margin-bottom: 0;
|
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(
|
const updateHistory = debounce(
|
||||||
async (formData, datasetId, isReplace, standalone, force, title, tabId) => {
|
async (formData, datasetId, isReplace, standalone, force, title, tabId) => {
|
||||||
const payload = { ...formData };
|
const payload = { ...formData };
|
||||||
|
@ -246,7 +228,6 @@ function ExploreViewContainer(props) {
|
||||||
const [lastQueriedControls, setLastQueriedControls] = useState(
|
const [lastQueriedControls, setLastQueriedControls] = useState(
|
||||||
props.controls,
|
props.controls,
|
||||||
);
|
);
|
||||||
const windowSize = useWindowSize();
|
|
||||||
|
|
||||||
const [showingModal, setShowingModal] = useState(false);
|
const [showingModal, setShowingModal] = useState(false);
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
@ -254,11 +235,6 @@ function ExploreViewContainer(props) {
|
||||||
const tabId = useTabId();
|
const tabId = useTabId();
|
||||||
|
|
||||||
const theme = useTheme();
|
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 = {
|
const defaultSidebarsWidth = {
|
||||||
controls_width: 320,
|
controls_width: 320,
|
||||||
|
@ -515,8 +491,6 @@ function ExploreViewContainer(props) {
|
||||||
function renderChartContainer() {
|
function renderChartContainer() {
|
||||||
return (
|
return (
|
||||||
<ExploreChartPanel
|
<ExploreChartPanel
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
{...props}
|
{...props}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
refreshOverlayVisible={chartIsStale}
|
refreshOverlayVisible={chartIsStale}
|
||||||
|
|
Loading…
Reference in New Issue