mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-20 06:34:55 +00:00
semantic-fe visual modeling pr (#21)
* [improvement][semantic-fe] Added an editing component to set filtering rules for Q&A. Now, the SQL editor will be accompanied by a list for display and control, to resolve ambiguity when using comma-separated values. [improvement][semantic-fe] Improved validation logic and prompt copywriting for data source/dimension/metric editing and creation. [improvement][semantic-fe] Improved user experience for visual modeling. Now, when using the legend to control the display/hide of data sources and their associated metric dimensions, the canvas will be re-layout based on the activated data source in the legend. * [improvement][semantic-fe] Submitted a new version of the visual modeling tool. [improvement][semantic-fe] Fixed an issue with the initialization of YoY and MoM metrics in Q&A settings. [improvement][semantic-fe] Added a version field to the database settings. [improvement][semantic-fe] 1. Added the ability to set YoY and MoM metrics in Q&A settings.2. Moved dimension value editing from the dimension editing window to the dimension list. --------- Co-authored-by: tristanliu <tristanliu@tencent.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
.standardFormRow {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
// padding-bottom: 16px;
|
||||
// border-bottom: 1px dashed @border-color-split;
|
||||
:global {
|
||||
.ant-form-item,
|
||||
.ant-legacy-form-item {
|
||||
margin-right: 24px;
|
||||
}
|
||||
.ant-form-item-label,
|
||||
.ant-legacy-form-item-label {
|
||||
label {
|
||||
margin-right: 0;
|
||||
color: @text-color;
|
||||
}
|
||||
}
|
||||
.ant-form-item-label,
|
||||
.ant-legacy-form-item-label,
|
||||
.ant-form-item-control,
|
||||
.ant-legacy-form-item-control {
|
||||
padding: 0;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
.label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 24px;
|
||||
color: @heading-color;
|
||||
font-size: @font-size-base;
|
||||
text-align: left;
|
||||
& > span {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
width: 80px;
|
||||
height: 32px;
|
||||
// height: 20px;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC-Medium, PingFang SC;
|
||||
line-height: 32px;
|
||||
// line-height: 20px;
|
||||
// &::after {
|
||||
// content: ':';
|
||||
// }
|
||||
}
|
||||
}
|
||||
.content {
|
||||
flex: 1 1 0;
|
||||
:global {
|
||||
.ant-form-item,
|
||||
.ant-legacy-form-item {
|
||||
&:last-child {
|
||||
display: block;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.standardFormRowLast {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.standardFormRowBlock {
|
||||
:global {
|
||||
.ant-form-item,
|
||||
.ant-legacy-form-item,
|
||||
div.ant-form-item-control-wrapper,
|
||||
div.ant-legacy-form-item-control-wrapper {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.standardFormRowGrid {
|
||||
:global {
|
||||
.ant-form-item,
|
||||
.ant-legacy-form-item,
|
||||
div.ant-form-item-control-wrapper,
|
||||
div.ant-legacy-form-item-control-wrapper {
|
||||
display: block;
|
||||
}
|
||||
.ant-form-item-label,
|
||||
.ant-legacy-form-item-label {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './index.less';
|
||||
|
||||
type StandardFormRowProps = {
|
||||
title?: string;
|
||||
last?: boolean;
|
||||
block?: boolean;
|
||||
grid?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
titleClassName?: string;
|
||||
};
|
||||
|
||||
const StandardFormRow: React.FC<StandardFormRowProps> = ({
|
||||
title,
|
||||
children,
|
||||
last,
|
||||
block,
|
||||
grid,
|
||||
titleClassName,
|
||||
...rest
|
||||
}) => {
|
||||
const cls = classNames(styles.standardFormRow, {
|
||||
[styles.standardFormRowBlock]: block,
|
||||
[styles.standardFormRowLast]: last,
|
||||
[styles.standardFormRowGrid]: grid,
|
||||
});
|
||||
|
||||
const labelCls = classNames(styles.label, titleClassName);
|
||||
|
||||
return (
|
||||
<div className={cls} {...rest}>
|
||||
{title && (
|
||||
<div className={labelCls}>
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StandardFormRow;
|
||||
@@ -0,0 +1,33 @@
|
||||
@import '~antd/es/style/themes/default.less';
|
||||
|
||||
.tagSelect {
|
||||
position: relative;
|
||||
max-height: 32px;
|
||||
margin-left: -8px;
|
||||
overflow: hidden;
|
||||
line-height: 32px;
|
||||
transition: all 0.3s;
|
||||
user-select: none;
|
||||
:global {
|
||||
.ant-tag {
|
||||
margin-right: 24px;
|
||||
padding: 0 8px;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
}
|
||||
&.expanded {
|
||||
max-height: 200px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.trigger {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
span.anticon {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
&.hasExpandTag {
|
||||
padding-right: 50px;
|
||||
}
|
||||
}
|
||||
191
webapp/packages/supersonic-fe/src/components/TagSelect/index.tsx
Normal file
191
webapp/packages/supersonic-fe/src/components/TagSelect/index.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import { useBoolean, useControllableValue } from 'ahooks';
|
||||
import { Tag } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { FC, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import styles from './index.less';
|
||||
|
||||
const { CheckableTag } = Tag;
|
||||
|
||||
export interface TagSelectOptionProps {
|
||||
value: string | number | undefined;
|
||||
style?: React.CSSProperties;
|
||||
checked?: boolean;
|
||||
onChange?: (value: string | number, state: boolean) => void;
|
||||
}
|
||||
|
||||
const TagSelectOption: React.FC<TagSelectOptionProps> & {
|
||||
isTagSelectOption: boolean;
|
||||
} = ({ children, checked, onChange, value }) => (
|
||||
<CheckableTag
|
||||
checked={!!checked}
|
||||
key={value}
|
||||
onChange={(state) => onChange && onChange(value, state)}
|
||||
>
|
||||
{children}
|
||||
</CheckableTag>
|
||||
);
|
||||
|
||||
TagSelectOption.isTagSelectOption = true;
|
||||
|
||||
type TagSelectOptionElement = React.ReactElement<TagSelectOptionProps, typeof TagSelectOption>;
|
||||
export interface TagSelectProps {
|
||||
onChange?: (value: (string | number)[]) => void;
|
||||
expandable?: boolean;
|
||||
value?: (string | number)[];
|
||||
defaultValue?: (string | number)[];
|
||||
style?: React.CSSProperties;
|
||||
hideCheckAll?: boolean;
|
||||
actionsText?: {
|
||||
expandText?: React.ReactNode;
|
||||
collapseText?: React.ReactNode;
|
||||
selectAllText?: React.ReactNode;
|
||||
};
|
||||
className?: string;
|
||||
Option?: TagSelectOptionProps;
|
||||
children?: TagSelectOptionElement | TagSelectOptionElement[];
|
||||
single?: boolean;
|
||||
disableUnCheck?: boolean;
|
||||
empty?: boolean;
|
||||
isSelectAll?: boolean;
|
||||
reverseCheckAll?: boolean;
|
||||
}
|
||||
|
||||
const TagSelect: FC<TagSelectProps> & { Option: typeof TagSelectOption } = (props) => {
|
||||
const {
|
||||
children,
|
||||
hideCheckAll = false,
|
||||
className,
|
||||
style,
|
||||
expandable,
|
||||
actionsText = {},
|
||||
single = false,
|
||||
disableUnCheck = false,
|
||||
empty = false,
|
||||
isSelectAll = false,
|
||||
reverseCheckAll = false,
|
||||
} = props;
|
||||
|
||||
const [expand, { toggle }] = useBoolean();
|
||||
|
||||
const [value, setValue] = useControllableValue<(string | number)[] | undefined>(props);
|
||||
|
||||
useEffect(() => {
|
||||
if (empty) {
|
||||
setValue([]);
|
||||
}
|
||||
}, [empty]);
|
||||
|
||||
const isTagSelectOption = (node: TagSelectOptionElement) =>
|
||||
node &&
|
||||
node.type &&
|
||||
(node.type.isTagSelectOption || node.type.displayName === 'TagSelectOption');
|
||||
|
||||
const getAllTags = () => {
|
||||
const childrenArray = React.Children.toArray(children) as TagSelectOptionElement[];
|
||||
const checkedTags = childrenArray
|
||||
.filter((child) => isTagSelectOption(child))
|
||||
.map((child) => child.props.value);
|
||||
return checkedTags || [];
|
||||
};
|
||||
|
||||
const onSelectAll = (checked: boolean) => {
|
||||
let checkedTags: (string | number)[] = [];
|
||||
if (reverseCheckAll) {
|
||||
setValue(undefined);
|
||||
return;
|
||||
}
|
||||
if (checked) {
|
||||
checkedTags = getAllTags();
|
||||
}
|
||||
setValue(checkedTags);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelectAll) {
|
||||
onSelectAll(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleTagChange = (tag: string | number, checked: boolean) => {
|
||||
let checkedTags: (string | number)[] = [...(value || [])];
|
||||
if (single && checkedTags.length > 0) {
|
||||
checkedTags = [checkedTags.join('')];
|
||||
}
|
||||
const index = checkedTags.indexOf(tag);
|
||||
if (checked && index === -1) {
|
||||
if (single) {
|
||||
checkedTags = [tag];
|
||||
} else {
|
||||
checkedTags.push(tag);
|
||||
}
|
||||
} else if (!checked && index > -1 && !disableUnCheck) {
|
||||
checkedTags.splice(index, 1);
|
||||
}
|
||||
setValue(checkedTags.length === 0 ? undefined : checkedTags);
|
||||
};
|
||||
|
||||
const checkedAll = getAllTags().length === value?.length;
|
||||
const hasChecked = value === undefined ? false : value?.length > 0;
|
||||
|
||||
const {
|
||||
expandText = '展开',
|
||||
collapseText = '收起',
|
||||
selectAllText = reverseCheckAll ? '不限' : '全部',
|
||||
} = actionsText;
|
||||
|
||||
const cls = classNames(styles.tagSelect, className, {
|
||||
[styles.hasExpandTag]: expandable,
|
||||
[styles.expanded]: expand,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cls} style={style}>
|
||||
{hideCheckAll ? null : (
|
||||
<CheckableTag
|
||||
checked={reverseCheckAll ? !hasChecked : checkedAll}
|
||||
key="tag-select-__all__"
|
||||
onChange={onSelectAll}
|
||||
>
|
||||
{selectAllText}
|
||||
</CheckableTag>
|
||||
)}
|
||||
{children &&
|
||||
React.Children.map(children, (child: TagSelectOptionElement) => {
|
||||
if (isTagSelectOption(child)) {
|
||||
return React.cloneElement(child, {
|
||||
key: `tag-select-${child.props.value}`,
|
||||
value: child.props.value,
|
||||
checked: value && value.indexOf(child.props.value) > -1,
|
||||
onChange: handleTagChange,
|
||||
});
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
{expandable && (
|
||||
<a
|
||||
className={styles.trigger}
|
||||
onClick={() => {
|
||||
toggle();
|
||||
}}
|
||||
>
|
||||
{expand ? (
|
||||
<>
|
||||
{collapseText} <UpOutlined />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{expandText}
|
||||
<DownOutlined />
|
||||
</>
|
||||
)}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TagSelect.Option = TagSelectOption;
|
||||
|
||||
export default TagSelect;
|
||||
Reference in New Issue
Block a user