400 lines
11 KiB
TypeScript
400 lines
11 KiB
TypeScript
/**
|
|
* 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 rison from 'rison';
|
|
import { styled, t, SupersetClient, JsonResponse } from '@superset-ui/core';
|
|
import { getUrlParam } from 'src/utils/urlUtils';
|
|
import { URL_PARAMS } from 'src/constants';
|
|
import { isNullish } from 'src/utils/common';
|
|
import Button from 'src/components/Button';
|
|
import { Select, Steps } from 'src/components';
|
|
import { Tooltip } from 'src/components/Tooltip';
|
|
|
|
import VizTypeGallery, {
|
|
MAX_ADVISABLE_VIZ_GALLERY_WIDTH,
|
|
} from 'src/explore/components/controls/VizTypeControl/VizTypeGallery';
|
|
import { findPermission } from 'src/utils/findPermission';
|
|
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
|
|
|
type Dataset = {
|
|
id: number;
|
|
table_name: string;
|
|
description: string;
|
|
datasource_type: string;
|
|
};
|
|
|
|
export type AddSliceContainerProps = {
|
|
user: UserWithPermissionsAndRoles;
|
|
};
|
|
|
|
export type AddSliceContainerState = {
|
|
datasource?: { label: string; value: string };
|
|
vizType: string | null;
|
|
canCreateDataset: boolean;
|
|
};
|
|
|
|
const ESTIMATED_NAV_HEIGHT = 56;
|
|
const ELEMENTS_EXCEPT_VIZ_GALLERY = ESTIMATED_NAV_HEIGHT + 250;
|
|
|
|
const StyledContainer = styled.div`
|
|
${({ theme }) => `
|
|
flex: 1 1 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
max-width: ${MAX_ADVISABLE_VIZ_GALLERY_WIDTH}px;
|
|
max-height: calc(100vh - ${ESTIMATED_NAV_HEIGHT}px);
|
|
border-radius: ${theme.gridUnit}px;
|
|
background-color: ${theme.colors.grayscale.light5};
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
padding-left: ${theme.gridUnit * 4}px;
|
|
padding-right: ${theme.gridUnit * 4}px;
|
|
padding-bottom: ${theme.gridUnit * 4}px;
|
|
|
|
h3 {
|
|
padding-bottom: ${theme.gridUnit * 3}px;
|
|
}
|
|
|
|
& .dataset {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
|
|
& > div {
|
|
min-width: 200px;
|
|
width: 300px;
|
|
}
|
|
|
|
& > span {
|
|
color: ${theme.colors.grayscale.light1};
|
|
margin-left: ${theme.gridUnit * 4}px;
|
|
}
|
|
}
|
|
|
|
& .viz-gallery {
|
|
border: 1px solid ${theme.colors.grayscale.light2};
|
|
border-radius: ${theme.gridUnit}px;
|
|
margin: ${theme.gridUnit}px 0px;
|
|
max-height: calc(100vh - ${ELEMENTS_EXCEPT_VIZ_GALLERY}px);
|
|
flex: 1;
|
|
}
|
|
|
|
& .footer {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: flex-end;
|
|
align-items: center;
|
|
|
|
& > span {
|
|
color: ${theme.colors.grayscale.light1};
|
|
margin-right: ${theme.gridUnit * 4}px;
|
|
}
|
|
}
|
|
|
|
/* The following extra ampersands (&&&&) are used to boost selector specificity */
|
|
|
|
&&&& .ant-steps-item-tail {
|
|
display: none;
|
|
}
|
|
|
|
&&&& .ant-steps-item-icon {
|
|
margin-right: ${theme.gridUnit * 2}px;
|
|
width: ${theme.gridUnit * 5}px;
|
|
height: ${theme.gridUnit * 5}px;
|
|
line-height: ${theme.gridUnit * 5}px;
|
|
}
|
|
|
|
&&&& .ant-steps-item-title {
|
|
line-height: ${theme.gridUnit * 5}px;
|
|
}
|
|
|
|
&&&& .ant-steps-item-content {
|
|
overflow: unset;
|
|
|
|
.ant-steps-item-description {
|
|
margin-top: ${theme.gridUnit}px;
|
|
}
|
|
}
|
|
|
|
&&&& .ant-tooltip-open {
|
|
display: inline;
|
|
}
|
|
|
|
&&&& .ant-select-selector {
|
|
padding: 0;
|
|
}
|
|
|
|
&&&& .ant-select-selection-placeholder {
|
|
padding-left: ${theme.gridUnit * 3}px;
|
|
}
|
|
`}
|
|
`;
|
|
|
|
const TooltipContent = styled.div<{ hasDescription: boolean }>`
|
|
${({ theme, hasDescription }) => `
|
|
.tooltip-header {
|
|
font-size: ${
|
|
hasDescription ? theme.typography.sizes.l : theme.typography.sizes.s
|
|
}px;
|
|
font-weight: ${
|
|
hasDescription
|
|
? theme.typography.weights.bold
|
|
: theme.typography.weights.normal
|
|
};
|
|
}
|
|
|
|
.tooltip-description {
|
|
margin-top: ${theme.gridUnit * 2}px;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 20;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
`}
|
|
`;
|
|
|
|
const StyledLabel = styled.span`
|
|
${({ theme }) => `
|
|
position: absolute;
|
|
left: ${theme.gridUnit * 3}px;
|
|
right: ${theme.gridUnit * 3}px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
`}
|
|
`;
|
|
|
|
const StyledStepTitle = styled.span`
|
|
${({
|
|
theme: {
|
|
typography: { sizes, weights },
|
|
},
|
|
}) => `
|
|
font-size: ${sizes.m}px;
|
|
font-weight: ${weights.bold};
|
|
`}
|
|
`;
|
|
|
|
const StyledStepDescription = styled.div`
|
|
${({ theme: { gridUnit } }) => `
|
|
margin-top: ${gridUnit * 4}px;
|
|
margin-bottom: ${gridUnit * 3}px;
|
|
`}
|
|
`;
|
|
|
|
export default class AddSliceContainer extends React.PureComponent<
|
|
AddSliceContainerProps,
|
|
AddSliceContainerState
|
|
> {
|
|
constructor(props: AddSliceContainerProps) {
|
|
super(props);
|
|
this.state = {
|
|
vizType: null,
|
|
canCreateDataset: findPermission(
|
|
'can_write',
|
|
'Dataset',
|
|
props.user.roles,
|
|
),
|
|
};
|
|
|
|
this.changeDatasource = this.changeDatasource.bind(this);
|
|
this.changeVizType = this.changeVizType.bind(this);
|
|
this.gotoSlice = this.gotoSlice.bind(this);
|
|
this.newLabel = this.newLabel.bind(this);
|
|
this.loadDatasources = this.loadDatasources.bind(this);
|
|
this.onVizTypeDoubleClick = this.onVizTypeDoubleClick.bind(this);
|
|
}
|
|
|
|
exploreUrl() {
|
|
const dashboardId = getUrlParam(URL_PARAMS.dashboardId);
|
|
let url = `/superset/explore/?viz_type=${this.state.vizType}&datasource=${this.state.datasource?.value}`;
|
|
if (!isNullish(dashboardId)) {
|
|
url += `&dashboard_id=${dashboardId}`;
|
|
}
|
|
return url;
|
|
}
|
|
|
|
gotoSlice() {
|
|
window.location.href = this.exploreUrl();
|
|
}
|
|
|
|
changeDatasource(datasource: { label: string; value: string }) {
|
|
this.setState({ datasource });
|
|
}
|
|
|
|
changeVizType(vizType: string | null) {
|
|
this.setState({ vizType });
|
|
}
|
|
|
|
isBtnDisabled() {
|
|
return !(this.state.datasource?.value && this.state.vizType);
|
|
}
|
|
|
|
onVizTypeDoubleClick() {
|
|
if (!this.isBtnDisabled()) {
|
|
this.gotoSlice();
|
|
}
|
|
}
|
|
|
|
newLabel(item: Dataset) {
|
|
return (
|
|
<Tooltip
|
|
mouseEnterDelay={1}
|
|
placement="right"
|
|
title={
|
|
<TooltipContent hasDescription={!!item.description}>
|
|
<div className="tooltip-header">{item.table_name}</div>
|
|
{item.description && (
|
|
<div className="tooltip-description">{item.description}</div>
|
|
)}
|
|
</TooltipContent>
|
|
}
|
|
>
|
|
<StyledLabel>{item.table_name}</StyledLabel>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
loadDatasources(search: string, page: number, pageSize: number) {
|
|
const query = rison.encode({
|
|
columns: ['id', 'table_name', 'description', 'datasource_type'],
|
|
filters: [{ col: 'table_name', opr: 'ct', value: search }],
|
|
page,
|
|
page_size: pageSize,
|
|
order_column: 'table_name',
|
|
order_direction: 'asc',
|
|
});
|
|
return SupersetClient.get({
|
|
endpoint: `/api/v1/dataset/?q=${query}`,
|
|
}).then((response: JsonResponse) => {
|
|
const list: {
|
|
customLabel: ReactNode;
|
|
id: number;
|
|
label: string;
|
|
value: string;
|
|
}[] = response.json.result.map((item: Dataset) => ({
|
|
id: item.id,
|
|
value: `${item.id}__${item.datasource_type}`,
|
|
customLabel: this.newLabel(item),
|
|
label: item.table_name,
|
|
}));
|
|
return {
|
|
data: list,
|
|
totalCount: response.json.count,
|
|
};
|
|
});
|
|
}
|
|
|
|
render() {
|
|
const isButtonDisabled = this.isBtnDisabled();
|
|
const datasetHelpText = this.state.canCreateDataset ? (
|
|
<span data-test="dataset-write">
|
|
<a
|
|
href="/tablemodelview/list/#create"
|
|
rel="noopener noreferrer"
|
|
target="_blank"
|
|
>
|
|
{t('Add a dataset')}
|
|
</a>
|
|
{` ${t('or')} `}
|
|
<a
|
|
href="https://superset.apache.org/docs/creating-charts-dashboards/creating-your-first-dashboard/#registering-a-new-table"
|
|
rel="noopener noreferrer"
|
|
target="_blank"
|
|
>
|
|
{`${t('view instructions')} `}
|
|
<i className="fa fa-external-link" />
|
|
</a>
|
|
.
|
|
</span>
|
|
) : (
|
|
<span data-test="no-dataset-write">
|
|
<a
|
|
href="https://superset.apache.org/docs/creating-charts-dashboards/creating-your-first-dashboard/#registering-a-new-table"
|
|
rel="noopener noreferrer"
|
|
target="_blank"
|
|
>
|
|
{`${t('View instructions')} `}
|
|
<i className="fa fa-external-link" />
|
|
</a>
|
|
.
|
|
</span>
|
|
);
|
|
|
|
return (
|
|
<StyledContainer>
|
|
<h3>{t('Create a new chart')}</h3>
|
|
<Steps direction="vertical" size="small">
|
|
<Steps.Step
|
|
title={<StyledStepTitle>{t('Choose a dataset')}</StyledStepTitle>}
|
|
status={this.state.datasource?.value ? 'finish' : 'process'}
|
|
description={
|
|
<StyledStepDescription className="dataset">
|
|
<Select
|
|
autoFocus
|
|
ariaLabel={t('Dataset')}
|
|
name="select-datasource"
|
|
onChange={this.changeDatasource}
|
|
options={this.loadDatasources}
|
|
optionFilterProps={['id', 'label']}
|
|
placeholder={t('Choose a dataset')}
|
|
showSearch
|
|
value={this.state.datasource}
|
|
/>
|
|
{datasetHelpText}
|
|
</StyledStepDescription>
|
|
}
|
|
/>
|
|
<Steps.Step
|
|
title={<StyledStepTitle>{t('Choose chart type')}</StyledStepTitle>}
|
|
status={this.state.vizType ? 'finish' : 'process'}
|
|
description={
|
|
<StyledStepDescription>
|
|
<VizTypeGallery
|
|
className="viz-gallery"
|
|
onChange={this.changeVizType}
|
|
onDoubleClick={this.onVizTypeDoubleClick}
|
|
selectedViz={this.state.vizType}
|
|
/>
|
|
</StyledStepDescription>
|
|
}
|
|
/>
|
|
</Steps>
|
|
<div className="footer">
|
|
{isButtonDisabled && (
|
|
<span>
|
|
{t('Please select both a Dataset and a Chart type to proceed')}
|
|
</span>
|
|
)}
|
|
<Button
|
|
buttonStyle="primary"
|
|
disabled={isButtonDisabled}
|
|
onClick={this.gotoSlice}
|
|
>
|
|
{t('Create new chart')}
|
|
</Button>
|
|
</div>
|
|
</StyledContainer>
|
|
);
|
|
}
|
|
}
|