mirror of
https://github.com/apache/superset.git
synced 2026-07-18 20:55:47 +00:00
feat(theming): land Ant Design v5 overhaul — dynamic themes, real dark mode + massive styling refactor (#31590)
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com> Co-authored-by: Diego Pucci <diegopucci.me@gmail.com> Co-authored-by: Mehmet Salih Yavuz <salih.yavuz@proton.me> Co-authored-by: Geido <60598000+geido@users.noreply.github.com> Co-authored-by: Alexandru Soare <37236580+alexandrusoare@users.noreply.github.com> Co-authored-by: Damian Pendrak <dpendrak@gmail.com> Co-authored-by: Pius Iniobong <67148161+payose@users.noreply.github.com> Co-authored-by: Enzo Martellucci <enzomartellucci@gmail.com> Co-authored-by: Kamil Gabryjelski <kamil.gabryjelski@gmail.com>
This commit is contained in:
committed by
GitHub
parent
2cc1ef88c8
commit
dd129fa403
@@ -36,14 +36,17 @@ import {
|
||||
DatasourceType,
|
||||
} from '@superset-ui/core';
|
||||
import { ColumnMeta, isSavedExpression } from '@superset-ui/chart-controls';
|
||||
import Tabs from 'src/components/Tabs';
|
||||
import Button from 'src/components/Button';
|
||||
import { Select } from 'src/components';
|
||||
import Tabs from '@superset-ui/core/components/Tabs';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
FormItem,
|
||||
Select,
|
||||
SQLEditor,
|
||||
EmptyState,
|
||||
} from '@superset-ui/core/components';
|
||||
|
||||
import { Form, FormItem } from 'src/components/Form';
|
||||
import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
|
||||
import { SQLEditor } from 'src/components/AsyncAceEditor';
|
||||
import { EmptyState } from 'src/components/EmptyState';
|
||||
import { getColumnKeywords } from 'src/explore/controlUtils/getColumnKeywords';
|
||||
import { StyledColumnOption } from 'src/explore/components/optionRenderers';
|
||||
import {
|
||||
@@ -53,10 +56,16 @@ import {
|
||||
import { ExplorePageState } from 'src/explore/types';
|
||||
import useResizeButton from './useResizeButton';
|
||||
|
||||
const TABS_KEYS = {
|
||||
SAVED: 'saved',
|
||||
SIMPLE: 'simple',
|
||||
SQL_EXPRESSION: 'sqlExpression',
|
||||
};
|
||||
|
||||
const StyledSelect = styled(Select)`
|
||||
.metric-option {
|
||||
& > svg {
|
||||
min-width: ${({ theme }) => `${theme.gridUnit * 4}px`};
|
||||
min-width: ${({ theme }) => `${theme.sizeUnit * 4}px`};
|
||||
}
|
||||
& > .option-label {
|
||||
overflow: hidden;
|
||||
@@ -306,164 +315,175 @@ const ColumnSelectPopover = ({
|
||||
height: ${height}px;
|
||||
width: ${width}px;
|
||||
`}
|
||||
>
|
||||
<Tabs.TabPane
|
||||
key="saved"
|
||||
tab={t('Saved')}
|
||||
disabled={disabledTabs.has('saved')}
|
||||
>
|
||||
{calculatedColumns.length > 0 ? (
|
||||
<FormItem label={savedExpressionsLabel}>
|
||||
<StyledSelect
|
||||
ariaLabel={savedExpressionsLabel}
|
||||
value={selectedCalculatedColumn?.column_name}
|
||||
onChange={onCalculatedColumnChange}
|
||||
allowClear
|
||||
autoFocus={!selectedCalculatedColumn}
|
||||
placeholder={t('%s column(s)', calculatedColumns.length)}
|
||||
options={calculatedColumns.map(calculatedColumn => ({
|
||||
value: calculatedColumn.column_name,
|
||||
label:
|
||||
calculatedColumn.verbose_name ||
|
||||
calculatedColumn.column_name,
|
||||
customLabel: (
|
||||
<StyledColumnOption column={calculatedColumn} showType />
|
||||
),
|
||||
key: calculatedColumn.column_name,
|
||||
}))}
|
||||
/>
|
||||
</FormItem>
|
||||
) : datasourceType === DatasourceType.Table ? (
|
||||
<EmptyState
|
||||
image="empty.svg"
|
||||
size="small"
|
||||
title={
|
||||
isTemporal
|
||||
? t('No temporal columns found')
|
||||
: t('No saved expressions found')
|
||||
}
|
||||
description={
|
||||
isTemporal
|
||||
? t(
|
||||
'Add calculated temporal columns to dataset in "Edit datasource" modal',
|
||||
)
|
||||
: t(
|
||||
'Add calculated columns to dataset in "Edit datasource" modal',
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
image="empty.svg"
|
||||
size="small"
|
||||
title={
|
||||
isTemporal
|
||||
? t('No temporal columns found')
|
||||
: t('No saved expressions found')
|
||||
}
|
||||
description={
|
||||
isTemporal ? (
|
||||
<>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={setDatasetAndClose}
|
||||
>
|
||||
{t('Create a dataset')}
|
||||
</span>{' '}
|
||||
{t(' to mark a column as a time column')}
|
||||
</>
|
||||
items={[
|
||||
{
|
||||
key: TABS_KEYS.SAVED,
|
||||
label: t('Saved'),
|
||||
disabled: disabledTabs.has('saved'),
|
||||
children: (
|
||||
<>
|
||||
{calculatedColumns.length > 0 ? (
|
||||
<FormItem label={savedExpressionsLabel}>
|
||||
<StyledSelect
|
||||
ariaLabel={savedExpressionsLabel}
|
||||
value={selectedCalculatedColumn?.column_name}
|
||||
onChange={onCalculatedColumnChange}
|
||||
allowClear
|
||||
autoFocus={!selectedCalculatedColumn}
|
||||
placeholder={t('%s column(s)', calculatedColumns.length)}
|
||||
options={calculatedColumns.map(calculatedColumn => ({
|
||||
value: calculatedColumn.column_name,
|
||||
label: (
|
||||
<StyledColumnOption
|
||||
column={calculatedColumn}
|
||||
showType
|
||||
/>
|
||||
),
|
||||
key: calculatedColumn.column_name,
|
||||
}))}
|
||||
/>
|
||||
</FormItem>
|
||||
) : datasourceType === DatasourceType.Table ? (
|
||||
<EmptyState
|
||||
image="empty.svg"
|
||||
size="small"
|
||||
title={
|
||||
isTemporal
|
||||
? t('No temporal columns found')
|
||||
: t('No saved expressions found')
|
||||
}
|
||||
description={
|
||||
isTemporal
|
||||
? t(
|
||||
'Add calculated temporal columns to dataset in "Edit datasource" modal',
|
||||
)
|
||||
: t(
|
||||
'Add calculated columns to dataset in "Edit datasource" modal',
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={setDatasetAndClose}
|
||||
>
|
||||
{t('Create a dataset')}
|
||||
</span>{' '}
|
||||
{t(' to add calculated columns')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane
|
||||
key="simple"
|
||||
tab={t('Simple')}
|
||||
disabled={disabledTabs.has('simple')}
|
||||
>
|
||||
{isTemporal && simpleColumns.length === 0 ? (
|
||||
<EmptyState
|
||||
image="empty.svg"
|
||||
size="small"
|
||||
title={t('No temporal columns found')}
|
||||
description={
|
||||
datasourceType === DatasourceType.Table ? (
|
||||
t('Mark a column as temporal in "Edit datasource" modal')
|
||||
<EmptyState
|
||||
image="empty.svg"
|
||||
size="small"
|
||||
title={
|
||||
isTemporal
|
||||
? t('No temporal columns found')
|
||||
: t('No saved expressions found')
|
||||
}
|
||||
description={
|
||||
isTemporal ? (
|
||||
<>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={setDatasetAndClose}
|
||||
>
|
||||
{t('Create a dataset')}
|
||||
</span>{' '}
|
||||
{t(' to mark a column as a time column')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={setDatasetAndClose}
|
||||
>
|
||||
{t('Create a dataset')}
|
||||
</span>{' '}
|
||||
{t(' to add calculated columns')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: TABS_KEYS.SIMPLE,
|
||||
label: t('Simple'),
|
||||
disabled: disabledTabs.has('simple'),
|
||||
children: (
|
||||
<>
|
||||
{isTemporal && simpleColumns.length === 0 ? (
|
||||
<EmptyState
|
||||
image="empty.svg"
|
||||
size="small"
|
||||
title={t('No temporal columns found')}
|
||||
description={
|
||||
datasourceType === DatasourceType.Table ? (
|
||||
t(
|
||||
'Mark a column as temporal in "Edit datasource" modal',
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={setDatasetAndClose}
|
||||
>
|
||||
{t('Create a dataset')}
|
||||
</span>{' '}
|
||||
{t(' to mark a column as a time column')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={setDatasetAndClose}
|
||||
>
|
||||
{t('Create a dataset')}
|
||||
</span>{' '}
|
||||
{t(' to mark a column as a time column')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<FormItem label={simpleColumnsLabel}>
|
||||
<Select
|
||||
ariaLabel={simpleColumnsLabel}
|
||||
value={selectedSimpleColumn?.column_name}
|
||||
onChange={onSimpleColumnChange}
|
||||
allowClear
|
||||
autoFocus={!selectedSimpleColumn}
|
||||
placeholder={t('%s column(s)', simpleColumns.length)}
|
||||
options={simpleColumns.map(simpleColumn => ({
|
||||
value: simpleColumn.column_name,
|
||||
label: simpleColumn.verbose_name || simpleColumn.column_name,
|
||||
customLabel: (
|
||||
<StyledColumnOption column={simpleColumn} showType />
|
||||
),
|
||||
key: simpleColumn.column_name,
|
||||
}))}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
</Tabs.TabPane>
|
||||
<FormItem label={simpleColumnsLabel}>
|
||||
<Select
|
||||
ariaLabel={simpleColumnsLabel}
|
||||
value={selectedSimpleColumn?.column_name}
|
||||
onChange={onSimpleColumnChange}
|
||||
allowClear
|
||||
autoFocus={!selectedSimpleColumn}
|
||||
placeholder={t('%s column(s)', simpleColumns.length)}
|
||||
options={simpleColumns.map(simpleColumn => ({
|
||||
value: simpleColumn.column_name,
|
||||
label: (
|
||||
<StyledColumnOption column={simpleColumn} showType />
|
||||
),
|
||||
key: simpleColumn.column_name,
|
||||
}))}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: TABS_KEYS.SQL_EXPRESSION,
|
||||
label: t('Custom SQL'),
|
||||
disabled: disabledTabs.has('sqlExpression'),
|
||||
children: (
|
||||
<>
|
||||
<SQLEditor
|
||||
value={
|
||||
adhocColumn?.sqlExpression ||
|
||||
selectedSimpleColumn?.column_name ||
|
||||
selectedCalculatedColumn?.expression
|
||||
}
|
||||
onFocus={onSqlEditorFocus}
|
||||
showLoadingForImport
|
||||
onChange={onSqlExpressionChange}
|
||||
width="100%"
|
||||
height={`${height - 80}px`}
|
||||
showGutter={false}
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
enableLiveAutocompletion
|
||||
className="filter-sql-editor"
|
||||
wrapEnabled
|
||||
ref={sqlEditorRef}
|
||||
keywords={keywords}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Tabs.TabPane
|
||||
key="sqlExpression"
|
||||
tab={t('Custom SQL')}
|
||||
disabled={disabledTabs.has('sqlExpression')}
|
||||
>
|
||||
<SQLEditor
|
||||
value={
|
||||
adhocColumn?.sqlExpression ||
|
||||
selectedSimpleColumn?.column_name ||
|
||||
selectedCalculatedColumn?.expression
|
||||
}
|
||||
onFocus={onSqlEditorFocus}
|
||||
showLoadingForImport
|
||||
onChange={onSqlExpressionChange}
|
||||
width="100%"
|
||||
height={`${height - 80}px`}
|
||||
showGutter={false}
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
enableLiveAutocompletion
|
||||
className="filter-sql-editor"
|
||||
wrapEnabled
|
||||
ref={sqlEditorRef}
|
||||
keywords={keywords}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<div>
|
||||
<Button buttonSize="small" onClick={onResetStateAndClose} cta>
|
||||
{t('Close')}
|
||||
|
||||
@@ -18,14 +18,14 @@
|
||||
*/
|
||||
import { useCallback, useState } from 'react';
|
||||
import { t, styled, useTheme } from '@superset-ui/core';
|
||||
import { Input } from 'src/components/Input';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import { Icons } from 'src/components/Icons';
|
||||
import { Input, Tooltip } from '@superset-ui/core/components';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
|
||||
const StyledInput = styled(Input)`
|
||||
border-radius: ${({ theme }) => theme.borderRadius};
|
||||
height: 26px;
|
||||
padding-left: ${({ theme }) => theme.gridUnit * 2.5}px;
|
||||
padding-left: ${({ theme }) => theme.sizeUnit * 2.5}px;
|
||||
border-color: ${({ theme }) => theme.colorSplit};
|
||||
`;
|
||||
|
||||
export const DndColumnSelectPopoverTitle = ({
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
QueryFormData,
|
||||
} from '@superset-ui/core';
|
||||
import { ColumnMeta } from '@superset-ui/chart-controls';
|
||||
import { TimeseriesDefaultFormData } from '@superset-ui/plugin-chart-echarts';
|
||||
|
||||
import {
|
||||
fireEvent,
|
||||
@@ -33,7 +32,7 @@ import {
|
||||
screen,
|
||||
within,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import type { AsyncAceEditorProps } from 'src/components/AsyncAceEditor';
|
||||
import type { AsyncAceEditorProps } from '@superset-ui/core/components';
|
||||
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
|
||||
import AdhocFilter from 'src/explore/components/controls/FilterControl/AdhocFilter';
|
||||
import { Operators } from 'src/explore/constants';
|
||||
@@ -47,8 +46,8 @@ import { Datasource } from '../../../types';
|
||||
import { DndItemType } from '../../DndItemType';
|
||||
import DatasourcePanelDragOption from '../../DatasourcePanel/DatasourcePanelDragOption';
|
||||
|
||||
jest.mock('src/components/AsyncAceEditor', () => ({
|
||||
...jest.requireActual('src/components/AsyncAceEditor'),
|
||||
jest.mock('@superset-ui/core/components/AsyncAceEditor', () => ({
|
||||
...jest.requireActual('@superset-ui/core/components/AsyncAceEditor'),
|
||||
SQLEditor: (props: AsyncAceEditorProps) => (
|
||||
<div data-test="react-ace">{props.value}</div>
|
||||
),
|
||||
@@ -127,7 +126,6 @@ test('renders options with saved metric', async () => {
|
||||
setup({
|
||||
formData: {
|
||||
...baseFormData,
|
||||
...TimeseriesDefaultFormData,
|
||||
metrics: ['saved_metric'],
|
||||
},
|
||||
}),
|
||||
@@ -172,7 +170,6 @@ test('renders options with adhoc metric', async () => {
|
||||
setup({
|
||||
formData: {
|
||||
...baseFormData,
|
||||
...TimeseriesDefaultFormData,
|
||||
metrics: [adhocMetric],
|
||||
},
|
||||
}),
|
||||
@@ -212,7 +209,6 @@ test('cannot drop a column that is not part of the simple column selection', ()
|
||||
{setup({
|
||||
formData: {
|
||||
...baseFormData,
|
||||
...TimeseriesDefaultFormData,
|
||||
metrics: [adhocMetric],
|
||||
},
|
||||
columns: [{ column_name: 'order_date' }],
|
||||
@@ -342,7 +338,6 @@ describe('when disallow_adhoc_metrics is set', () => {
|
||||
{setup({
|
||||
formData: {
|
||||
...baseFormData,
|
||||
...TimeseriesDefaultFormData,
|
||||
metrics: [adhocMetric],
|
||||
},
|
||||
datasource: {
|
||||
@@ -391,7 +386,6 @@ describe('when disallow_adhoc_metrics is set', () => {
|
||||
{setup({
|
||||
formData: {
|
||||
...baseFormData,
|
||||
...TimeseriesDefaultFormData,
|
||||
metrics: [adhocMetric],
|
||||
},
|
||||
datasource: {
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
isColumnMeta,
|
||||
isTemporalColumn,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import Modal from 'src/components/Modal';
|
||||
import { Modal } from '@superset-ui/core/components';
|
||||
import {
|
||||
OPERATOR_ENUM_TO_OPERATOR_TYPE,
|
||||
Operators,
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
import { ReactNode, useCallback, useContext, useEffect, useMemo } from 'react';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import { t, useTheme } from '@superset-ui/core';
|
||||
import { t } from '@superset-ui/core';
|
||||
import ControlHeader from 'src/explore/components/ControlHeader';
|
||||
import {
|
||||
AddControlLabel,
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
DatasourcePanelDndItem,
|
||||
DndItemValue,
|
||||
} from 'src/explore/components/DatasourcePanel/types';
|
||||
import { Icons } from 'src/components/Icons';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { DndItemType } from '../../DndItemType';
|
||||
import { DraggingContext, DropzoneContext } from '../../ExploreContainer';
|
||||
|
||||
@@ -54,7 +54,6 @@ export default function DndSelectLabel({
|
||||
isLoading,
|
||||
...props
|
||||
}: DndSelectLabelProps) {
|
||||
const theme = useTheme();
|
||||
const canDropProp = props.canDrop;
|
||||
const canDropValueProp = props.canDropValue;
|
||||
|
||||
@@ -100,13 +99,7 @@ export default function DndSelectLabel({
|
||||
cancelHover={!props.onClickGhostButton}
|
||||
onClick={props.onClickGhostButton}
|
||||
>
|
||||
<Icons.PlusOutlined
|
||||
iconSize="m"
|
||||
iconColor={theme.colors.grayscale.light1}
|
||||
css={theme => ({
|
||||
margin: `auto ${theme.gridUnit}px auto 0`,
|
||||
})}
|
||||
/>
|
||||
<Icons.PlusOutlined iconSize="m" />
|
||||
{t(props.ghostButtonText)}
|
||||
</AddControlLabel>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
import { useCallback } from 'react';
|
||||
import { css, styled, t, useTheme } from '@superset-ui/core';
|
||||
import { Icons } from 'src/components/Icons';
|
||||
import { Icons, InfoTooltip } from '@superset-ui/core/components';
|
||||
import {
|
||||
CaretContainer,
|
||||
CloseContainer,
|
||||
@@ -26,10 +26,9 @@ import {
|
||||
Label,
|
||||
} from 'src/explore/components/controls/OptionControls';
|
||||
import { OptionProps } from 'src/explore/components/controls/DndColumnSelectControl/types';
|
||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||
|
||||
const StyledInfoTooltipWithTrigger = styled(InfoTooltipWithTrigger)`
|
||||
margin: 0 ${({ theme }) => theme.gridUnit}px;
|
||||
const StyledInfoTooltip = styled(InfoTooltip)`
|
||||
margin: 0 ${({ theme }) => theme.sizeUnit}px;
|
||||
`;
|
||||
|
||||
export default function Option({
|
||||
@@ -71,10 +70,9 @@ export default function Option({
|
||||
)}
|
||||
<Label data-test="control-label">{children}</Label>
|
||||
{(!!datasourceWarningMessage || isExtra) && (
|
||||
<StyledInfoTooltipWithTrigger
|
||||
icon="exclamation-triangle"
|
||||
<StyledInfoTooltip
|
||||
type="warning"
|
||||
placement="top"
|
||||
bsStyle="warning"
|
||||
tooltip={
|
||||
datasourceWarningMessage ||
|
||||
t(`
|
||||
@@ -89,7 +87,7 @@ export default function Option({
|
||||
<Icons.RightOutlined
|
||||
iconSize="m"
|
||||
css={css`
|
||||
margin-top: ${theme.gridUnit}px;
|
||||
margin: ${theme.sizeUnit}px;
|
||||
`}
|
||||
iconColor={theme.colors.grayscale.light1}
|
||||
/>
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
OptionProps,
|
||||
OptionItemInterface,
|
||||
} from 'src/explore/components/controls/DndColumnSelectControl/types';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import { Tooltip } from '@superset-ui/core/components';
|
||||
import { StyledColumnOption } from 'src/explore/components/optionRenderers';
|
||||
import { styled, isAdhocColumn } from '@superset-ui/core';
|
||||
import { ColumnMeta } from '@superset-ui/chart-controls';
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
POPOVER_INITIAL_HEIGHT,
|
||||
POPOVER_INITIAL_WIDTH,
|
||||
} from 'src/explore/constants';
|
||||
import { Icons } from '@superset-ui/core/components';
|
||||
|
||||
const RESIZE_THROTTLE_MS = 50;
|
||||
|
||||
@@ -132,14 +133,12 @@ export default function useResizeButton(
|
||||
}, [onMouseUp]);
|
||||
|
||||
return [
|
||||
// TODO: Remove fa-icon
|
||||
// eslint-disable-next-line icons/no-fa-icons-usage
|
||||
<i
|
||||
<Icons.ArrowsAltOutlined
|
||||
role="button"
|
||||
aria-label="Resize"
|
||||
tabIndex={0}
|
||||
onMouseDown={onDragDown}
|
||||
className="fa fa-expand edit-popover-resize text-muted"
|
||||
className="edit-popover-resize"
|
||||
/>,
|
||||
width,
|
||||
height,
|
||||
|
||||
Reference in New Issue
Block a user