Files
superset2/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
Kamil Gabryjelski c8304a2821 feat(explore): Redesign of Run/Save buttons (#19558)
* feat(explore): Move save button to header, run button to bottom of control panel

* Make the tabs sticky

* Add error icon to Data tab

* Show message when creating chart and all controls are filled correctly

* Add tests and storybook

* Fix tests

* Disable save button when control have errors

* Fix types

* Apply code review comments

* Replace styled with css

* Remove unused import
2022-04-13 16:58:39 +02:00

333 lines
9.5 KiB
JavaScript

/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import PropTypes from 'prop-types';
import {
CategoricalColorNamespace,
css,
SupersetClient,
t,
} from '@superset-ui/core';
import {
fetchUISpecificReport,
toggleActive,
deleteActiveReport,
} from 'src/reports/actions/reports';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import { chartPropShape } from 'src/dashboard/util/propShapes';
import AlteredSliceTag from 'src/components/AlteredSliceTag';
import FaveStar from 'src/components/FaveStar';
import Button from 'src/components/Button';
import Icons from 'src/components/Icons';
import PropertiesModal from 'src/explore/components/PropertiesModal';
import { sliceUpdated } from 'src/explore/actions/exploreActions';
import CertifiedBadge from 'src/components/CertifiedBadge';
import { Tooltip } from 'src/components/Tooltip';
import ExploreAdditionalActionsMenu from '../ExploreAdditionalActionsMenu';
import { ChartEditableTitle } from './ChartEditableTitle';
const propTypes = {
actions: PropTypes.object.isRequired,
canOverwrite: PropTypes.bool.isRequired,
canDownload: PropTypes.bool.isRequired,
dashboardId: PropTypes.number,
isStarred: PropTypes.bool.isRequired,
slice: PropTypes.object,
sliceName: PropTypes.string,
table_name: PropTypes.string,
form_data: PropTypes.object,
ownState: PropTypes.object,
timeout: PropTypes.number,
chart: chartPropShape,
saveDisabled: PropTypes.bool,
};
const saveButtonStyles = theme => css`
color: ${theme.colors.primary.dark2};
& > span[role='img'] {
margin-right: 0;
}
`;
const headerStyles = theme => css`
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
justify-content: space-between;
height: 100%;
span[role='button'] {
display: flex;
height: 100%;
}
.title-panel {
display: flex;
align-items: center;
min-width: 0;
margin-right: ${theme.gridUnit * 12}px;
}
.right-button-panel {
display: flex;
align-items: center;
}
`;
const buttonsStyles = theme => css`
display: flex;
align-items: center;
padding-left: ${theme.gridUnit * 2}px;
& .fave-unfave-icon {
padding: 0 ${theme.gridUnit}px;
&:first-child {
padding-left: 0;
}
}
`;
const saveButtonContainerStyles = theme => css`
margin-right: ${theme.gridUnit * 2}px;
`;
export class ExploreChartHeader extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
isPropertiesModalOpen: false,
};
this.openPropertiesModal = this.openPropertiesModal.bind(this);
this.closePropertiesModal = this.closePropertiesModal.bind(this);
this.fetchChartDashboardData = this.fetchChartDashboardData.bind(this);
}
componentDidMount() {
const { dashboardId } = this.props;
if (this.canAddReports()) {
const { user, chart } = this.props;
// this is in the case that there is an anonymous user.
this.props.fetchUISpecificReport(
user.userId,
'chart_id',
'charts',
chart.id,
);
}
if (dashboardId) {
this.fetchChartDashboardData();
}
}
async fetchChartDashboardData() {
const { dashboardId, slice } = this.props;
await SupersetClient.get({
endpoint: `/api/v1/chart/${slice.slice_id}`,
})
.then(res => {
const response = res?.json?.result;
if (response && response.dashboards && response.dashboards.length) {
const { dashboards } = response;
const dashboard =
dashboardId &&
dashboards.length &&
dashboards.find(d => d.id === dashboardId);
if (dashboard && dashboard.json_metadata) {
// setting the chart to use the dashboard custom label colors if any
const metadata = JSON.parse(dashboard.json_metadata);
const sharedLabelColors = metadata.shared_label_colors || {};
const customLabelColors = metadata.label_colors || {};
const mergedLabelColors = {
...sharedLabelColors,
...customLabelColors,
};
const categoricalNamespace =
CategoricalColorNamespace.getNamespace();
Object.keys(mergedLabelColors).forEach(label => {
categoricalNamespace.setColor(
label,
mergedLabelColors[label],
metadata.color_scheme,
);
});
}
}
})
.catch(() => {});
}
postChartFormData() {
this.props.actions.postChartFormData(
this.props.form_data,
true,
this.props.timeout,
this.props.chart.id,
this.props.ownState,
);
}
openPropertiesModal() {
this.setState({
isPropertiesModalOpen: true,
});
}
closePropertiesModal() {
this.setState({
isPropertiesModalOpen: false,
});
}
canAddReports() {
if (!isFeatureEnabled(FeatureFlag.ALERT_REPORTS)) {
return false;
}
const { user } = this.props;
if (!user?.userId) {
// this is in the case that there is an anonymous user.
return false;
}
const roles = Object.keys(user.roles || []);
const permissions = roles.map(key =>
user.roles[key].filter(
perms => perms[0] === 'menu_access' && perms[1] === 'Manage',
),
);
return permissions[0].length > 0;
}
render() {
const {
actions,
chart,
user,
formData,
slice,
canOverwrite,
canDownload,
isStarred,
sliceUpdated,
sliceName,
onSaveChart,
saveDisabled,
} = this.props;
const { latestQueryFormData, sliceFormData } = chart;
const oldSliceName = slice?.slice_name;
return (
<div id="slice-header" css={headerStyles}>
<div className="title-panel">
<ChartEditableTitle
title={sliceName}
canEdit={
!slice ||
canOverwrite ||
(slice?.owners || []).includes(user?.userId)
}
onSave={actions.updateChartTitle}
placeholder={t('Add the name of the chart')}
/>
{slice && (
<span css={buttonsStyles}>
{slice.certified_by && (
<CertifiedBadge
certifiedBy={slice.certified_by}
details={slice.certification_details}
/>
)}
{user.userId && (
<FaveStar
itemId={slice.slice_id}
fetchFaveStar={actions.fetchFaveStar}
saveFaveStar={actions.saveFaveStar}
isStarred={isStarred}
showTooltip
/>
)}
{this.state.isPropertiesModalOpen && (
<PropertiesModal
show={this.state.isPropertiesModalOpen}
onHide={this.closePropertiesModal}
onSave={sliceUpdated}
slice={slice}
/>
)}
{sliceFormData && (
<AlteredSliceTag
className="altered"
origFormData={{ ...sliceFormData, chartTitle: oldSliceName }}
currentFormData={{ ...formData, chartTitle: sliceName }}
/>
)}
</span>
)}
</div>
<div className="right-button-panel">
<Tooltip
title={
saveDisabled
? t('Add required control values to save chart')
: null
}
>
{/* needed to wrap button in a div - antd tooltip doesn't work with disabled button */}
<div css={saveButtonContainerStyles}>
<Button
buttonStyle="secondary"
onClick={onSaveChart}
disabled={saveDisabled}
data-test="query-save-button"
css={saveButtonStyles}
>
<Icons.SaveOutlined iconSize="l" />
{t('Save')}
</Button>
</div>
</Tooltip>
<ExploreAdditionalActionsMenu
onOpenInEditor={actions.redirectSQLLab}
onOpenPropertiesModal={this.openPropertiesModal}
slice={slice}
canDownloadCSV={canDownload}
latestQueryFormData={latestQueryFormData}
canAddReports={this.canAddReports()}
/>
</div>
</div>
);
}
}
ExploreChartHeader.propTypes = propTypes;
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{ sliceUpdated, fetchUISpecificReport, toggleActive, deleteActiveReport },
dispatch,
);
}
export default connect(null, mapDispatchToProps)(ExploreChartHeader);