This is an automated email from the ASF dual-hosted git repository. rusackas pushed a commit to branch move-controls in repository https://gitbox.apache.org/repos/asf/superset.git
commit 7f4a3a3d0ff45b62fd2b95f535275663365450e6 Author: Evan Rusackas <[email protected]> AuthorDate: Wed Aug 13 14:34:06 2025 -0700 refactor: Modernize control panel layouts with Ant Design Grid - Replace Bootstrap grid classes with Ant Design Row/Col components - Update ControlRow.tsx to use Ant Design grid system - Replace all className="control-row" with Row/Col components - Import Row/Col from @superset-ui/core/components for consistency - Add new ControlPanelLayout utilities for flexible layouts - Standardize spacing with gutter={[16, 8]} across all control groups This modernizes the control panel layout system to use Ant Design's consistent grid system instead of mixed Bootstrap/custom CSS classes, improving maintainability and visual consistency. 🤖 Generated with Claude Code Co-Authored-By: Claude <[email protected]> --- .../components/AxisControlSection.tsx | 242 ++++++------ .../components/ControlPanelLayout.tsx | 154 ++++++++ .../components/DeckGLControlsSection.tsx | 419 ++++++++++---------- .../components/FilterControlsSection.tsx | 421 +++++++++++---------- .../components/FormatControlGroup.tsx | 183 ++++----- .../components/LabelControlGroup.tsx | 241 ++++++------ .../components/MarkerControlGroup.tsx | 105 ++--- .../shared-controls/components/OpacityControl.tsx | 3 +- .../shared-controls/components/PieShapeControl.tsx | 205 +++++----- .../components/TableControlsSection.tsx | 311 ++++++++------- .../components/TimeseriesControlPanel.tsx | 117 +++--- .../src/shared-controls/components/index.tsx | 3 + .../src/explore/components/ControlRow.tsx | 50 +-- 13 files changed, 1388 insertions(+), 1066 deletions(-) diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/AxisControlSection.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/AxisControlSection.tsx index d393a5bcbb..2ed219f4d1 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/AxisControlSection.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/AxisControlSection.tsx @@ -19,6 +19,7 @@ import { FC } from 'react'; import { t } from '@superset-ui/core'; import { Input, Select, Switch, InputNumber } from 'antd'; +import { Row, Col } from '@superset-ui/core/components'; export interface AxisControlSectionProps { axis: 'x' | 'y'; @@ -97,137 +98,152 @@ export const AxisControlSection: FC<AxisControlSectionProps> = ({ return ( <div className="axis-control-section"> {showTitle && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t(`${axisUpper} Axis Title`)}</label> - <Input - value={values[titleKey] || ''} - onChange={e => onChange(titleKey, e.target.value)} - placeholder={t(`Enter ${axis} axis title`)} - /> - <small className="text-muted"> - {t( - 'Overrides the axis title derived from the metric or column name', - )} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t(`${axisUpper} Axis Title`)}</label> + <Input + value={values[titleKey] || ''} + onChange={e => onChange(titleKey, e.target.value)} + placeholder={t(`Enter ${axis} axis title`)} + /> + <small className="text-muted"> + {t( + 'Overrides the axis title derived from the metric or column name', + )} + </small> + </Col> + </Row> )} {showFormat && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t(`${axisUpper} Axis Format`)}</label> - <Select - value={ - values[formatKey] || (timeFormat ? 'smart_date' : 'SMART_NUMBER') - } - onChange={value => onChange(formatKey, value)} - style={{ width: '100%' }} - showSearch - placeholder={t('Select or type a format')} - options={(timeFormat - ? D3_TIME_FORMAT_OPTIONS - : D3_FORMAT_OPTIONS - ).map(([value, label]) => ({ - value, - label, - }))} - /> - <small className="text-muted"> - {timeFormat - ? t('D3 time format for x axis') - : t('D3 format for axis values')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t(`${axisUpper} Axis Format`)}</label> + <Select + value={ + values[formatKey] || + (timeFormat ? 'smart_date' : 'SMART_NUMBER') + } + onChange={value => onChange(formatKey, value)} + style={{ width: '100%' }} + showSearch + placeholder={t('Select or type a format')} + options={(timeFormat + ? D3_TIME_FORMAT_OPTIONS + : D3_FORMAT_OPTIONS + ).map(([value, label]) => ({ + value, + label, + }))} + /> + <small className="text-muted"> + {timeFormat + ? t('D3 time format for x axis') + : t('D3 format for axis values')} + </small> + </Col> + </Row> )} {showRotation && isXAxis && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Label Rotation')}</label> - <Select - value={values[rotationKey] || 0} - onChange={value => onChange(rotationKey, value)} - style={{ width: '100%' }} - options={ROTATION_OPTIONS.map(([value, label]) => ({ - value, - label, - }))} - /> - <small className="text-muted"> - {t('Rotation angle for axis labels')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Label Rotation')}</label> + <Select + value={values[rotationKey] || 0} + onChange={value => onChange(rotationKey, value)} + style={{ width: '100%' }} + options={ROTATION_OPTIONS.map(([value, label]) => ({ + value, + label, + }))} + /> + <small className="text-muted"> + {t('Rotation angle for axis labels')} + </small> + </Col> + </Row> )} {showBounds && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t(`${axisUpper} Axis Bounds`)}</label> - <div style={{ display: 'flex', gap: 8 }}> - <InputNumber - value={values[boundsMinKey]} - onChange={value => onChange(boundsMinKey, value)} - placeholder={t('Min')} - style={{ flex: 1 }} - /> - <InputNumber - value={values[boundsMaxKey]} - onChange={value => onChange(boundsMaxKey, value)} - placeholder={t('Max')} - style={{ flex: 1 }} - /> - </div> - <small className="text-muted"> - {t('Bounds for axis values. Leave empty for automatic scaling.')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t(`${axisUpper} Axis Bounds`)}</label> + <div style={{ display: 'flex', gap: 8 }}> + <InputNumber + value={values[boundsMinKey]} + onChange={value => onChange(boundsMinKey, value)} + placeholder={t('Min')} + style={{ flex: 1 }} + /> + <InputNumber + value={values[boundsMaxKey]} + onChange={value => onChange(boundsMaxKey, value)} + placeholder={t('Max')} + style={{ flex: 1 }} + /> + </div> + <small className="text-muted"> + {t('Bounds for axis values. Leave empty for automatic scaling.')} + </small> + </Col> + </Row> )} {showLogarithmic && !isXAxis && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values[logScaleKey] || false} - onChange={checked => onChange(logScaleKey, checked)} - /> - {t('Logarithmic Scale')} - </label> - <small className="text-muted"> - {t('Use a logarithmic scale for the Y-axis')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values[logScaleKey] || false} + onChange={checked => onChange(logScaleKey, checked)} + /> + {t('Logarithmic Scale')} + </label> + <small className="text-muted"> + {t('Use a logarithmic scale for the Y-axis')} + </small> + </Col> + </Row> )} {showMinorTicks && !isXAxis && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values[minorTicksKey] || false} - onChange={checked => onChange(minorTicksKey, checked)} - /> - {t('Show Minor Ticks')} - </label> - <small className="text-muted"> - {t('Show minor grid lines on the axis')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values[minorTicksKey] || false} + onChange={checked => onChange(minorTicksKey, checked)} + /> + {t('Show Minor Ticks')} + </label> + <small className="text-muted"> + {t('Show minor grid lines on the axis')} + </small> + </Col> + </Row> )} {showTruncate && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={ - values[truncateKey] || values[truncateLabelsKey] || false - } - onChange={checked => { - onChange(truncateKey, checked); - onChange(truncateLabelsKey, checked); - }} - /> - {t(`Truncate ${axisUpper} Axis Labels`)} - </label> - <small className="text-muted"> - {t('Truncate long axis labels to prevent overlap')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={ + values[truncateKey] || values[truncateLabelsKey] || false + } + onChange={checked => { + onChange(truncateKey, checked); + onChange(truncateLabelsKey, checked); + }} + /> + {t(`Truncate ${axisUpper} Axis Labels`)} + </label> + <small className="text-muted"> + {t('Truncate long axis labels to prevent overlap')} + </small> + </Col> + </Row> )} </div> ); diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ControlPanelLayout.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ControlPanelLayout.tsx new file mode 100644 index 0000000000..af5ee5b9cc --- /dev/null +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/ControlPanelLayout.tsx @@ -0,0 +1,154 @@ +/** + * 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 { ReactNode, FC } from 'react'; +import { Row, Col, Collapse } from '@superset-ui/core/components'; + +/** + * Props for control panel sections + */ +export interface ControlSectionProps { + label?: ReactNode; + description?: ReactNode; + expanded?: boolean; + children: ReactNode; +} + +/** + * A collapsible section in the control panel + */ +export const ControlSection: FC<ControlSectionProps> = ({ + label, + description, + expanded = true, + children, +}) => { + if (!label) { + // No label means no collapsible wrapper + return <>{children}</>; + } + + return ( + <Collapse defaultActiveKey={expanded ? ['1'] : []} ghost> + <Collapse.Panel + header={ + <span> + {label} + {description && ( + <span style={{ marginLeft: 8, fontSize: '0.85em', opacity: 0.7 }}> + {description} + </span> + )} + </span> + } + key="1" + > + {children} + </Collapse.Panel> + </Collapse> + ); +}; + +/** + * Props for control row - uses Ant Design grid + */ +export interface ControlRowProps { + children: ReactNode; + gutter?: number | [number, number]; +} + +/** + * A row of controls using Ant Design's grid system + * Automatically distributes controls evenly across columns + */ +export const ControlPanelRow: FC<ControlRowProps> = ({ + children, + gutter = [16, 16], +}) => { + const childArray = Array.isArray(children) ? children : [children]; + const validChildren = childArray.filter( + child => child !== null && child !== undefined, + ); + const colSpan = + validChildren.length > 0 ? Math.floor(24 / validChildren.length) : 24; + + return ( + <Row gutter={gutter} style={{ marginBottom: 16 }}> + {validChildren.map((child, index) => ( + <Col key={index} span={colSpan}> + {child} + </Col> + ))} + </Row> + ); +}; + +/** + * Props for the main control panel layout + */ +export interface ControlPanelLayoutProps { + children: ReactNode; +} + +/** + * Main control panel layout container + */ +export const ControlPanelLayout: FC<ControlPanelLayoutProps> = ({ + children, +}) => ( + <div className="control-panel-layout" style={{ padding: '16px 0' }}> + {children} + </div> +); + +/** + * Helper function to create a full-width single control row + */ +export const SingleControlRow: FC<{ children: ReactNode }> = ({ children }) => ( + <Row gutter={[16, 16]} style={{ marginBottom: 16 }}> + <Col span={24}>{children}</Col> + </Row> +); + +/** + * Helper function to create a two-column control row + */ +export const TwoColumnRow: FC<{ left: ReactNode; right: ReactNode }> = ({ + left, + right, +}) => ( + <Row gutter={[16, 16]} style={{ marginBottom: 16 }}> + <Col span={12}>{left}</Col> + <Col span={12}>{right}</Col> + </Row> +); + +/** + * Helper function to create a three-column control row + */ +export const ThreeColumnRow: FC<{ + left: ReactNode; + center: ReactNode; + right: ReactNode; +}> = ({ left, center, right }) => ( + <Row gutter={[16, 16]} style={{ marginBottom: 16 }}> + <Col span={8}>{left}</Col> + <Col span={8}>{center}</Col> + <Col span={8}>{right}</Col> + </Row> +); diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/DeckGLControlsSection.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/DeckGLControlsSection.tsx index 8ffe9cbd95..dc5902f1d3 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/DeckGLControlsSection.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/DeckGLControlsSection.tsx @@ -19,6 +19,7 @@ import { FC } from 'react'; import { t } from '@superset-ui/core'; import { Switch, Select, Input, Slider } from 'antd'; +import { Row, Col } from '@superset-ui/core/components'; export interface DeckGLControlsSectionProps { layerType?: @@ -76,252 +77,282 @@ const DeckGLControlsSection: FC<DeckGLControlsSectionProps> = ({ <div className="deckgl-controls-section"> {/* Map Style */} {showMapStyle && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Map Style')}</label> - <Select - value={values.mapbox_style || 'mapbox://styles/mapbox/light-v9'} - onChange={value => onChange('mapbox_style', value)} - style={{ width: '100%' }} - options={[ - { - value: 'mapbox://styles/mapbox/streets-v11', - label: t('Streets'), - }, - { value: 'mapbox://styles/mapbox/light-v9', label: t('Light') }, - { value: 'mapbox://styles/mapbox/dark-v9', label: t('Dark') }, - { - value: 'mapbox://styles/mapbox/satellite-v9', - label: t('Satellite'), - }, - { - value: 'mapbox://styles/mapbox/outdoors-v11', - label: t('Outdoors'), - }, - ]} - /> - <small className="text-muted"> - {t('Base map style for the visualization')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Map Style')}</label> + <Select + value={values.mapbox_style || 'mapbox://styles/mapbox/light-v9'} + onChange={value => onChange('mapbox_style', value)} + style={{ width: '100%' }} + options={[ + { + value: 'mapbox://styles/mapbox/streets-v11', + label: t('Streets'), + }, + { value: 'mapbox://styles/mapbox/light-v9', label: t('Light') }, + { value: 'mapbox://styles/mapbox/dark-v9', label: t('Dark') }, + { + value: 'mapbox://styles/mapbox/satellite-v9', + label: t('Satellite'), + }, + { + value: 'mapbox://styles/mapbox/outdoors-v11', + label: t('Outdoors'), + }, + ]} + /> + <small className="text-muted"> + {t('Base map style for the visualization')} + </small> + </Col> + </Row> )} {/* Viewport */} {showViewport && ( <> - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Zoom')}</label> - <Slider - value={values.zoom || 11} - onChange={value => onChange('zoom', value)} - min={0} - max={22} - step={0.1} - marks={{ 0: '0', 11: '11', 22: '22' }} - /> - <small className="text-muted">{t('Map zoom level')}</small> - </div> - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.autozoom || true} - onChange={checked => onChange('autozoom', checked)} + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Zoom')}</label> + <Slider + value={values.zoom || 11} + onChange={value => onChange('zoom', value)} + min={0} + max={22} + step={0.1} + marks={{ 0: '0', 11: '11', 22: '22' }} /> - {t('Auto Zoom')} - </label> - <small className="text-muted"> - {t('Automatically zoom to fit data bounds')} - </small> - </div> + <small className="text-muted">{t('Map zoom level')}</small> + </Col> + </Row> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.autozoom || true} + onChange={checked => onChange('autozoom', checked)} + /> + {t('Auto Zoom')} + </label> + <small className="text-muted"> + {t('Automatically zoom to fit data bounds')} + </small> + </Col> + </Row> </> )} {/* Point/Shape Size Controls */} {showPointRadius && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Point Radius')}</label> - <Slider - value={values.point_radius_fixed?.value || 1000} - onChange={value => - onChange('point_radius_fixed', { type: 'fix', value }) - } - min={1} - max={10000} - step={10} - /> - <small className="text-muted"> - {t('Fixed radius for points in meters')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Point Radius')}</label> + <Slider + value={values.point_radius_fixed?.value || 1000} + onChange={value => + onChange('point_radius_fixed', { type: 'fix', value }) + } + min={1} + max={10000} + step={10} + /> + <small className="text-muted"> + {t('Fixed radius for points in meters')} + </small> + </Col> + </Row> )} {showLineWidth && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Line Width')}</label> - <Slider - value={values.line_width || 1} - onChange={value => onChange('line_width', value)} - min={1} - max={50} - step={1} - /> - <small className="text-muted">{t('Width of lines in pixels')}</small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Line Width')}</label> + <Slider + value={values.line_width || 1} + onChange={value => onChange('line_width', value)} + min={1} + max={50} + step={1} + /> + <small className="text-muted">{t('Width of lines in pixels')}</small> + </Col> + </Row> )} {/* 3D Controls */} {show3D && ( <> - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.extruded || false} - onChange={checked => onChange('extruded', checked)} - /> - {t('3D')} - </label> - <small className="text-muted">{t('Show data in 3D')}</small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.extruded || false} + onChange={checked => onChange('extruded', checked)} + /> + {t('3D')} + </label> + <small className="text-muted">{t('Show data in 3D')}</small> + </Col> + </Row> {values.extruded && showElevation && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Elevation')}</label> - <Slider - value={values.elevation || 0.1} - onChange={value => onChange('elevation', value)} - min={0} - max={1} - step={0.01} - /> - <small className="text-muted"> - {t('Elevation multiplier for 3D rendering')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Elevation')}</label> + <Slider + value={values.elevation || 0.1} + onChange={value => onChange('elevation', value)} + min={0} + max={1} + step={0.01} + /> + <small className="text-muted"> + {t('Elevation multiplier for 3D rendering')} + </small> + </Col> + </Row> )} </> )} {/* Opacity */} {showOpacity && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Opacity')}</label> - <Slider - value={values.opacity || 80} - onChange={value => onChange('opacity', value)} - min={0} - max={100} - step={1} - marks={{ 0: '0%', 50: '50%', 100: '100%' }} - /> - <small className="text-muted">{t('Layer opacity')}</small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Opacity')}</label> + <Slider + value={values.opacity || 80} + onChange={value => onChange('opacity', value)} + min={0} + max={100} + step={1} + marks={{ 0: '0%', 50: '50%', 100: '100%' }} + /> + <small className="text-muted">{t('Layer opacity')}</small> + </Col> + </Row> )} {/* Coverage (for hex, grid) */} {showCoverage && (layerType === 'hex' || layerType === 'grid') && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Coverage')}</label> - <Slider - value={values.coverage || 1} - onChange={value => onChange('coverage', value)} - min={0} - max={1} - step={0.01} - /> - <small className="text-muted">{t('Cell coverage radius')}</small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Coverage')}</label> + <Slider + value={values.coverage || 1} + onChange={value => onChange('coverage', value)} + min={0} + max={1} + step={0.01} + /> + <small className="text-muted">{t('Cell coverage radius')}</small> + </Col> + </Row> )} {/* Legend */} {showLegend && ( <> - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Legend Position')}</label> - <Select - value={values.legend_position || 'top_right'} - onChange={value => onChange('legend_position', value)} - style={{ width: '100%' }} - options={[ - { value: 'top_left', label: t('Top left') }, - { value: 'top_right', label: t('Top right') }, - { value: 'bottom_left', label: t('Bottom left') }, - { value: 'bottom_right', label: t('Bottom right') }, - ]} - /> - </div> - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Legend Format')}</label> - <Input - value={values.legend_format || ''} - onChange={e => onChange('legend_format', e.target.value)} - placeholder=".3s" - /> - <small className="text-muted"> - {t('D3 number format for legend')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Legend Position')}</label> + <Select + value={values.legend_position || 'top_right'} + onChange={value => onChange('legend_position', value)} + style={{ width: '100%' }} + options={[ + { value: 'top_left', label: t('Top left') }, + { value: 'top_right', label: t('Top right') }, + { value: 'bottom_left', label: t('Bottom left') }, + { value: 'bottom_right', label: t('Bottom right') }, + ]} + /> + </Col> + </Row> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Legend Format')}</label> + <Input + value={values.legend_format || ''} + onChange={e => onChange('legend_format', e.target.value)} + placeholder=".3s" + /> + <small className="text-muted"> + {t('D3 number format for legend')} + </small> + </Col> + </Row> </> )} {/* Filters */} {showFilters && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.filter_nulls || true} - onChange={checked => onChange('filter_nulls', checked)} - /> - {t('Filter Nulls')} - </label> - <small className="text-muted"> - {t('Filter out null values from data')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.filter_nulls || true} + onChange={checked => onChange('filter_nulls', checked)} + /> + {t('Filter Nulls')} + </label> + <small className="text-muted"> + {t('Filter out null values from data')} + </small> + </Col> + </Row> )} {/* Tooltip */} {showTooltip && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Tooltip')}</label> - <Input.TextArea - value={values.js_tooltip || ''} - onChange={e => onChange('js_tooltip', e.target.value)} - placeholder={t('JavaScript tooltip generator')} - rows={3} - /> - <small className="text-muted"> - {t('JavaScript code for custom tooltip')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Tooltip')}</label> + <Input.TextArea + value={values.js_tooltip || ''} + onChange={e => onChange('js_tooltip', e.target.value)} + placeholder={t('JavaScript tooltip generator')} + rows={3} + /> + <small className="text-muted"> + {t('JavaScript code for custom tooltip')} + </small> + </Col> + </Row> )} {/* Animation */} {showAnimation && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.animation || false} - onChange={checked => onChange('animation', checked)} - /> - {t('Animate')} - </label> - <small className="text-muted"> - {t('Animate visualization over time')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.animation || false} + onChange={checked => onChange('animation', checked)} + /> + {t('Animate')} + </label> + <small className="text-muted"> + {t('Animate visualization over time')} + </small> + </Col> + </Row> )} {/* Multiplier for some visualizations */} {showMultiplier && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Multiplier')}</label> - <Slider - value={values.multiplier || 1} - onChange={value => onChange('multiplier', value)} - min={0.01} - max={10} - step={0.01} - /> - <small className="text-muted">{t('Value multiplier')}</small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Multiplier')}</label> + <Slider + value={values.multiplier || 1} + onChange={value => onChange('multiplier', value)} + min={0.01} + max={10} + step={0.01} + /> + <small className="text-muted">{t('Value multiplier')}</small> + </Col> + </Row> )} </div> ); diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/FilterControlsSection.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/FilterControlsSection.tsx index 48a700f4c9..3e99494ab9 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/FilterControlsSection.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/FilterControlsSection.tsx @@ -19,6 +19,7 @@ import { FC } from 'react'; import { t } from '@superset-ui/core'; import { Switch, Select, Input, InputNumber } from 'antd'; +import { Row, Col } from '@superset-ui/core/components'; export interface FilterControlsSectionProps { filterType: 'select' | 'range' | 'time' | 'time_column' | 'time_grain'; @@ -53,204 +54,230 @@ const FilterControlsSection: FC<FilterControlsSectionProps> = ({ <div className="filter-controls-section"> {/* Multiple Selection */} {showMultiple && isSelect && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.multiSelect || false} - onChange={checked => onChange('multiSelect', checked)} - /> - {t('Multiple Select')} - </label> - <small className="text-muted"> - {t('Allow selecting multiple values')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.multiSelect || false} + onChange={checked => onChange('multiSelect', checked)} + /> + {t('Multiple Select')} + </label> + <small className="text-muted"> + {t('Allow selecting multiple values')} + </small> + </Col> + </Row> )} {/* Search */} {showSearch && isSelect && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.enableEmptyFilter || false} - onChange={checked => onChange('enableEmptyFilter', checked)} - /> - {t('Enable Empty Filter')} - </label> - <small className="text-muted">{t('Allow empty filter values')}</small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.enableEmptyFilter || false} + onChange={checked => onChange('enableEmptyFilter', checked)} + /> + {t('Enable Empty Filter')} + </label> + <small className="text-muted"> + {t('Allow empty filter values')} + </small> + </Col> + </Row> )} {/* Inverse Selection */} {showInverseSelection && isSelect && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.inverseSelection || false} - onChange={checked => onChange('inverseSelection', checked)} - /> - {t('Inverse Selection')} - </label> - <small className="text-muted"> - {t('Exclude selected values instead of including them')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.inverseSelection || false} + onChange={checked => onChange('inverseSelection', checked)} + /> + {t('Inverse Selection')} + </label> + <small className="text-muted"> + {t('Exclude selected values instead of including them')} + </small> + </Col> + </Row> )} {/* Parent Filter */} {showParentFilter && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.parentFilter || false} - onChange={checked => onChange('parentFilter', checked)} - /> - {t('Parent Filter')} - </label> - <small className="text-muted"> - {t('Filter is dependent on another filter')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.parentFilter || false} + onChange={checked => onChange('parentFilter', checked)} + /> + {t('Parent Filter')} + </label> + <small className="text-muted"> + {t('Filter is dependent on another filter')} + </small> + </Col> + </Row> )} {/* Default Value */} {showDefaultValue && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Default Value')}</label> - {isSelect ? ( - <Input - value={values.defaultValue || ''} - onChange={e => onChange('defaultValue', e.target.value)} - placeholder={t('Enter default value')} - /> - ) : isRange ? ( - <div style={{ display: 'flex', gap: 8 }}> - <InputNumber - value={values.defaultValueMin} - onChange={value => onChange('defaultValueMin', value)} - placeholder={t('Min')} - style={{ flex: 1 }} + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Default Value')}</label> + {isSelect ? ( + <Input + value={values.defaultValue || ''} + onChange={e => onChange('defaultValue', e.target.value)} + placeholder={t('Enter default value')} /> - <InputNumber - value={values.defaultValueMax} - onChange={value => onChange('defaultValueMax', value)} - placeholder={t('Max')} - style={{ flex: 1 }} + ) : isRange ? ( + <div style={{ display: 'flex', gap: 8 }}> + <InputNumber + value={values.defaultValueMin} + onChange={value => onChange('defaultValueMin', value)} + placeholder={t('Min')} + style={{ flex: 1 }} + /> + <InputNumber + value={values.defaultValueMax} + onChange={value => onChange('defaultValueMax', value)} + placeholder={t('Max')} + style={{ flex: 1 }} + /> + </div> + ) : ( + <Input + value={values.defaultValue || ''} + onChange={e => onChange('defaultValue', e.target.value)} + placeholder={t('Enter default value')} /> - </div> - ) : ( - <Input - value={values.defaultValue || ''} - onChange={e => onChange('defaultValue', e.target.value)} - placeholder={t('Enter default value')} - /> - )} - <small className="text-muted"> - {t('Default value to use when filter is first loaded')} - </small> - </div> + )} + <small className="text-muted"> + {t('Default value to use when filter is first loaded')} + </small> + </Col> + </Row> )} {/* Sort Options for Select */} {isSelect && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Sort Filter Values')}</label> - <Select - value={values.sortFilter || false} - onChange={value => onChange('sortFilter', value)} - style={{ width: '100%' }} - options={[ - { value: false, label: t('No Sort') }, - { value: true, label: t('Sort Ascending') }, - { value: 'desc', label: t('Sort Descending') }, - ]} - /> - <small className="text-muted"> - {t('Sort filter values alphabetically')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Sort Filter Values')}</label> + <Select + value={values.sortFilter || false} + onChange={value => onChange('sortFilter', value)} + style={{ width: '100%' }} + options={[ + { value: false, label: t('No Sort') }, + { value: true, label: t('Sort Ascending') }, + { value: 'desc', label: t('Sort Descending') }, + ]} + /> + <small className="text-muted"> + {t('Sort filter values alphabetically')} + </small> + </Col> + </Row> )} {/* Search for Select Filter */} {isSelect && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.searchAllOptions || false} - onChange={checked => onChange('searchAllOptions', checked)} - /> - {t('Search All Options')} - </label> - <small className="text-muted"> - {t('Search all filter options, not just displayed ones')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.searchAllOptions || false} + onChange={checked => onChange('searchAllOptions', checked)} + /> + {t('Search All Options')} + </label> + <small className="text-muted"> + {t('Search all filter options, not just displayed ones')} + </small> + </Col> + </Row> )} {/* Range Options */} {isRange && ( <> - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Min Value')}</label> - <InputNumber - value={values.rangeMin} - onChange={value => onChange('rangeMin', value)} - style={{ width: '100%' }} - /> - <small className="text-muted"> - {t('Minimum value for the range')} - </small> - </div> - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Max Value')}</label> - <InputNumber - value={values.rangeMax} - onChange={value => onChange('rangeMax', value)} - style={{ width: '100%' }} - /> - <small className="text-muted"> - {t('Maximum value for the range')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Min Value')}</label> + <InputNumber + value={values.rangeMin} + onChange={value => onChange('rangeMin', value)} + style={{ width: '100%' }} + /> + <small className="text-muted"> + {t('Minimum value for the range')} + </small> + </Col> + </Row> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Max Value')}</label> + <InputNumber + value={values.rangeMax} + onChange={value => onChange('rangeMax', value)} + style={{ width: '100%' }} + /> + <small className="text-muted"> + {t('Maximum value for the range')} + </small> + </Col> + </Row> </> )} {/* Time Options */} {(isTime || isTimeColumn) && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.defaultToFirstValue || false} - onChange={checked => onChange('defaultToFirstValue', checked)} - /> - {t('Default to First Value')} - </label> - <small className="text-muted"> - {t('Default to the first available time value')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.defaultToFirstValue || false} + onChange={checked => onChange('defaultToFirstValue', checked)} + /> + {t('Default to First Value')} + </label> + <small className="text-muted"> + {t('Default to the first available time value')} + </small> + </Col> + </Row> )} {/* Time Grain Options */} {isTimeGrain && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Default Time Grain')}</label> - <Select - value={values.defaultTimeGrain || 'day'} - onChange={value => onChange('defaultTimeGrain', value)} - style={{ width: '100%' }} - options={[ - { value: 'minute', label: t('Minute') }, - { value: 'hour', label: t('Hour') }, - { value: 'day', label: t('Day') }, - { value: 'week', label: t('Week') }, - { value: 'month', label: t('Month') }, - { value: 'quarter', label: t('Quarter') }, - { value: 'year', label: t('Year') }, - ]} - /> - <small className="text-muted">{t('Default time granularity')}</small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Default Time Grain')}</label> + <Select + value={values.defaultTimeGrain || 'day'} + onChange={value => onChange('defaultTimeGrain', value)} + style={{ width: '100%' }} + options={[ + { value: 'minute', label: t('Minute') }, + { value: 'hour', label: t('Hour') }, + { value: 'day', label: t('Day') }, + { value: 'week', label: t('Week') }, + { value: 'month', label: t('Month') }, + { value: 'quarter', label: t('Quarter') }, + { value: 'year', label: t('Year') }, + ]} + /> + <small className="text-muted"> + {t('Default time granularity')} + </small> + </Col> + </Row> )} {/* UI Configuration */} @@ -258,44 +285,50 @@ const FilterControlsSection: FC<FilterControlsSectionProps> = ({ {t('UI Configuration')} </h4> - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.instant_filtering || true} - onChange={checked => onChange('instant_filtering', checked)} - /> - {t('Instant Filtering')} - </label> - <small className="text-muted"> - {t('Apply filters instantly as they change')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.instant_filtering || true} + onChange={checked => onChange('instant_filtering', checked)} + /> + {t('Instant Filtering')} + </label> + <small className="text-muted"> + {t('Apply filters instantly as they change')} + </small> + </Col> + </Row> - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.show_apply || false} - onChange={checked => onChange('show_apply', checked)} - /> - {t('Show Apply Button')} - </label> - <small className="text-muted"> - {t('Show an apply button for the filter')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.show_apply || false} + onChange={checked => onChange('show_apply', checked)} + /> + {t('Show Apply Button')} + </label> + <small className="text-muted"> + {t('Show an apply button for the filter')} + </small> + </Col> + </Row> - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.show_clear || true} - onChange={checked => onChange('show_clear', checked)} - /> - {t('Show Clear Button')} - </label> - <small className="text-muted"> - {t('Show a clear button for the filter')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.show_clear || true} + onChange={checked => onChange('show_clear', checked)} + /> + {t('Show Clear Button')} + </label> + <small className="text-muted"> + {t('Show a clear button for the filter')} + </small> + </Col> + </Row> </div> ); }; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/FormatControlGroup.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/FormatControlGroup.tsx index 9da5cf90bc..c9b98e4ceb 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/FormatControlGroup.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/FormatControlGroup.tsx @@ -19,6 +19,7 @@ import { FC } from 'react'; import { t } from '@superset-ui/core'; import { Select } from 'antd'; +import { Row, Col } from '@superset-ui/core/components'; export interface FormatControlGroupProps { showNumber?: boolean; @@ -127,101 +128,111 @@ const FormatControlGroup: FC<FormatControlGroupProps> = ({ return ( <div className="format-control-group"> {showNumber && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{numberFormatLabel}</label> - <Select - value={values.number_format || 'SMART_NUMBER'} - onChange={value => onChange('number_format', value)} - style={{ width: '100%' }} - showSearch - placeholder={t('Select or type a custom format')} - options={formatOptions.map(([value, label]) => ({ - value, - label, - }))} - /> - <small className="text-muted"> - {t('D3 format string for numbers. See ')} - <a - href="https://github.com/d3/d3-format/blob/main/README.md#format" - target="_blank" - rel="noopener noreferrer" - > - {t('D3 format docs')} - </a> - {t(' for details.')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{numberFormatLabel}</label> + <Select + value={values.number_format || 'SMART_NUMBER'} + onChange={value => onChange('number_format', value)} + style={{ width: '100%' }} + showSearch + placeholder={t('Select or type a custom format')} + options={formatOptions.map(([value, label]) => ({ + value, + label, + }))} + /> + <small className="text-muted"> + {t('D3 format string for numbers. See ')} + <a + href="https://github.com/d3/d3-format/blob/main/README.md#format" + target="_blank" + rel="noopener noreferrer" + > + {t('D3 format docs')} + </a> + {t(' for details.')} + </small> + </Col> + </Row> )} {showCurrency && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{currencyFormatLabel}</label> - <Select - value={values.currency_format || 'USD'} - onChange={value => onChange('currency_format', value)} - style={{ width: '100%' }} - showSearch - placeholder={t('Select currency')} - options={CURRENCY_OPTIONS} - /> - <small className="text-muted"> - {t('Currency to use for formatting')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{currencyFormatLabel}</label> + <Select + value={values.currency_format || 'USD'} + onChange={value => onChange('currency_format', value)} + style={{ width: '100%' }} + showSearch + placeholder={t('Select currency')} + options={CURRENCY_OPTIONS} + /> + <small className="text-muted"> + {t('Currency to use for formatting')} + </small> + </Col> + </Row> )} {showDate && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{dateFormatLabel}</label> - <Select - value={values.date_format || 'smart_date'} - onChange={value => onChange('date_format', value)} - style={{ width: '100%' }} - showSearch - placeholder={t('Select or type a custom format')} - options={D3_TIME_FORMAT_OPTIONS.map(([value, label]) => ({ - value, - label, - }))} - /> - <small className="text-muted"> - {t('D3 time format string. See ')} - <a - href="https://github.com/d3/d3-time-format/blob/main/README.md#locale_format" - target="_blank" - rel="noopener noreferrer" - > - {t('D3 time format docs')} - </a> - {t(' for details.')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{dateFormatLabel}</label> + <Select + value={values.date_format || 'smart_date'} + onChange={value => onChange('date_format', value)} + style={{ width: '100%' }} + showSearch + placeholder={t('Select or type a custom format')} + options={D3_TIME_FORMAT_OPTIONS.map(([value, label]) => ({ + value, + label, + }))} + /> + <small className="text-muted"> + {t('D3 time format string. See ')} + <a + href="https://github.com/d3/d3-time-format/blob/main/README.md#locale_format" + target="_blank" + rel="noopener noreferrer" + > + {t('D3 time format docs')} + </a> + {t(' for details.')} + </small> + </Col> + </Row> )} {showPercentage && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{percentageFormatLabel}</label> - <Select - value={values.percentage_format || '.0%'} - onChange={value => onChange('percentage_format', value)} - style={{ width: '100%' }} - showSearch - placeholder={t('Select or type a custom format')} - options={[ - ['.0%', t('0%')], - ['.1%', t('0.1%')], - ['.2%', t('0.12%')], - ['.3%', t('0.123%')], - [',.0%', t('1,234%')], - [',.1%', t('1,234.5%')], - ].map(([value, label]) => ({ - value, - label, - }))} - /> - <small className="text-muted">{t('D3 format for percentages')}</small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{percentageFormatLabel}</label> + <Select + value={values.percentage_format || '.0%'} + onChange={value => onChange('percentage_format', value)} + style={{ width: '100%' }} + showSearch + placeholder={t('Select or type a custom format')} + options={[ + ['.0%', t('0%')], + ['.1%', t('0.1%')], + ['.2%', t('0.12%')], + ['.3%', t('0.123%')], + [',.0%', t('1,234%')], + [',.1%', t('1,234.5%')], + ].map(([value, label]) => ({ + value, + label, + }))} + /> + <small className="text-muted"> + {t('D3 format for percentages')} + </small> + </Col> + </Row> )} </div> ); diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/LabelControlGroup.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/LabelControlGroup.tsx index 7841ac1192..86cd58d2a2 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/LabelControlGroup.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/LabelControlGroup.tsx @@ -19,6 +19,7 @@ import { FC } from 'react'; import { t } from '@superset-ui/core'; import { Select, Switch, InputNumber, Input } from 'antd'; +import { Row, Col } from '@superset-ui/core/components'; export interface LabelControlGroupProps { chartType?: 'pie' | 'sunburst' | 'treemap' | 'funnel' | 'gauge'; @@ -69,143 +70,167 @@ const LabelControlGroup: FC<LabelControlGroupProps> = ({ return ( <div className="label-control-group"> {/* Show Labels Toggle */} - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={showLabels} - onChange={checked => onChange('show_labels', checked)} - /> - {t('Show Labels')} - </label> - <small className="text-muted"> - {t('Whether to display the labels')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={showLabels} + onChange={checked => onChange('show_labels', checked)} + /> + {t('Show Labels')} + </label> + <small className="text-muted"> + {t('Whether to display the labels')} + </small> + </Col> + </Row> {showLabels && ( <> {/* Label Type */} {showLabelType && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Label Type')}</label> - <Select - value={labelType} - onChange={value => onChange('label_type', value)} - style={{ width: '100%' }} - options={LABEL_TYPE_OPTIONS.map(([value, label]) => ({ - value, - label, - }))} - /> - <small className="text-muted"> - {t('What should be shown on the label?')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Label Type')}</label> + <Select + value={labelType} + onChange={value => onChange('label_type', value)} + style={{ width: '100%' }} + options={LABEL_TYPE_OPTIONS.map(([value, label]) => ({ + value, + label, + }))} + /> + <small className="text-muted"> + {t('What should be shown on the label?')} + </small> + </Col> + </Row> )} {/* Label Template */} {showTemplate && labelType === 'template' && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Label Template')}</label> - <Input.TextArea - value={values.label_template || ''} - onChange={e => onChange('label_template', e.target.value)} - placeholder="{name}: {value} ({percent}%)" - rows={3} - /> - <small className="text-muted"> - {t( - 'Format data labels. Use variables: {name}, {value}, {percent}. \\n represents a new line.', - )} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Label Template')}</label> + <Input.TextArea + value={values.label_template || ''} + onChange={e => onChange('label_template', e.target.value)} + placeholder="{name}: {value} ({percent}%)" + rows={3} + /> + <small className="text-muted"> + {t( + 'Format data labels. Use variables: {name}, {value}, {percent}. \\n represents a new line.', + )} + </small> + </Col> + </Row> )} {/* Label Threshold */} {showThreshold && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Label Threshold')}</label> - <InputNumber - value={values.show_labels_threshold ?? 5} - onChange={value => onChange('show_labels_threshold', value)} - min={0} - max={100} - step={0.5} - formatter={value => `${value}%`} - parser={value => Number((value as string).replace('%', ''))} - style={{ width: '100%' }} - /> - <small className="text-muted"> - {t('Minimum threshold in percentage points for showing labels')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Label Threshold')}</label> + <InputNumber + value={values.show_labels_threshold ?? 5} + onChange={value => onChange('show_labels_threshold', value)} + min={0} + max={100} + step={0.5} + formatter={value => `${value}%`} + parser={value => Number((value as string).replace('%', ''))} + style={{ width: '100%' }} + /> + <small className="text-muted"> + {t( + 'Minimum threshold in percentage points for showing labels', + )} + </small> + </Col> + </Row> )} {/* Labels Outside (Pie specific) */} {showOutside && chartType === 'pie' && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.labels_outside || false} - onChange={checked => onChange('labels_outside', checked)} - /> - {t('Put labels outside')} - </label> - <small className="text-muted"> - {t('Put the labels outside of the pie?')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label + style={{ display: 'flex', alignItems: 'center', gap: 8 }} + > + <Switch + checked={values.labels_outside || false} + onChange={checked => onChange('labels_outside', checked)} + /> + {t('Put labels outside')} + </label> + <small className="text-muted"> + {t('Put the labels outside of the pie?')} + </small> + </Col> + </Row> )} {/* Label Line (Pie specific) */} {showLabelLine && chartType === 'pie' && values.labels_outside && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.label_line || false} - onChange={checked => onChange('label_line', checked)} - /> - {t('Label Line')} - </label> - <small className="text-muted"> - {t('Draw a line from the label to the slice')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label + style={{ display: 'flex', alignItems: 'center', gap: 8 }} + > + <Switch + checked={values.label_line || false} + onChange={checked => onChange('label_line', checked)} + /> + {t('Label Line')} + </label> + <small className="text-muted"> + {t('Draw a line from the label to the slice')} + </small> + </Col> + </Row> )} {/* Label Rotation */} {showRotation && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Label Rotation')}</label> - <Select - value={values.label_rotation || '0'} - onChange={value => onChange('label_rotation', value)} - style={{ width: '100%' }} - options={LABEL_ROTATION_OPTIONS.map(([value, label]) => ({ - value, - label, - }))} - /> - <small className="text-muted"> - {t('Rotation angle of labels')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Label Rotation')}</label> + <Select + value={values.label_rotation || '0'} + onChange={value => onChange('label_rotation', value)} + style={{ width: '100%' }} + options={LABEL_ROTATION_OPTIONS.map(([value, label]) => ({ + value, + label, + }))} + /> + <small className="text-muted"> + {t('Rotation angle of labels')} + </small> + </Col> + </Row> )} {/* Show Upper Labels (Treemap specific) */} {showUpperLabels && chartType === 'treemap' && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.show_upper_labels || false} - onChange={checked => onChange('show_upper_labels', checked)} - /> - {t('Show Upper Labels')} - </label> - <small className="text-muted"> - {t('Show labels for parent nodes')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label + style={{ display: 'flex', alignItems: 'center', gap: 8 }} + > + <Switch + checked={values.show_upper_labels || false} + onChange={checked => onChange('show_upper_labels', checked)} + /> + {t('Show Upper Labels')} + </label> + <small className="text-muted"> + {t('Show labels for parent nodes')} + </small> + </Col> + </Row> )} </> )} diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/MarkerControlGroup.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/MarkerControlGroup.tsx index 8cb6cb015d..2f01d26b9d 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/MarkerControlGroup.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/MarkerControlGroup.tsx @@ -18,7 +18,8 @@ */ import { FC } from 'react'; import { t } from '@superset-ui/core'; -import { Switch, Slider, InputNumber, Row, Col } from 'antd'; +import { Switch, Slider, InputNumber } from 'antd'; +import { Row, Col } from '@superset-ui/core/components'; export interface MarkerControlGroupProps { enabledLabel?: string; @@ -67,57 +68,63 @@ const MarkerControlGroup: FC<MarkerControlGroupProps> = ({ return ( <div className="marker-control-group"> - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={markerEnabled} - onChange={handleEnabledChange} - disabled={disabled} - /> - {enabledLabel} - </label> - <small className="text-muted"> - {t('Draw markers on data points for better visibility')} - </small> - </div> - - {markerEnabled && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'block', marginBottom: 8 }}> - {sizeLabel} + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={markerEnabled} + onChange={handleEnabledChange} + disabled={disabled} + /> + {enabledLabel} </label> - <Row gutter={16} align="middle"> - <Col span={16}> - <Slider - min={minSize} - max={maxSize} - step={1} - value={markerSize} - onChange={handleSizeChange} - disabled={disabled || !markerEnabled} - marks={{ - [minSize]: minSize.toString(), - [Math.floor(maxSize / 2)]: Math.floor(maxSize / 2).toString(), - [maxSize]: maxSize.toString(), - }} - /> - </Col> - <Col span={8}> - <InputNumber - min={minSize} - max={maxSize} - step={1} - value={markerSize} - onChange={handleInputChange} - disabled={disabled || !markerEnabled} - style={{ width: '100%' }} - /> - </Col> - </Row> <small className="text-muted"> - {t('Size of the markers in pixels')} + {t('Draw markers on data points for better visibility')} </small> - </div> + </Col> + </Row> + + {markerEnabled && ( + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'block', marginBottom: 8 }}> + {sizeLabel} + </label> + <Row gutter={16} align="middle"> + <Col span={16}> + <Slider + min={minSize} + max={maxSize} + step={1} + value={markerSize} + onChange={handleSizeChange} + disabled={disabled || !markerEnabled} + marks={{ + [minSize]: minSize.toString(), + [Math.floor(maxSize / 2)]: Math.floor( + maxSize / 2, + ).toString(), + [maxSize]: maxSize.toString(), + }} + /> + </Col> + <Col span={8}> + <InputNumber + min={minSize} + max={maxSize} + step={1} + value={markerSize} + onChange={handleInputChange} + disabled={disabled || !markerEnabled} + style={{ width: '100%' }} + /> + </Col> + </Row> + <small className="text-muted"> + {t('Size of the markers in pixels')} + </small> + </Col> + </Row> )} </div> ); diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/OpacityControl.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/OpacityControl.tsx index 5eb505aabf..716576acf1 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/OpacityControl.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/OpacityControl.tsx @@ -18,7 +18,8 @@ */ import { FC } from 'react'; import { t } from '@superset-ui/core'; -import { Slider, InputNumber, Row, Col } from 'antd'; +import { Slider, InputNumber } from 'antd'; +import { Row, Col } from '@superset-ui/core/components'; export interface OpacityControlProps { name?: string; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/PieShapeControl.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/PieShapeControl.tsx index fbd5b899f8..0b02cda821 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/PieShapeControl.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/PieShapeControl.tsx @@ -18,7 +18,8 @@ */ import { FC } from 'react'; import { t } from '@superset-ui/core'; -import { Select, Switch, Slider, InputNumber, Row, Col } from 'antd'; +import { Select, Switch, Slider, InputNumber } from 'antd'; +import { Row, Col } from '@superset-ui/core/components'; export interface PieShapeControlProps { showDonut?: boolean; @@ -49,115 +50,123 @@ const PieShapeControl: FC<PieShapeControlProps> = ({ <div className="pie-shape-control"> {/* Donut Toggle */} {showDonut && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={isDonut} - onChange={checked => onChange('donut', checked)} - /> - {t('Donut')} - </label> - <small className="text-muted"> - {t('Do you want a donut or a pie?')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={isDonut} + onChange={checked => onChange('donut', checked)} + /> + {t('Donut')} + </label> + <small className="text-muted"> + {t('Do you want a donut or a pie?')} + </small> + </Col> + </Row> )} {/* Inner Radius (for Donut) */} {showRadius && isDonut && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Inner Radius')}</label> - <Row gutter={16} align="middle"> - <Col span={16}> - <Slider - min={0} - max={100} - step={1} - value={innerRadius} - onChange={value => onChange('innerRadius', value)} - marks={{ - 0: '0%', - 50: '50%', - 100: '100%', - }} - /> - </Col> - <Col span={8}> - <InputNumber - min={0} - max={100} - step={1} - value={innerRadius} - onChange={value => onChange('innerRadius', value)} - formatter={value => `${value}%`} - parser={value => Number((value as string).replace('%', ''))} - style={{ width: '100%' }} - /> - </Col> - </Row> - <small className="text-muted"> - {t('Inner radius of donut hole')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Inner Radius')}</label> + <Row gutter={16} align="middle"> + <Col span={16}> + <Slider + min={0} + max={100} + step={1} + value={innerRadius} + onChange={value => onChange('innerRadius', value)} + marks={{ + 0: '0%', + 50: '50%', + 100: '100%', + }} + /> + </Col> + <Col span={8}> + <InputNumber + min={0} + max={100} + step={1} + value={innerRadius} + onChange={value => onChange('innerRadius', value)} + formatter={value => `${value}%`} + parser={value => Number((value as string).replace('%', ''))} + style={{ width: '100%' }} + /> + </Col> + </Row> + <small className="text-muted"> + {t('Inner radius of donut hole')} + </small> + </Col> + </Row> )} {/* Outer Radius */} {showRadius && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Outer Radius')}</label> - <Row gutter={16} align="middle"> - <Col span={16}> - <Slider - min={0} - max={100} - step={1} - value={outerRadius} - onChange={value => onChange('outerRadius', value)} - marks={{ - 0: '0%', - 50: '50%', - 100: '100%', - }} - /> - </Col> - <Col span={8}> - <InputNumber - min={0} - max={100} - step={1} - value={outerRadius} - onChange={value => onChange('outerRadius', value)} - formatter={value => `${value}%`} - parser={value => Number((value as string).replace('%', ''))} - style={{ width: '100%' }} - /> - </Col> - </Row> - <small className="text-muted"> - {t('Outer edge of the pie/donut')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Outer Radius')}</label> + <Row gutter={16} align="middle"> + <Col span={16}> + <Slider + min={0} + max={100} + step={1} + value={outerRadius} + onChange={value => onChange('outerRadius', value)} + marks={{ + 0: '0%', + 50: '50%', + 100: '100%', + }} + /> + </Col> + <Col span={8}> + <InputNumber + min={0} + max={100} + step={1} + value={outerRadius} + onChange={value => onChange('outerRadius', value)} + formatter={value => `${value}%`} + parser={value => Number((value as string).replace('%', ''))} + style={{ width: '100%' }} + /> + </Col> + </Row> + <small className="text-muted"> + {t('Outer edge of the pie/donut')} + </small> + </Col> + </Row> )} {/* Rose Type (Nightingale Chart) */} {showRoseType && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Rose Type')}</label> - <Select - value={values.roseType || null} - onChange={value => onChange('roseType', value)} - style={{ width: '100%' }} - allowClear - placeholder={t('None')} - options={ROSE_TYPE_OPTIONS.map(([value, label]) => ({ - value, - label, - }))} - /> - <small className="text-muted"> - {t('Whether to show as Nightingale chart (polar area chart)')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Rose Type')}</label> + <Select + value={values.roseType || null} + onChange={value => onChange('roseType', value)} + style={{ width: '100%' }} + allowClear + placeholder={t('None')} + options={ROSE_TYPE_OPTIONS.map(([value, label]) => ({ + value, + label, + }))} + /> + <small className="text-muted"> + {t('Whether to show as Nightingale chart (polar area chart)')} + </small> + </Col> + </Row> )} </div> ); diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/TableControlsSection.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/TableControlsSection.tsx index 929d9b69c8..2e4e92faeb 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/TableControlsSection.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/TableControlsSection.tsx @@ -19,6 +19,7 @@ import { FC } from 'react'; import { t } from '@superset-ui/core'; import { Switch, Select, Input } from 'antd'; +import { Row, Col } from '@superset-ui/core/components'; import FormatControlGroup from './FormatControlGroup'; export interface TableControlsSectionProps { @@ -51,183 +52,205 @@ const TableControlsSection: FC<TableControlsSectionProps> = ({ {/* Pagination Controls */} {showPagination && !isPivot && ( <> - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.server_pagination || false} - onChange={checked => onChange('server_pagination', checked)} - /> - {t('Server Pagination')} - </label> - <small className="text-muted"> - {t('Enable server-side pagination for large datasets')} - </small> - </div> - {values.server_pagination && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Page Length')}</label> - <Select - value={values.server_page_length || 10} - onChange={value => onChange('server_page_length', value)} - style={{ width: '100%' }} - options={[ - { value: 10, label: '10' }, - { value: 25, label: '25' }, - { value: 50, label: '50' }, - { value: 100, label: '100' }, - { value: 200, label: '200' }, - ]} - /> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.server_pagination || false} + onChange={checked => onChange('server_pagination', checked)} + /> + {t('Server Pagination')} + </label> <small className="text-muted"> - {t('Number of rows per page')} + {t('Enable server-side pagination for large datasets')} </small> - </div> + </Col> + </Row> + {values.server_pagination && ( + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Page Length')}</label> + <Select + value={values.server_page_length || 10} + onChange={value => onChange('server_page_length', value)} + style={{ width: '100%' }} + options={[ + { value: 10, label: '10' }, + { value: 25, label: '25' }, + { value: 50, label: '50' }, + { value: 100, label: '100' }, + { value: 200, label: '200' }, + ]} + /> + <small className="text-muted"> + {t('Number of rows per page')} + </small> + </Col> + </Row> )} </> )} {/* Cell Bars */} {showCellBars && !isPivot && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.show_cell_bars || false} - onChange={checked => onChange('show_cell_bars', checked)} - /> - {t('Show Cell Bars')} - </label> - <small className="text-muted"> - {t('Display mini bar charts in numeric columns')} - </small> - </div> - )} - - {/* Totals */} - {showTotals && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.show_totals || values.rowTotals || false} - onChange={checked => { - if (isPivot) { - onChange('rowTotals', checked); - onChange('colTotals', checked); - } else { - onChange('show_totals', checked); - } - }} - /> - {t('Show Totals')} - </label> - <small className="text-muted"> - {isPivot - ? t('Show row and column totals') - : t('Show total row at bottom')} - </small> - </div> - )} - - {/* Subtotals for Pivot */} - {isPivot && ( - <> - <div className="control-row" style={{ marginBottom: 16 }}> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <Switch - checked={values.rowSubTotals || false} - onChange={checked => onChange('rowSubTotals', checked)} + checked={values.show_cell_bars || false} + onChange={checked => onChange('show_cell_bars', checked)} /> - {t('Show Row Subtotals')} + {t('Show Cell Bars')} </label> <small className="text-muted"> - {t('Show subtotals for row groups')} + {t('Display mini bar charts in numeric columns')} </small> - </div> - {values.rowSubTotals && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Subtotal Position')}</label> - <Select - value={values.rowSubtotalPosition || 'bottom'} - onChange={value => onChange('rowSubtotalPosition', value)} - style={{ width: '100%' }} - options={[ - { value: 'top', label: t('Top') }, - { value: 'bottom', label: t('Bottom') }, - ]} - /> - </div> - )} - <div className="control-row" style={{ marginBottom: 16 }}> + </Col> + </Row> + )} + + {/* Totals */} + {showTotals && ( + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <Switch - checked={values.colSubTotals || false} - onChange={checked => onChange('colSubTotals', checked)} + checked={values.show_totals || values.rowTotals || false} + onChange={checked => { + if (isPivot) { + onChange('rowTotals', checked); + onChange('colTotals', checked); + } else { + onChange('show_totals', checked); + } + }} /> - {t('Show Column Subtotals')} + {t('Show Totals')} </label> <small className="text-muted"> - {t('Show subtotals for column groups')} + {isPivot + ? t('Show row and column totals') + : t('Show total row at bottom')} </small> - </div> - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.transposePivot || false} - onChange={checked => onChange('transposePivot', checked)} - /> - {t('Transpose Pivot')} - </label> - <small className="text-muted">{t('Swap rows and columns')}</small> - </div> + </Col> + </Row> + )} + + {/* Subtotals for Pivot */} + {isPivot && ( + <> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.rowSubTotals || false} + onChange={checked => onChange('rowSubTotals', checked)} + /> + {t('Show Row Subtotals')} + </label> + <small className="text-muted"> + {t('Show subtotals for row groups')} + </small> + </Col> + </Row> + {values.rowSubTotals && ( + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Subtotal Position')}</label> + <Select + value={values.rowSubtotalPosition || 'bottom'} + onChange={value => onChange('rowSubtotalPosition', value)} + style={{ width: '100%' }} + options={[ + { value: 'top', label: t('Top') }, + { value: 'bottom', label: t('Bottom') }, + ]} + /> + </Col> + </Row> + )} + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.colSubTotals || false} + onChange={checked => onChange('colSubTotals', checked)} + /> + {t('Show Column Subtotals')} + </label> + <small className="text-muted"> + {t('Show subtotals for column groups')} + </small> + </Col> + </Row> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.transposePivot || false} + onChange={checked => onChange('transposePivot', checked)} + /> + {t('Transpose Pivot')} + </label> + <small className="text-muted">{t('Swap rows and columns')}</small> + </Col> + </Row> </> )} {/* Conditional Formatting */} {showConditionalFormatting && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Conditional Formatting')}</label> - <Input.TextArea - value={values.conditional_formatting || ''} - onChange={e => onChange('conditional_formatting', e.target.value)} - placeholder={t('Enter conditional formatting rules as JSON')} - rows={4} - /> - <small className="text-muted"> - {t('Apply conditional color formatting to cells')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Conditional Formatting')}</label> + <Input.TextArea + value={values.conditional_formatting || ''} + onChange={e => onChange('conditional_formatting', e.target.value)} + placeholder={t('Enter conditional formatting rules as JSON')} + rows={4} + /> + <small className="text-muted"> + {t('Apply conditional color formatting to cells')} + </small> + </Col> + </Row> )} {/* Timestamp Format */} {showTimestampFormat && !isPivot && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label>{t('Timestamp Format')}</label> - <Input - value={values.table_timestamp_format || ''} - onChange={e => onChange('table_timestamp_format', e.target.value)} - placeholder="%Y-%m-%d %H:%M:%S" - /> - <small className="text-muted"> - {t('D3 time format for timestamp columns')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label>{t('Timestamp Format')}</label> + <Input + value={values.table_timestamp_format || ''} + onChange={e => onChange('table_timestamp_format', e.target.value)} + placeholder="%Y-%m-%d %H:%M:%S" + /> + <small className="text-muted"> + {t('D3 time format for timestamp columns')} + </small> + </Col> + </Row> )} {/* Allow HTML */} {showAllowHtml && ( - <div className="control-row" style={{ marginBottom: 16 }}> - <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> - <Switch - checked={values.allow_render_html || false} - onChange={checked => onChange('allow_render_html', checked)} - /> - {t('Allow HTML')} - </label> - <small className="text-muted"> - {t( - 'Render HTML content in cells (security warning: only enable for trusted data)', - )} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 16 }}> + <Col span={24}> + <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}> + <Switch + checked={values.allow_render_html || false} + onChange={checked => onChange('allow_render_html', checked)} + /> + {t('Allow HTML')} + </label> + <small className="text-muted"> + {t( + 'Render HTML content in cells (security warning: only enable for trusted data)', + )} + </small> + </Col> + </Row> )} {/* Format Controls */} diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/TimeseriesControlPanel.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/TimeseriesControlPanel.tsx index ca24826696..abac7f5b03 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/TimeseriesControlPanel.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/TimeseriesControlPanel.tsx @@ -19,6 +19,7 @@ import { FC } from 'react'; import { t } from '@superset-ui/core'; import { Select, Radio } from 'antd'; +import { Row, Col } from '@superset-ui/core/components'; import AxisControlSection from './AxisControlSection'; import FormatControlGroup from './FormatControlGroup'; import OpacityControl from './OpacityControl'; @@ -84,69 +85,75 @@ const TimeseriesControlPanel: FC<TimeseriesControlPanelProps> = ({ <div className="timeseries-control-panel"> {/* Series Type Selection */} {showSeriesType && SERIES_TYPE_OPTIONS[variant] && ( - <div className="control-row" style={{ marginBottom: 24 }}> - <label>{t('Series Style')}</label> - <Select - value={ - values.seriesType || - (SERIES_TYPE_OPTIONS[variant][0] - ? SERIES_TYPE_OPTIONS[variant][0][0] - : 'line') - } - onChange={value => onChange('seriesType', value)} - style={{ width: '100%' }} - options={SERIES_TYPE_OPTIONS[variant].map( - ([value, label]: [string, string]) => ({ - value, - label, - }), - )} - /> - <small className="text-muted"> - {t('Series chart type (line, smooth, step, etc)')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 24 }}> + <Col span={24}> + <label>{t('Series Style')}</label> + <Select + value={ + values.seriesType || + (SERIES_TYPE_OPTIONS[variant][0] + ? SERIES_TYPE_OPTIONS[variant][0][0] + : 'line') + } + onChange={value => onChange('seriesType', value)} + style={{ width: '100%' }} + options={SERIES_TYPE_OPTIONS[variant].map( + ([value, label]: [string, string]) => ({ + value, + label, + }), + )} + /> + <small className="text-muted"> + {t('Series chart type (line, smooth, step, etc)')} + </small> + </Col> + </Row> )} {/* Stack Options */} {showStack && ( - <div className="control-row" style={{ marginBottom: 24 }}> - <label>{t('Stacking')}</label> - <Select - value={values.stack || null} - onChange={value => onChange('stack', value)} - style={{ width: '100%' }} - allowClear - placeholder={t('No stacking')} - options={STACK_OPTIONS.map(([value, label]) => ({ - value, - label, - }))} - /> - <small className="text-muted"> - {t('Stack series on top of each other')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 24 }}> + <Col span={24}> + <label>{t('Stacking')}</label> + <Select + value={values.stack || null} + onChange={value => onChange('stack', value)} + style={{ width: '100%' }} + allowClear + placeholder={t('No stacking')} + options={STACK_OPTIONS.map(([value, label]) => ({ + value, + label, + }))} + /> + <small className="text-muted"> + {t('Stack series on top of each other')} + </small> + </Col> + </Row> )} {/* Bar Orientation */} {showOrientation && hasBarOptions && ( - <div className="control-row" style={{ marginBottom: 24 }}> - <label>{t('Bar Orientation')}</label> - <Radio.Group - value={values.orientation || 'vertical'} - onChange={e => onChange('orientation', e.target.value)} - > - <Radio value="vertical">{t('Vertical')}</Radio> - <Radio value="horizontal">{t('Horizontal')}</Radio> - </Radio.Group> - <small - className="text-muted" - style={{ display: 'block', marginTop: 8 }} - > - {t('Orientation of bar chart')} - </small> - </div> + <Row gutter={[16, 8]} style={{ marginBottom: 24 }}> + <Col span={24}> + <label>{t('Bar Orientation')}</label> + <Radio.Group + value={values.orientation || 'vertical'} + onChange={e => onChange('orientation', e.target.value)} + > + <Radio value="vertical">{t('Vertical')}</Radio> + <Radio value="horizontal">{t('Horizontal')}</Radio> + </Radio.Group> + <small + className="text-muted" + style={{ display: 'block', marginTop: 8 }} + > + {t('Orientation of bar chart')} + </small> + </Col> + </Row> )} {/* Area Chart Options */} diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/index.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/index.tsx index 9cc4c6b126..b64952ae4d 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/index.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/index.tsx @@ -51,4 +51,7 @@ export * from './SharedControlComponents'; // Export React control panel export { ReactControlPanel } from './ReactControlPanel'; +// Export control panel layout components +export * from './ControlPanelLayout'; + // Inline control functions are exported from SharedControlComponents diff --git a/superset-frontend/src/explore/components/ControlRow.tsx b/superset-frontend/src/explore/components/ControlRow.tsx index d9f669c8dd..cfdbeabf16 100644 --- a/superset-frontend/src/explore/components/ControlRow.tsx +++ b/superset-frontend/src/explore/components/ControlRow.tsx @@ -17,8 +17,7 @@ * under the License. */ import { useCallback, ReactElement } from 'react'; - -const NUM_COLUMNS = 12; +import { Row, Col } from '@superset-ui/core/components'; type Control = ReactElement | null; @@ -30,28 +29,31 @@ export default function ControlRow({ controls }: { controls: Control[] }) { : control?.props; return props?.type === 'HiddenControl' || props?.isVisible === false; }, []); - // Invisible control should not be counted - // in the columns number - const countableControls = controls.filter( - control => !isHiddenControl(control), - ); - const colSize = countableControls.length - ? NUM_COLUMNS / countableControls.length - : NUM_COLUMNS; + + // Filter out hidden controls for column calculation + const visibleControls = controls.filter(control => !isHiddenControl(control)); + + // Calculate column span based on number of visible controls + const colSpan = + visibleControls.length > 0 ? Math.floor(24 / visibleControls.length) : 24; + return ( - <div className="row"> - {controls.map((control, i) => ( - <div - data-test="control-item" - className={`col-lg-${colSize} col-xs-12`} - style={{ - display: isHiddenControl(control) ? 'none' : 'block', - }} - key={i} - > - {control} - </div> - ))} - </div> + <Row gutter={[16, 16]} style={{ marginBottom: 16 }}> + {controls.map((control, i) => { + if (isHiddenControl(control)) { + // Hidden controls are rendered but not displayed + return ( + <div key={i} style={{ display: 'none' }}> + {control} + </div> + ); + } + return ( + <Col key={i} span={colSpan} data-test="control-item"> + {control} + </Col> + ); + })} + </Row> ); }
