blob: 27567be6ac6ea58e60c3647602f5bda554c93f24 [file] [log] [blame]
/**
* 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 { createRef, useCallback, useMemo } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { nanoid } from 'nanoid';
import Tabs from '@superset-ui/core/components/Tabs';
import { css, styled, t, useTheme } from '@superset-ui/core';
import { removeTables, setActiveSouthPaneTab } from 'src/SqlLab/actions/sqlLab';
import { Label } from '@superset-ui/core/components';
import { Icons } from '@superset-ui/core/components/Icons';
import { SqlLabRootState } from 'src/SqlLab/types';
import { useExtensionsContext } from 'src/extensions/ExtensionsContext';
import ExtensionsManager from 'src/extensions/ExtensionsManager';
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
import QueryHistory from '../QueryHistory';
import {
STATUS_OPTIONS,
STATE_TYPE_MAP,
STATUS_OPTIONS_LOCALIZED,
} from '../../constants';
import Results from './Results';
import TablePreview from '../TablePreview';
/*
editorQueries are queries executed by users passed from SqlEditor component
dataPreviewQueries are all queries executed for preview of table data (from SqlEditorLeft)
*/
export interface SouthPaneProps {
queryEditorId: string;
latestQueryId?: string;
displayLimit: number;
defaultQueryLimit: number;
}
const TABS_KEYS = {
RESULTS: 'Results',
HISTORY: 'History',
};
const StyledPane = styled.div`
width: 100%;
height: 100%;
.ant-tabs .ant-tabs-content-holder {
overflow: visible;
}
.SouthPaneTabs {
height: 100%;
display: flex;
flex-direction: column;
.scrollable {
overflow-y: auto;
}
}
.ant-tabs-tabpane {
.scrollable {
overflow-y: auto;
}
}
.tab-content {
.alert {
margin-top: ${({ theme }) => theme.sizeUnit * 2}px;
}
button.fetch {
margin-top: ${({ theme }) => theme.sizeUnit * 2}px;
}
}
`;
const SouthPane = ({
queryEditorId,
latestQueryId,
displayLimit,
defaultQueryLimit,
}: SouthPaneProps) => {
const { id, tabViewId } = useQueryEditor(queryEditorId, ['tabViewId']);
const editorId = tabViewId ?? id;
const theme = useTheme();
const dispatch = useDispatch();
const contributions =
ExtensionsManager.getInstance().getViewContributions('sqllab.panels') || [];
const { getView } = useExtensionsContext();
const { offline, tables } = useSelector(
({ sqlLab: { offline, tables } }: SqlLabRootState) => ({
offline,
tables,
}),
shallowEqual,
);
const activeSouthPaneTab =
useSelector<SqlLabRootState, string>(
state => state.sqlLab.activeSouthPaneTab as string,
) ?? 'Results';
const pinnedTables = useMemo(
() => tables.filter(({ queryEditorId: qeId }) => String(editorId) === qeId),
[editorId, tables],
);
const pinnedTableKeys = useMemo(
() =>
Object.fromEntries(
pinnedTables.map(({ id, dbId, catalog, schema, name }) => [
id,
[dbId, catalog, schema, name].join(':'),
]),
),
[pinnedTables],
);
const southPaneRef = createRef<HTMLDivElement>();
const switchTab = (id: string) => {
dispatch(setActiveSouthPaneTab(id));
};
const removeTable = useCallback(
(key, action) => {
if (action === 'remove') {
const table = pinnedTables.find(
({ dbId, catalog, schema, name }) =>
[dbId, catalog, schema, name].join(':') === key,
);
dispatch(removeTables([table]));
}
},
[dispatch, pinnedTables],
);
if (offline) {
return (
<Label type={STATE_TYPE_MAP[STATUS_OPTIONS.offline]}>
{STATUS_OPTIONS_LOCALIZED.offline}
</Label>
);
}
const tabItems = [
{
key: TABS_KEYS.RESULTS,
label: t('Results'),
children: (
<Results
latestQueryId={latestQueryId}
displayLimit={displayLimit}
defaultQueryLimit={defaultQueryLimit}
/>
),
closable: false,
},
{
key: TABS_KEYS.HISTORY,
label: t('Query history'),
children: (
<QueryHistory
queryEditorId={queryEditorId}
displayLimit={displayLimit}
latestQueryId={latestQueryId}
/>
),
closable: false,
},
...pinnedTables.map(({ id, dbId, catalog, schema, name }) => ({
key: pinnedTableKeys[id],
label: (
<>
<Icons.InsertRowAboveOutlined
iconSize="l"
css={css`
margin-bottom: ${theme.sizeUnit * 0.5}px;
margin-right: ${theme.sizeUnit}px;
`}
/>
{`${schema}.${decodeURIComponent(name)}`}
</>
),
children: (
<TablePreview
dbId={dbId}
catalog={catalog}
schema={schema}
tableName={name}
/>
),
})),
...contributions.map(contribution => ({
key: contribution.id,
label: contribution.name,
children: getView(contribution.id),
forceRender: true,
closable: false,
})),
];
return (
<StyledPane data-test="south-pane" className="SouthPane" ref={southPaneRef}>
<Tabs
type="editable-card"
activeKey={pinnedTableKeys[activeSouthPaneTab] || activeSouthPaneTab}
className="SouthPaneTabs"
onChange={switchTab}
id={nanoid(11)}
animated={false}
onEdit={removeTable}
hideAdd
items={tabItems}
/>
</StyledPane>
);
};
export default SouthPane;