import React, { Suspense } from 'react' import { compose } from 'redux' import { connect } from 'react-redux' import { createStructuredSelector } from 'reselect' import injectReducer from 'utils/injectReducer' import injectSaga from 'utils/injectSaga' import reducer from 'containers/Widget/reducer' import viewReducer from 'containers/View/reducer' import saga from 'containers/Widget/sagas' import viewSaga from 'containers/View/sagas' import controlReducer from 'containers/ControlPanel/reducer' import { hideNavigator } from 'containers/App/actions' import { ViewActions } from 'containers/View/actions' const { loadViews, loadViewsDetail, loadViewData, loadColumnDistinctValue } = ViewActions import { WidgetActions } from 'containers/Widget/actions' import { makeSelectCurrentWidget, makeSelectLoading, makeSelectDataLoading } from 'containers/Widget/selectors' import { makeSelectViews, makeSelectFormedViews } from 'containers/View/selectors' import { RouteComponentWithParams } from 'utils/types' import { IViewBase, IFormedViews, IView } from 'containers/View/types' import OperatingPanel from './OperatingPanel' import Widget, { IWidgetProps } from '../Widget' import { IDataRequestBody } from 'app/containers/Dashboard/types' import EditorHeader from 'components/EditorHeader' import WorkbenchSettingForm from './WorkbenchSettingForm' import DashboardItemMask, { IDashboardItemMaskProps } from 'containers/Dashboard/components/DashboardItemMask' import { DEFAULT_SPLITER, DEFAULT_CACHE_EXPIRED } from 'app/globalConstants' import { getStyleConfig } from 'containers/Widget/components/util' import ChartTypes from '../../config/chart/ChartTypes' import { FieldSortTypes, fieldGroupedSort } from '../Config/Sort' import { message } from 'antd' import 'assets/less/resizer.less' import { IDistinctValueReqeustParams, IControl } from 'app/components/Control/types' import { IReference } from './Reference/types' import { IWorkbenchSettings, WorkbenchQueryMode } from './types' import { IWidgetFormed, IWidgetRaw } from '../../types' import { ControlQueryMode } from 'app/components/Control/constants' const styles = require('./Workbench.less') interface IWorkbenchProps { views: IViewBase[] formedViews: IFormedViews currentWidget: IWidgetFormed loading: boolean dataLoading: boolean onHideNavigator: () => void onLoadViews: (projectId: number, resolve?: () => void) => void onLoadViewDetail: ( viewIds: number[], resolve?: (views: IView[]) => void ) => void onLoadWidgetDetail: (id: number) => void onLoadViewData: ( viewId: number, requestParams: IDataRequestBody, resolve: (data) => void, reject: (error) => void ) => void onAddWidget: (widget: Omit, resolve: () => void) => void onEditWidget: (widget: IWidgetRaw, resolve: () => void) => void onLoadColumnDistinctValue: ( paramsByViewId: { [viewId: string]: Omit }, callback: (options?: object[]) => void ) => void onClearCurrentWidget: () => void onExecuteComputed: (sql: string) => void } interface IWorkbenchStates { id: number name: string description: string selectedViewId: number controls: any[] references: IReference[] computed: any[] autoLoadData: boolean controlQueryMode: ControlQueryMode limit: number cache: boolean expired: number splitSize: number originalWidgetProps: IWidgetProps originalComputed: any[] widgetProps: IWidgetProps settingFormVisible: boolean settings: IWorkbenchSettings } const SplitPane = React.lazy(() => import('react-split-pane')) export class Workbench extends React.Component< IWorkbenchProps & RouteComponentWithParams, IWorkbenchStates > { private operatingPanel: OperatingPanel = null private defaultSplitSize = 440 private maxSplitSize = this.defaultSplitSize * 1.5 constructor(props) { super(props) const splitSize = +localStorage.getItem('workbenchSplitSize') || this.defaultSplitSize this.state = { id: 0, name: '', description: '', selectedViewId: null, controls: [], references: [], computed: [], originalComputed: [], cache: false, autoLoadData: true, controlQueryMode: ControlQueryMode.Immediately, limit: null, expired: DEFAULT_CACHE_EXPIRED, splitSize, originalWidgetProps: null, widgetProps: { data: [], pagination: { pageNo: 0, pageSize: 0, totalCount: 0, withPaging: false }, cols: [], rows: [], metrics: [], secondaryMetrics: [], filters: [], chartStyles: getStyleConfig({}), selectedChart: ChartTypes.Table, orders: [], mode: 'pivot', model: {}, onPaginationChange: this.paginationChange }, settingFormVisible: false, settings: this.initSettings() } } private placeholder = { name: '请输入可视化组件名称', description: '请输入描述…' } public componentWillMount() { const { match, onLoadWidgetDetail } = this.props const { widgetId } = match.params this.loadViews(() => { if (widgetId !== 'add' && !Number.isNaN(Number(widgetId))) { onLoadWidgetDetail(+widgetId) } }) } public componentDidMount() { this.props.onHideNavigator() } public componentWillReceiveProps(nextProps: IWorkbenchProps) { const { currentWidget } = nextProps if (currentWidget && currentWidget !== this.props.currentWidget) { const { id, name, description, viewId, config } = currentWidget const { controls, references, limit, cache, expired, computed, autoLoadData, queryMode, ...rest } = config this.setState({ id, name, description, controls, references, cache, autoLoadData, controlQueryMode: queryMode, limit, expired, selectedViewId: viewId, originalWidgetProps: { ...rest }, widgetProps: { ...rest }, originalComputed: computed }) } } public componentWillUnmount() { this.props.onClearCurrentWidget() } private initSettings = (): IWorkbenchSettings => { let workbenchSettings = { queryMode: WorkbenchQueryMode.Immediately, multiDrag: false } try { const loginUser = JSON.parse(localStorage.getItem('loginUser')) const currentUserWorkbenchSetting = JSON.parse( localStorage.getItem(`${loginUser.id}_workbench_settings`) ) if (currentUserWorkbenchSetting) { workbenchSettings = currentUserWorkbenchSetting } } catch (err) { throw new Error(err) } return workbenchSettings } private loadViews = (callback?: () => void) => { const { match, onLoadViews } = this.props const { projectId } = match.params onLoadViews(Number(projectId), () => { if (callback) { callback() } }) } private changeName = (e) => { this.setState({ name: e.currentTarget.value }) } private changeDesc = (e) => { this.setState({ description: e.currentTarget.value }) } private viewSelect = (viewId: number) => { const { formedViews } = this.props const nextState = { selectedViewId: viewId, controls: [], controlQueryMode: ControlQueryMode.Immediately, references: [], cache: false, expired: DEFAULT_CACHE_EXPIRED } if (formedViews[viewId]) { this.setState(nextState) } else { this.props.onLoadViewDetail([viewId], () => { this.setState(nextState) }) } } private setControls = (controls: IControl[], queryMode: ControlQueryMode) => { this.setState({ controls, controlQueryMode: queryMode }) } private setReferences = (references: IReference[]) => { this.setState({ references, widgetProps: { ...this.state.widgetProps, references } }) } private deleteComputed = (computeField) => { const { from } = computeField const { match, onEditWidget } = this.props const { id, name, description, selectedViewId, controls, references, cache, autoLoadData, limit, expired, widgetProps, computed, originalWidgetProps, originalComputed } = this.state if (from === 'originalComputed') { this.setState( { originalComputed: originalComputed.filter( (oc) => oc.id !== computeField.id ) }, () => { const { originalComputed, computed } = this.state const widget = { name, description, type: 1, viewId: selectedViewId, projectId: Number(match.params.projectId), config: JSON.stringify({ ...widgetProps, controls, references, computed: originalComputed && originalComputed ? [...computed, ...originalComputed] : [...computed], limit, cache, autoLoadData, expired, data: [] }), publish: true } if (id) { onEditWidget({ ...widget, id }, () => void 0) } } ) } else if (from === 'computed') { this.setState( { computed: computed.filter((cm) => cm.id !== computeField.id) }, () => { const { originalComputed, computed } = this.state const widget = { name, description, type: 1, viewId: selectedViewId, projectId: Number(match.params.projectId), config: JSON.stringify({ ...widgetProps, controls, references, computed: originalComputed && originalComputed ? [...computed, ...originalComputed] : [...computed], limit, cache, autoLoadData, expired, data: [] }), publish: true } if (id) { onEditWidget({ ...widget, id }, () => void 0) } } ) } } private setComputed = (computeField) => { const { computed, originalComputed } = this.state const { from, sqlExpression } = computeField // todo 首先做sql合法校验; sqlExpression let isEdit = void 0 let newComputed = null if (from === 'originalComputed') { isEdit = originalComputed ? originalComputed.some((cm) => cm.id === computeField.id) : false newComputed = isEdit ? originalComputed.map((cm) => { if (cm.id === computeField.id) { return computeField } else { return cm } }) : originalComputed.concat(computeField) this.setState({ originalComputed: newComputed }) } else if (from === 'computed') { isEdit = computed.some((cm) => cm.id === computeField.id) newComputed = isEdit ? computed.map((cm) => { if (cm.id === computeField.id) { return computeField } else { return cm } }) : computed.concat(computeField) this.setState({ computed: newComputed }) } else { this.setState({ computed: computed.concat(computeField) }) } } private limitChange = (value) => { this.setState({ limit: value }) } private cacheChange = (e) => { this.setState({ cache: e.target.value }) } private expiredChange = (value) => { this.setState({ expired: value }) } private setWidgetProps = (widgetProps: IWidgetProps) => { const { cols, rows } = widgetProps const data = [...(widgetProps.data || this.state.widgetProps.data)] const customOrders = cols .concat(rows) .filter(({ sort }) => sort && sort.sortType === FieldSortTypes.Custom) .map(({ name, sort }) => ({ name, list: sort[FieldSortTypes.Custom].sortList })) fieldGroupedSort(data, customOrders) this.setState({ widgetProps: { ...widgetProps, data, references: this.state.references } }) } private saveWidget = () => { const { match, onAddWidget, onEditWidget } = this.props const { id, name, description, selectedViewId, controls, controlQueryMode, references, limit, cache, expired, widgetProps, computed, originalComputed, autoLoadData } = this.state if (!name.trim()) { message.error('Widget名称不能为空') return } if (!selectedViewId) { message.error('请选择一个View') return } const widget = { name, description, type: 1, viewId: selectedViewId, projectId: Number(match.params.projectId), config: JSON.stringify({ ...widgetProps, controls, queryMode: controlQueryMode, references, computed: originalComputed && originalComputed ? [...computed, ...originalComputed] : [...computed], limit, cache, expired, autoLoadData, data: [] }), publish: true } const prefix = window.localStorage.getItem('inDataService') ?? '' const prefixPath = prefix ? '/' + prefix : prefix if (id) { onEditWidget({ ...widget, id }, () => { message.success('修改成功') const editSignDashboard = sessionStorage.getItem( 'editWidgetFromDashboard' ) const editSignDisplay = sessionStorage.getItem('editWidgetFromDisplay') if (editSignDashboard) { sessionStorage.removeItem('editWidgetFromDashboard') const [ projectId, portalId, dashboardId, itemId ] = editSignDashboard.split(DEFAULT_SPLITER) this.props.history.replace( `/project/${projectId}/portal/${portalId}/dashboard/${dashboardId}` ) } else if (editSignDisplay) { sessionStorage.removeItem('editWidgetFromDisplay') const [projectId, displayId] = editSignDisplay.split(DEFAULT_SPLITER) this.props.history.replace( `/project/${projectId}/display/${displayId}` ) } else { // /project/1/dataShareService/widgets this.props.history.replace( `/project/${match.params.projectId}${prefixPath}/widgets` ) } }) } else { onAddWidget(widget, () => { message.success('添加成功') this.props.history.replace(`/project/${match.params.projectId}/widgets`) }) } } private cancel = () => { sessionStorage.removeItem('editWidgetFromDashboard') sessionStorage.removeItem('editWidgetFromDisplay') this.props.history.goBack() } private paginationChange = (pageNo: number, pageSize: number, orders) => { this.operatingPanel.flipPage(pageNo, pageSize, orders) } private chartStylesChange = (propPath: string[], value: string) => { const { widgetProps } = this.state const { chartStyles } = widgetProps const updatedChartStyles = { ...chartStyles } propPath.reduce((subObj, propName, idx) => { if (idx === propPath.length - 1) { subObj[propName] = value } return subObj[propName] }, updatedChartStyles) this.setWidgetProps({ ...widgetProps, chartStyles: updatedChartStyles }) } private saveSplitSize(newSize: number) { localStorage.setItem('workbenchSplitSize', newSize.toString()) } private resizeChart = () => { this.setState({ widgetProps: { ...this.state.widgetProps, renderType: 'resize' } }) } private changeAutoLoadData = (e) => { this.setState({ autoLoadData: e.target.value }) } private openSettingForm = () => { this.setState({ settingFormVisible: true }) } private saveSettingForm = (values: IWorkbenchSettings) => { try { const loginUser = JSON.parse(localStorage.getItem('loginUser')) localStorage.setItem( `${loginUser.id}_workbench_settings`, JSON.stringify(values) ) this.setState({ settings: values }) } catch (err) { throw new Error(err) } this.closeSettingForm() } private closeSettingForm = () => { this.setState({ settingFormVisible: false }) } public render() { const { views, formedViews, loading, dataLoading, onLoadViewData, onLoadColumnDistinctValue, onLoadViewDetail } = this.props const { name, description, selectedViewId, controls, controlQueryMode, references, limit, cache, autoLoadData, expired, computed, splitSize, originalWidgetProps, originalComputed, widgetProps, settingFormVisible, settings } = this.state const { queryMode: workbenchQueryMode, multiDrag } = settings const { selectedChart, cols, rows, metrics, data } = widgetProps const hasDataConfig = !!(cols.length || rows.length || metrics.length) const maskProps: IDashboardItemMaskProps = { loading: dataLoading, chartType: selectedChart, empty: !data.length, hasDataConfig } return (
(this.operatingPanel = f)} views={views} formedViews={formedViews} selectedViewId={selectedViewId} originalWidgetProps={originalWidgetProps} originalComputed={originalComputed} controls={controls} controlQueryMode={controlQueryMode} references={references} limit={limit} cache={cache} autoLoadData={autoLoadData} expired={expired} workbenchQueryMode={workbenchQueryMode} multiDrag={multiDrag} computed={computed} onViewSelect={this.viewSelect} onChangeAutoLoadData={this.changeAutoLoadData} onSetControls={this.setControls} onSetReferences={this.setReferences} onLimitChange={this.limitChange} onCacheChange={this.cacheChange} onExpiredChange={this.expiredChange} onSetWidgetProps={this.setWidgetProps} onSetComputed={this.setComputed} onDeleteComputed={this.deleteComputed} onLoadData={onLoadViewData} onLoadColumnDistinctValue={onLoadColumnDistinctValue} onLoadViews={this.loadViews} onLoadViewDetail={onLoadViewDetail} />
} empty={} editing={true} onPaginationChange={this.paginationChange} onChartStylesChange={this.chartStylesChange} />
) } } const mapStateToProps = createStructuredSelector({ views: makeSelectViews(), formedViews: makeSelectFormedViews(), currentWidget: makeSelectCurrentWidget(), loading: makeSelectLoading(), dataLoading: makeSelectDataLoading() }) export function mapDispatchToProps(dispatch) { return { onHideNavigator: () => dispatch(hideNavigator()), onLoadViews: (projectId, resolve) => dispatch(loadViews(projectId, resolve)), onLoadViewDetail: (viewIds, resolve) => dispatch(loadViewsDetail(viewIds, resolve)), onLoadWidgetDetail: (id) => dispatch(WidgetActions.loadWidgetDetail(id)), onLoadViewData: (viewId, requestParams, resolve, reject) => dispatch(loadViewData(viewId, requestParams, resolve, reject)), onAddWidget: (widget, resolve) => dispatch(WidgetActions.addWidget(widget, resolve)), onEditWidget: (widget, resolve) => dispatch(WidgetActions.editWidget(widget, resolve)), onLoadColumnDistinctValue: ( paramsByViewId: { [viewId: string]: Omit }, callback: (options?: object[]) => void ) => dispatch(loadColumnDistinctValue(paramsByViewId, callback)), onClearCurrentWidget: () => dispatch(WidgetActions.clearCurrentWidget()), onExecuteComputed: (sql) => dispatch(WidgetActions.executeComputed(sql)) } } const withConnect = connect<{}, {}>(mapStateToProps, mapDispatchToProps) const withReducerWidget = injectReducer({ key: 'widget', reducer }) const withSagaWidget = injectSaga({ key: 'widget', saga }) const withReducerView = injectReducer({ key: 'view', reducer: viewReducer }) const withSagaView = injectSaga({ key: 'view', saga: viewSaga }) const withControlReducer = injectReducer({ key: 'control', reducer: controlReducer }) export default compose( withReducerWidget, withReducerView, withControlReducer, withSagaView, withSagaWidget, withConnect )(Workbench)