@ant-design/icons#PlusCircleOutlined TypeScript Examples
The following examples show how to use
@ant-design/icons#PlusCircleOutlined.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: index.tsx From XFlow with MIT License | 6 votes |
GraphToolbar = (props: Props) => {
const { onAddNodeClick, onDeleteNodeClick, onConnectEdgeClick } = props
const [selectedNodes, setSelectedNodes] = React.useState([])
/** 监听画布中选中的节点 */
const watchModelService = async () => {
const appRef = useXFlowApp()
const modelService = appRef && appRef?.modelService
if (modelService) {
const model = await MODELS.SELECTED_NODES.getModel(modelService)
model.watch(async () => {
const nodes = await MODELS.SELECTED_NODES.useValue(modelService)
setSelectedNodes(nodes)
})
}
}
watchModelService()
return (
<div className="xflow-er-solution-toolbar">
<div className="icon" onClick={() => onAddNodeClick()}>
<span>添加节点</span>
<PlusCircleOutlined />
</div>
<div className="icon" onClick={() => onConnectEdgeClick()}>
<span>添加关系</span>
<LinkOutlined />
</div>
<div
className={`icon ${selectedNodes?.length > 0 ? '' : 'disabled'}`}
onClick={() => onDeleteNodeClick()}
>
<span>删除节点</span>
<DeleteOutlined />
</div>
</div>
)
}
Example #2
Source File: index.tsx From drip-table with MIT License | 6 votes |
public renderFormItem(item: unknown, index: number) {
return (
<div className={styles['array-component-form-container']} key={index}>
<div className={styles['array-component-left-container']}>
{ (this.props.schema['ui:props']?.items as DTGComponentPropertySchema[])
.map((schema, i) => this.renderAttributeItem(schema, i, index)) }
</div>
<div className={styles['array-component-right-container']}>
<Button
icon={<PlusCircleOutlined />}
shape="circle"
size="small"
onClick={() => {
const currentValue = this.props.value?.slice() || [];
currentValue.splice(index + 1, 0, { paramName: '', prefix: '', suffix: '' });
this.props.onChange?.(currentValue);
}}
/>
<Button
icon={<MinusCircleOutlined />}
shape="circle"
size="small"
onClick={() => {
const currentValue = this.props.value?.slice() || [];
currentValue.splice(index, 1);
this.props.onChange?.(currentValue);
}}
/>
</div>
</div>
);
}
Example #3
Source File: EmptyStates.tsx From posthog-foss with MIT License | 5 votes |
export function SavedInsightsEmptyState(): JSX.Element {
const { addGraph } = useActions(savedInsightsLogic)
const {
filters: { tab },
insights,
usingFilters,
} = useValues(savedInsightsLogic)
// show the search string that was used to make the results, not what it currently is
const searchString = insights.filters?.search || null
const { title, description } = SAVED_INSIGHTS_COPY[tab]
return (
<div className="saved-insight-empty-state">
<div className="empty-state-inner">
<div className="illustration-main">
<IconTrendUp />
</div>
<h2 className="empty-state__title">
{usingFilters
? searchString
? title.replace('$CONDITION', `matching "${searchString}"`)
: title.replace('$CONDITION', `matching these filters`)
: title.replace('$CONDITION', 'for this project')}
</h2>
<p className="empty-state__description">{description}</p>
{tab !== SavedInsightsTabs.Favorites && (
<Button
size="large"
type="primary"
onClick={() => addGraph('Trends')} // Add trends graph by default
data-attr="add-insight-button-empty-state"
icon={<PlusCircleOutlined />}
className="add-insight-button"
>
New Insight
</Button>
)}
</div>
</div>
)
}
Example #4
Source File: NumberValidatorInput.tsx From next-basics with GNU General Public License v3.0 | 5 votes |
export function NumberValidatorInput(
props: NumberValidatorInputProps
): React.ReactElement {
const { t } = useTranslation(NS_FLOW_BUILDER);
const handleChange = (
value: string | number,
field: "method" | "value",
index: number
): void => {
const newValue = update(props.value, {
$splice: [[index, 1, { ...props.value[index], [field]: value }]],
});
props.onChange?.(newValue);
};
return (
<div>
{props.value?.map((item, index) => (
<div key={index} className={styles.wrapper}>
<Input.Group compact style={{ flex: 1 }}>
<Select
value={item.method}
style={{ width: 100 }}
placeholder={t(K.COMPARE_METHOD_PLACEHOLDER)}
onChange={(value) => handleChange(value, "method", index)}
>
{compareMethodList.map((name) => (
<Select.Option key={name} value={name}>
{name}
</Select.Option>
))}
</Select>
<InputNumber
value={item.value}
style={{ width: "calc(100% - 100px)" }}
min={0}
step={1}
placeholder={t(K.COMPARE_VALUE_PLACEHOLDER)}
onChange={(value) =>
handleChange(value as number, "value", index)
}
/>
</Input.Group>
<Button
className={editorStyles.iconBtn}
type="link"
style={{ flexBasis: 30 }}
onClick={() => props.onRemove(index)}
>
<MinusOutlined />
</Button>
</div>
))}
<Button
className={editorStyles.iconBtn}
type="link"
onClick={props.onAdd}
>
<PlusCircleOutlined />
</Button>
</div>
);
}
Example #5
Source File: toolbar-config.tsx From XFlow with MIT License | 5 votes |
IconStore.set('PlusCircleOutlined', PlusCircleOutlined)
Example #6
Source File: Col.tsx From ant-extensions with MIT License | 5 votes |
Col: React.FC<IColConfig> = React.memo((item) => {
const { t } = useTranslation(I18nKey);
const { isEditing, updateConfig, addWidget } = useContext(Context);
const { id, children, colSpan } = item;
const refEl = useRef<HTMLDivElement>(null);
const [span, setSpan] = useState(colSpan);
useLayoutEffect(() => {
setSpan(span);
}, [span]);
const onResize = (evt: MouseEvent) => {
const newX = evt.clientX;
if (refEl.current && refEl.current.parentElement) {
const box = refEl.current.getBoundingClientRect();
const minWidth = Math.round(refEl.current.parentElement.offsetWidth / 12);
let newSpan = Math.floor((newX - (box.left - minWidth)) / minWidth) || 1;
if (newSpan > 12) newSpan = 12;
setSpan(newSpan as AnyObject);
}
};
const onResizeEnd = () => {
if (refEl.current) {
updateConfig(id, "span", colSpan);
}
document.body.style.cursor = "unset";
document.removeEventListener("mousemove", onResize);
document.removeEventListener("mouseup", onResizeEnd);
};
const onResizeStart = (e: React.MouseEvent) => {
e.preventDefault();
document.body.style.cursor = "col-resize";
document.addEventListener("mousemove", onResize);
document.addEventListener("mouseup", onResizeEnd);
};
const isStretched = useMemo(() => {
return children && children.length > 0 && children[0].type === EnumTypes.TILE;
}, [children]);
return (
<Item
item={item}
itemRef={refEl}
style={{ gridColumnEnd: `span ${span}`, gridAutoRows: isStretched ? "auto" : "max-content" }}
>
{Array.isArray(children) &&
(children as AnyObject).map((item: AnyObject) => {
switch (item.type) {
case EnumTypes.HEADING:
return <Heading key={item.id} {...item} />;
case EnumTypes.DIVIDER:
return <Divider key={item.id} {...item} />;
case EnumTypes.ROW:
return <Row key={item.id} {...item} />;
case EnumTypes.TILE:
return <Tile key={item.id} {...item} />;
default:
return null;
}
})}
{isEditing && (!children || children.length === 0) && (
<div className="ant-ext-pm__emptyCol">
<div>{t("label.drag")}</div>
<AntDivider>or</AntDivider>
<Button
icon={<PlusCircleOutlined />}
onClick={(e) => [addWidget(item.id), e.stopPropagation()]}
>
Add New Widget
</Button>
</div>
)}
{isEditing && <div className="ant-ext-pm__resizer" onMouseDown={onResizeStart} />}
</Item>
);
})
Example #7
Source File: index.tsx From fe-v5 with Apache License 2.0 | 5 votes |
export default function index({ targets }) {
const namePrefix = ['overrides'];
return (
<Form.List name={namePrefix}>
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => {
return (
<Panel
isInner
header='override'
extra={
<Space>
<PlusCircleOutlined
onClick={() => {
add({
type: 'special',
});
}}
/>
{fields.length > 1 && (
<MinusCircleOutlined
onClick={() => {
remove(name);
}}
/>
)}
</Space>
}
>
<Form.Item label='查询条件名称' {...restField} name={[name, 'matcher', 'value']}>
<Select suffixIcon={<CaretDownOutlined />} allowClear>
{_.map(targets, (target) => {
return (
<Select.Option key={target.refId} value={target.refId}>
{target.refId}
</Select.Option>
);
})}
</Select>
</Form.Item>
<ValueMappings preNamePrefix={namePrefix} namePrefix={[name, 'properties', 'valueMappings']} />
<StandardOptions preNamePrefix={namePrefix} namePrefix={[name, 'properties', 'standardOptions']} />
</Panel>
);
})}
</>
)}
</Form.List>
);
}
Example #8
Source File: index.tsx From visual-layout with MIT License | 5 votes |
Home: React.FC<{}> = () => {
const { appService } = useContext(AppContext);
const projects: ProjectObject[] = [];
appService.projects.forEach(project => projects.unshift(project));
return (
<Visible>
{({ visible, setVisible }) => (
<>
<Drawer
title="项目 Home"
placement="top"
height={'calc(100% - 40px)'}
mask={false}
onClose={() => setVisible(false)}
visible={visible}
>
<Row gutter={[20, 20]}>
<Col className={styles.newBuild} span={4}>
<div
onClick={() => {
appService.new();
setVisible(false);
}}
>
<span>
<PlusCircleOutlined />
</span>
<span>新建项目</span>
</div>
</Col>
{projects.map(project => (
<Col className={styles.projects} span={4} key={project.id}>
<Project
project={project}
appService={appService}
setVisible={setVisible}
/>
</Col>
))}
</Row>
</Drawer>
<div
onClick={() => setVisible(!visible)}
style={{ height: '100%', width: '100%', padding: '5px 10px' }}
>
项目
<FolderAddOutlined
style={{ color: 'white', fontSize: 16, marginLeft: 5 }}
/>
</div>
</>
)}
</Visible>
);
}
Example #9
Source File: PathCleanFilterInput.tsx From posthog-foss with MIT License | 5 votes |
export function PathCleanFilterInput(): JSX.Element {
const [open, setOpen] = useState(false)
const { insightProps } = useValues(insightLogic)
const { filter } = useValues(pathsLogic(insightProps))
const { setFilter } = useActions(pathsLogic(insightProps))
return (
<>
<PathCleanFilters
style={{ paddingLeft: 10 }}
pageKey="pathcleanfilters-local"
pathCleaningFilters={filter.local_path_cleaning_filters || []}
onChange={(newItem) => {
setFilter({
local_path_cleaning_filters: [...(filter.local_path_cleaning_filters || []), newItem],
})
}}
onRemove={(index) => {
const newState = (filter.local_path_cleaning_filters || []).filter((_, i) => i !== index)
setFilter({ local_path_cleaning_filters: newState })
}}
/>
<Row align="middle" justify="space-between" style={{ paddingLeft: 0 }}>
<Popup
visible={open}
placement={'bottom-end'}
fallbackPlacements={['bottom-start']}
onClickOutside={() => setOpen(false)}
overlay={
<PathRegexPopup
item={{}}
onClose={() => setOpen(false)}
onComplete={(newItem) => {
setFilter({
local_path_cleaning_filters: [
...(filter.local_path_cleaning_filters || []),
newItem,
],
})
setOpen(false)
}}
/>
}
>
{({ setRef }) => {
return (
<>
<Button
ref={setRef}
onClick={() => setOpen(!open)}
className="new-prop-filter"
data-attr={'new-prop-filter-' + 'pathcleanfilters-local'}
type="link"
style={{ paddingLeft: 0 }}
icon={<PlusCircleOutlined />}
>
{'Add Rule'}
</Button>
</>
)
}}
</Popup>
<PathCleanFilterToggle filters={filter} onChange={setFilter} />
</Row>
</>
)
}
Example #10
Source File: EmptyStates.tsx From posthog-foss with MIT License | 5 votes |
export function FunnelSingleStepState(): JSX.Element {
const { insightProps } = useValues(insightLogic)
const { filters, clickhouseFeaturesEnabled } = useValues(funnelLogic(insightProps))
const { setFilters } = useActions(funnelLogic(insightProps))
const { addFilter } = useActions(entityFilterLogic({ setFilters, filters, typeKey: 'EditFunnel-action' }))
return (
<div className="insight-empty-state funnels-empty-state">
<div className="empty-state-inner">
<div className="illustration-main">
<PlusCircleOutlined />
</div>
<h2 className="funnels-empty-state__title">Add another step!</h2>
<p className="funnels-empty-state__description">
You’re almost there! Funnels require at least two steps before calculating.
{clickhouseFeaturesEnabled
? ' Once you have two steps defined, additional changes will recalculate automatically.'
: ''}
</p>
<div className="mt text-center">
<Button
size="large"
onClick={() => addFilter()}
data-attr="add-action-event-button-empty-state"
icon={<PlusCircleOutlined />}
className="add-action-event-button"
>
Add funnel step
</Button>
</div>
<div className="mt text-center">
<a
data-attr="funnels-single-step-help"
href="https://posthog.com/docs/user-guides/funnels?utm_medium=in-product&utm_campaign=funnel-empty-state"
target="_blank"
rel="noopener"
className="flex-center"
style={{ justifyContent: 'center' }}
>
Learn more about funnels in our support documentation
<IconOpenInNew style={{ marginLeft: 4, fontSize: '0.85em' }} />
</a>
</div>
</div>
</div>
)
}
Example #11
Source File: TaxonomicBreakdownButton.tsx From posthog-foss with MIT License | 5 votes |
export function TaxonomicBreakdownButton({
breakdownType,
onChange,
onlyCohorts,
buttonType = 'link',
}: TaxonomicBreakdownButtonProps): JSX.Element {
const [open, setOpen] = useState(false)
const { allEventNames } = useValues(insightLogic)
const { groupsTaxonomicTypes } = useValues(groupsModel)
return (
<Popup
overlay={
<TaxonomicFilter
groupType={breakdownType}
onChange={(taxonomicGroup, value) => {
if (value) {
onChange(value, taxonomicGroup)
setOpen(false)
}
}}
eventNames={allEventNames}
taxonomicGroupTypes={
onlyCohorts
? [TaxonomicFilterGroupType.CohortsWithAllUsers]
: [
TaxonomicFilterGroupType.EventProperties,
TaxonomicFilterGroupType.PersonProperties,
...groupsTaxonomicTypes,
TaxonomicFilterGroupType.CohortsWithAllUsers,
]
}
/>
}
placement={'bottom-start'}
fallbackPlacements={['bottom-end']}
visible={open}
onClickOutside={() => setOpen(false)}
>
{({ setRef }) => (
<Button
type={buttonType}
icon={<PlusCircleOutlined />}
data-attr="add-breakdown-button"
onClick={() => setOpen(!open)}
className="taxonomic-breakdown-filter tag-button"
ref={setRef}
>
<PropertyKeyInfo
value={
breakdownType === TaxonomicFilterGroupType.CohortsWithAllUsers
? 'Add cohort'
: 'Add breakdown'
}
/>
</Button>
)}
</Popup>
)
}
Example #12
Source File: Comparison.tsx From fe-v5 with Apache License 2.0 | 5 votes |
render() {
const { curComparison } = this.state;
const handleClick = (e) => {
const index = this.state.curComparison.findIndex(cc => cc === e.key)
let newCurComparison
if (index === -1) {
newCurComparison = [
...this.state.curComparison,
e.key
]
this.setState({
curComparison: newCurComparison
})
} else {
let curComparisonCopy = [...this.state.curComparison]
curComparisonCopy.splice(index, 1)
newCurComparison = curComparisonCopy
this.setState({
curComparison: curComparisonCopy
})
}
const { onChange, relativeTimeComparison, comparisonOptions } = this.props;
onChange({
...this.refresh(),
comparison: newCurComparison,
relativeTimeComparison,
comparisonOptions,
});
}
const menu = (
<Menu onClick={handleClick} selectedKeys={curComparison}>
<Menu.Item key='1d'>1d</Menu.Item>
<Menu.Item key='7d'>7d</Menu.Item>
</Menu>
)
return (
<div className="graph-config-inner-comparison">
{/* <Select
dropdownMatchSelectWidth={false}
mode="multiple"
style={{ minWidth: 80, width: 'auto', verticalAlign: 'middle' }}
value={curComparison}
onChange={this.handleComparisonChange}>
<Option key={'1d'} value={'1d'}>1天</Option>
<Option key={'7d'} value={'7d'}>7天</Option>
</Select> */}
{this.state.curComparison.map(cc =>
<Tag key={cc} closable onClose={e => {
handleClick({key: cc})
}}>
{cc}
</Tag>
)}
<Dropdown overlay={menu}>
<a className="ant-dropdown-link" onClick={e => e.preventDefault()}>
<PlusCircleOutlined />
</a>
</Dropdown>
</div>
);
}
Example #13
Source File: PathItemFilters.tsx From posthog-foss with MIT License | 5 votes |
export function PathItemFilters({
propertyFilters = null,
onChange = null,
pageKey,
style = {},
taxonomicGroupTypes,
wildcardOptions,
}: PropertyFiltersProps): JSX.Element {
const logicProps = { propertyFilters, onChange, pageKey, urlOverride: 'exclude_events' }
const { filters } = useValues(propertyFilterLogic(logicProps))
const { setFilter, remove, setFilters } = useActions(propertyFilterLogic(logicProps))
useEffect(() => {
if (propertyFilters && !objectsEqual(propertyFilters, filters)) {
setFilters([...propertyFilters, {}])
}
}, [propertyFilters])
return (
<div className="mb" style={style}>
<BindLogic logic={propertyFilterLogic} props={logicProps}>
{filters?.length &&
filters.map((filter, index) => {
return (
<div key={index} style={{ margin: '0.25rem 0', padding: '0.25rem 0' }}>
<PathItemSelector
pathItem={filter.value as string | undefined}
onChange={(pathItem) => setFilter(index, pathItem, pathItem, null, 'event')}
index={index}
taxonomicGroupTypes={taxonomicGroupTypes}
wildcardOptions={wildcardOptions}
>
{!filter.value ? (
<Button
className="new-prop-filter"
data-attr={'new-prop-filter-' + pageKey}
type="link"
style={{ paddingLeft: 0 }}
icon={<PlusCircleOutlined />}
>
Add exclusion
</Button>
) : (
<Row align="middle">
<FilterButton>{filter.value as string}</FilterButton>
{!!Object.keys(filters[index]).length && (
<CloseButton
onClick={(e: Event) => {
remove(index)
e.stopPropagation()
}}
style={{
cursor: 'pointer',
float: 'none',
paddingLeft: 8,
alignSelf: 'center',
}}
/>
)}
</Row>
)}
</PathItemSelector>
</div>
)
})}
</BindLogic>
</div>
)
}
Example #14
Source File: MainMenu.tsx From mayoor with MIT License | 5 votes |
MainMenu: React.FC = () => {
const { t } = useTranslation();
const { currentUser } = useAppState();
return (
<StyledMenu>
<li>
<CategoryName>{t('Orders')}</CategoryName>
<LinkItem icon={<PlusCircleOutlined />} name={t('Add order')} to={'/orders/new'} />
<LinkItem
icon={<FileSearchOutlined />}
name={t('List orders')}
to={'/orders/list'}
/>
<LinkItem
icon={<BgColorsOutlined />}
name={t('To be printed')}
to={'/orders/print'}
/>
<LinkItem
icon={<HighlightOutlined />}
name={t('Waiting for production')}
to={'/orders/production'}
/>
</li>
<li>
<CategoryName>{t('Customers')}</CategoryName>
<LinkItem
icon={<UserAddOutlined />}
name={t('Add customer')}
to={'/customers/new'}
/>
<LinkItem icon={<TeamOutlined />} name={t('Customers')} to={'/customers/list'} />
</li>
{currentUser?.role === UserRole.EXECUTIVE && (
<li>
<CategoryName>{t('Administration')}</CategoryName>
<LinkItem icon={<FileTextOutlined />} name={t('Material')} to={'/materials'} />
<LinkItem icon={<TeamOutlined />} name={t('Users')} to={'/users'} />
</li>
)}
</StyledMenu>
);
}
Example #15
Source File: index.tsx From fe-v5 with Apache License 2.0 | 5 votes |
export default function index({ targets }) {
const namePrefix = ['overrides'];
return (
<Form.List name={namePrefix}>
{(fields, { add, remove }) =>
fields.map(({ key, name, ...restField }) => {
return (
<Panel
key={key}
isInner
header='override'
extra={
<Space>
<PlusCircleOutlined
onClick={() => {
add({
type: 'special',
});
}}
/>
{fields.length > 1 && (
<MinusCircleOutlined
onClick={() => {
remove(name);
}}
/>
)}
</Space>
}
>
<Form.Item label='查询条件名称' {...restField} name={[name, 'matcher', 'value']}>
<Select suffixIcon={<CaretDownOutlined />} allowClear>
{_.map(targets, (target) => {
return (
<Select.Option key={target.refId} value={target.refId}>
{target.refId}
</Select.Option>
);
})}
</Select>
</Form.Item>
<ValueMappings preNamePrefix={namePrefix} namePrefix={[name, 'properties', 'valueMappings']} />
<StandardOptions preNamePrefix={namePrefix} namePrefix={[name, 'properties', 'standardOptions']} />
</Panel>
);
})
}
</Form.List>
);
}
Example #16
Source File: editModal.tsx From fe-v5 with Apache License 2.0 | 4 votes |
editModal: React.FC<Props> = ({ isModalVisible, editModalFinish }) => {
const { t, i18n } = useTranslation();
const [form] = Form.useForm();
const { clusters: clusterList } = useSelector<RootState, CommonStoreState>((state) => state.common);
const [contactList, setInitContactList] = useState([]);
const [notifyGroups, setNotifyGroups] = useState([]);
const [field, setField] = useState<string>('cluster');
const [refresh, setRefresh] = useState(true);
useEffect(() => {
getNotifyChannel();
getGroups('');
return () => {};
}, []);
const enableDaysOfWeekOptions = [t('周日'), t('周一'), t('周二'), t('周三'), t('周四'), t('周五'), t('周六')].map((v, i) => {
return <Option value={String(i)} key={i}>{`${v}`}</Option>;
});
const contactListCheckboxes = contactList.map((c: { key: string; label: string }) => (
<Checkbox value={c.key} key={c.label}>
{c.label}
</Checkbox>
));
const notifyGroupsOptions = notifyGroups.map((ng: any) => (
<Option value={ng.id} key={ng.id}>
{ng.name}
</Option>
));
const getNotifyChannel = async () => {
const res = await getNotifiesList();
let contactList = res || [];
setInitContactList(contactList);
};
const getGroups = async (str) => {
const res = await getTeamInfoList({ query: str });
const data = res.dat || res;
setNotifyGroups(data || []);
};
const debounceFetcher = useCallback(debounce(getGroups, 800), []);
const modelOk = () => {
form.validateFields().then(async (values) => {
const data = { ...values };
switch (values.field) {
case 'enable_time':
data.enable_stime = values.enable_time[0].format('HH:mm');
data.enable_etime = values.enable_time[1].format('HH:mm');
delete data.enable_time;
break;
case 'disabled':
data.disabled = !values.enable_status ? 1 : 0;
delete data.enable_status;
break;
case 'enable_in_bg':
data.enable_in_bg = values.enable_in_bg ? 1 : 0;
break;
case 'callbacks':
data.callbacks = values.callbacks.map((item) => item.url);
break;
case 'notify_recovered':
data.notify_recovered = values.notify_recovered ? 1 : 0;
break;
default:
break;
}
delete data.field;
Object.keys(data).forEach((key) => {
// 因为功能上有清除备注的需求,需要支持传空
if (data[key] === undefined) {
data[key] = '';
}
if (Array.isArray(data[key])) {
data[key] = data[key].join(' ');
}
});
editModalFinish(true, data);
});
};
const editModalClose = () => {
editModalFinish(false);
};
const fieldChange = (val) => {
setField(val);
};
return (
<>
<Modal
title={t('批量更新')}
visible={isModalVisible}
onOk={modelOk}
onCancel={() => {
editModalClose();
}}
>
<Form
{...layout}
form={form}
className='strategy-form'
layout={refresh ? 'horizontal' : 'horizontal'}
initialValues={{
prom_eval_interval: 15,
disabled: 0, // 0:立即启用 1:禁用
enable_status: true, // true:立即启用 false:禁用
notify_recovered: 1, // 1:启用
enable_time: [moment('00:00', 'HH:mm'), moment('23:59', 'HH:mm')],
cluster: clusterList[0] || 'Default', // 生效集群
enable_days_of_week: ['1', '2', '3', '4', '5', '6', '0'],
field: 'cluster',
}}
>
<Form.Item
label={t('字段:')}
name='field'
rules={[
{
required: false,
},
]}
>
<Select suffixIcon={<CaretDownOutlined />} style={{ width: '100%' }} onChange={fieldChange}>
{fields.map((item) => (
<Option key={item.id} value={item.field}>
{item.name}
</Option>
))}
</Select>
</Form.Item>
{(() => {
switch (field) {
case 'note':
return (
<>
<Form.Item
label={t('改为:')}
name='note'
rules={[
{
required: false,
},
]}
>
<Input placeholder={t('请输入规则备注')} />
</Form.Item>
</>
);
case 'runbook_url':
return (
<>
<Form.Item label={t('改为:')} name='runbook_url'>
<Input />
</Form.Item>
</>
);
case 'cluster':
return (
<>
<Form.Item
label={t('改为:')}
name='cluster'
rules={[
{
required: false,
message: t('生效集群不能为空'),
},
]}
>
<Select suffixIcon={<CaretDownOutlined />}>
{clusterList?.map((item) => (
<Option value={item} key={item}>
{item}
</Option>
))}
</Select>
</Form.Item>
</>
);
case 'severity':
return (
<>
<Form.Item
label={t('改为:')}
name='severity'
initialValue={2}
rules={[
{
required: false,
message: t('告警级别不能为空'),
},
]}
>
<Radio.Group>
<Radio value={1}>{t('一级报警')}</Radio>
<Radio value={2}>{t('二级报警')}</Radio>
<Radio value={3}>{t('三级报警')}</Radio>
</Radio.Group>
</Form.Item>
</>
);
case 'disabled':
return (
<>
<Form.Item
label={t('改为:')}
name='enable_status'
rules={[
{
required: false,
message: t('立即启用不能为空'),
},
]}
valuePropName='checked'
>
<Switch />
</Form.Item>
</>
);
case 'enable_in_bg':
return (
<>
<Form.Item label={t('改为:')} name='enable_in_bg' valuePropName='checked'>
<SwitchWithLabel label='根据告警事件中的ident归属关系判断' />
</Form.Item>
</>
);
case 'prom_eval_interval':
return (
<>
<Form.Item
label={t('改为:')}
rules={[
{
required: false,
message: t('执行频率不能为空'),
},
]}
>
<Space>
<Form.Item style={{ marginBottom: 0 }} name='prom_eval_interval' initialValue={15} wrapperCol={{ span: 10 }}>
<InputNumber
min={1}
onChange={(val) => {
setRefresh(!refresh);
}}
/>
</Form.Item>
秒
<Tooltip title={t(`每隔${form.getFieldValue('prom_eval_interval')}秒,把PromQL作为查询条件,去查询后端存储,如果查到了数据就表示当次有监控数据触发了规则`)}>
<QuestionCircleFilled />
</Tooltip>
</Space>
</Form.Item>
</>
);
case 'prom_for_duration':
return (
<>
<Form.Item
label={t('改为:')}
rules={[
{
required: false,
message: t('持续时长不能为空'),
},
]}
>
<Space>
<Form.Item style={{ marginBottom: 0 }} name='prom_for_duration' initialValue={60} wrapperCol={{ span: 10 }}>
<InputNumber min={0} />
</Form.Item>
秒
<Tooltip
title={t(
`通常持续时长大于执行频率,在持续时长内按照执行频率多次执行PromQL查询,每次都触发才生成告警;如果持续时长置为0,表示只要有一次PromQL查询触发阈值,就生成告警`,
)}
>
<QuestionCircleFilled />
</Tooltip>
</Space>
</Form.Item>
</>
);
case 'notify_channels':
return (
<>
<Form.Item label={t('改为:')} name='notify_channels'>
<Checkbox.Group>{contactListCheckboxes}</Checkbox.Group>
</Form.Item>
</>
);
case 'notify_groups':
return (
<>
<Form.Item label={t('改为:')} name='notify_groups'>
<Select mode='multiple' showSearch optionFilterProp='children' filterOption={false} onSearch={(e) => debounceFetcher(e)} onBlur={() => getGroups('')}>
{notifyGroupsOptions}
</Select>
</Form.Item>
</>
);
case 'notify_recovered':
return (
<>
<Form.Item label={t('改为:')} name='notify_recovered' valuePropName='checked'>
<Switch />
</Form.Item>
</>
);
case 'recover_duration':
return (
<>
<Form.Item label={t('改为:')}>
<Space>
<Form.Item
style={{ marginBottom: 0 }}
name='recover_duration'
initialValue={0}
wrapperCol={{ span: 10 }}
rules={[
{
required: false,
message: t('留观时长不能为空'),
},
]}
>
<InputNumber
min={0}
onChange={(val) => {
setRefresh(!refresh);
}}
/>
</Form.Item>
秒
<Tooltip title={t(`持续${form.getFieldValue('recover_duration') || 0}秒没有再次触发阈值才发送恢复通知`)}>
<QuestionCircleFilled />
</Tooltip>
</Space>
</Form.Item>
</>
);
case 'notify_repeat_step':
return (
<>
<Form.Item label={t('改为:')}>
<Space>
<Form.Item
style={{ marginBottom: 0 }}
name='notify_repeat_step'
initialValue={60}
wrapperCol={{ span: 10 }}
rules={[
{
required: false,
message: t('重复发送频率不能为空'),
},
]}
>
<InputNumber
min={0}
onChange={(val) => {
setRefresh(!refresh);
}}
/>
</Form.Item>
分钟
<Tooltip title={t(`如果告警持续未恢复,间隔${form.getFieldValue('notify_repeat_step')}分钟之后重复提醒告警接收组的成员`)}>
<QuestionCircleFilled />
</Tooltip>
</Space>
</Form.Item>
</>
);
case 'callbacks':
return (
<>
<Form.Item label={t('改为:')}>
<Form.List name='callbacks' initialValue={[{}]}>
{(fields, { add, remove }, { errors }) => (
<>
{fields.map((field, index) => (
<Row gutter={[10, 0]} key={field.key}>
<Col span={22}>
<Form.Item name={[field.name, 'url']}>
<Input />
</Form.Item>
</Col>
<Col span={1}>
<MinusCircleOutlined className='control-icon-normal' onClick={() => remove(field.name)} />
</Col>
</Row>
))}
<PlusCircleOutlined className='control-icon-normal' onClick={() => add()} />
</>
)}
</Form.List>
</Form.Item>
</>
);
case 'append_tags':
return (
<>
<Form.Item label='附加标签' name='append_tags' rules={[{ required: false, message: '请填写至少一项标签!' }, isValidFormat]}>
<Select mode='tags' tokenSeparators={[' ']} open={false} placeholder={'标签格式为 key=value ,使用回车或空格分隔'} tagRender={tagRender} />
</Form.Item>
</>
);
case 'enable_time':
return (
<>
<Form.Item
label={t('改为:')}
name='enable_days_of_week'
rules={[
{
required: false,
message: t('生效时间不能为空'),
},
]}
>
<Select mode='tags'>{enableDaysOfWeekOptions}</Select>
</Form.Item>
<Form.Item
name='enable_time'
{...tailLayout}
rules={[
{
required: false,
message: t('生效时间不能为空'),
},
]}
>
<TimePicker.RangePicker
format='HH:mm'
onChange={(val, val2) => {
form.setFieldsValue({
enable_stime: val2[0],
enable_etime: val2[1],
});
}}
/>
</Form.Item>
</>
);
default:
return null;
}
})()}
</Form>
</Modal>
</>
);
}
Example #17
Source File: Graph.tsx From fe-v5 with Apache License 2.0 | 4 votes |
export default function Graph(props: IProps) {
const { metric, match, range, step, onClose } = props;
const newGroups = _.map(
_.filter(match.dimensionLabels, (item) => !_.isEmpty(item.value)),
'label',
);
const [refreshFlag, setRefreshFlag] = useState(_.uniqueId('refreshFlag_'));
const [calcFunc, setCalcFunc] = useState('');
const [comparison, setComparison] = useState<string[]>([]);
const [aggrFunc, setAggrFunc] = useState('avg');
const [aggrGroups, setAggrGroups] = useState<string[]>(newGroups);
const [labels, setLabels] = useState<string[]>([]);
const [series, setSeries] = useState<any[]>([]);
const [highLevelConfig, setHighLevelConfig] = useState({
shared: true,
sharedSortDirection: 'desc',
legend: true,
util: 'none',
colorRange: colors[0].value,
reverseColorOrder: false,
colorDomainAuto: true,
colorDomain: [],
chartheight: 300,
});
const [chartType, setChartType] = useState('line');
const [reduceFunc, setReduceFunc] = useState('last');
const lineGraphProps = {
custom: {
drawStyle: 'lines',
fillOpacity: 0,
stack: 'hidden',
lineInterpolation: 'smooth',
},
options: {
legend: {
displayMode: highLevelConfig.legend ? 'list' : 'hidden',
},
tooltip: {
mode: highLevelConfig.shared ? 'all' : 'single',
sort: highLevelConfig.sharedSortDirection,
},
standardOptions: {
util: highLevelConfig.util,
},
},
};
const hexbinGraphProps = {
custom: {
calc: reduceFunc,
colorRange: highLevelConfig.colorRange,
reverseColorOrder: highLevelConfig.reverseColorOrder,
colorDomainAuto: highLevelConfig.colorDomainAuto,
colorDomain: highLevelConfig.colorDomain,
},
options: {
standardOptions: {
util: highLevelConfig.util,
},
},
};
const graphStandardOptions = {
line: <LineGraphStandardOptions highLevelConfig={highLevelConfig} setHighLevelConfig={setHighLevelConfig} />,
hexbin: <HexbinGraphStandardOptions highLevelConfig={highLevelConfig} setHighLevelConfig={setHighLevelConfig} />,
};
useEffect(() => {
setAggrGroups(newGroups);
}, [JSON.stringify(newGroups)]);
useEffect(() => {
const matchStr = getMatchStr(match);
getLabels(`${metric}${matchStr}`, range).then((res) => {
setLabels(res);
});
}, [refreshFlag, JSON.stringify(match), JSON.stringify(range)]);
useEffect(() => {
getQueryRange({
metric,
match: getMatchStr(match),
range,
step,
aggrFunc,
aggrGroups,
calcFunc,
comparison,
}).then((res) => {
setSeries(res);
});
}, [refreshFlag, metric, JSON.stringify(match), JSON.stringify(range), step, calcFunc, comparison, aggrFunc, aggrGroups]);
return (
<Card
size='small'
style={{ marginBottom: 10 }}
title={metric}
className='n9e-metric-views-metrics-graph'
extra={
<Space>
<Space size={0} style={{ marginRight: 10 }}>
<LineChartOutlined
className={classNames({
'button-link-icon': true,
active: chartType === 'line',
})}
onClick={() => {
setChartType('line');
}}
/>
<Divider type='vertical' />
<HexbinIcon
className={classNames({
'button-link-icon': true,
active: chartType === 'hexbin',
})}
onClick={() => {
setChartType('hexbin');
}}
/>
</Space>
<Popover placement='left' content={graphStandardOptions[chartType]} trigger='click' autoAdjustOverflow={false} getPopupContainer={() => document.body}>
<a>
<SettingOutlined />
</a>
</Popover>
<a>
<SyncOutlined
onClick={() => {
setRefreshFlag(_.uniqueId('refreshFlag_'));
}}
/>
</a>
<a>
<ShareAltOutlined
onClick={() => {
const curCluster = localStorage.getItem('curCluster');
const dataProps = {
type: 'timeseries',
version: '2.0.0',
name: metric,
step,
range,
...lineGraphProps,
targets: _.map(
getExprs({
metric,
match: getMatchStr(match),
aggrFunc,
aggrGroups,
calcFunc,
comparison,
}),
(expr) => {
return {
expr,
};
},
),
};
setTmpChartData([
{
configs: JSON.stringify({
curCluster,
dataProps,
}),
},
]).then((res) => {
const ids = res.dat;
window.open('/chart/' + ids);
});
}}
/>
</a>
<a>
<CloseCircleOutlined onClick={onClose} />
</a>
</Space>
}
>
<div>
<Space>
<div>
计算函数:
<Dropdown
overlay={
<Menu onClick={(e) => setCalcFunc(e.key === 'clear' ? '' : e.key)} selectedKeys={[calcFunc]}>
<Menu.Item key='rate_1m'>rate_1m</Menu.Item>
<Menu.Item key='rate_5m'>rate_5m</Menu.Item>
<Menu.Item key='increase_1m'>increase_1m</Menu.Item>
<Menu.Item key='increase_5m'>increase_5m</Menu.Item>
<Menu.Divider></Menu.Divider>
<Menu.Item key='clear'>clear</Menu.Item>
</Menu>
}
>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
{calcFunc || '无'} <DownOutlined />
</a>
</Dropdown>
</div>
<div>
环比:
{comparison.map((ag) => (
<Tag
key={ag}
closable
onClose={() => {
setComparison(_.without(comparison, ag));
}}
>
{ag}
</Tag>
))}
<Dropdown
overlay={
<Menu
onClick={(e) => {
if (comparison.indexOf(e.key) === -1) {
setComparison([...comparison, e.key]);
} else {
setComparison(_.without(comparison, e.key));
}
}}
selectedKeys={comparison}
>
<Menu.Item key='1d'>1d</Menu.Item>
<Menu.Item key='7d'>7d</Menu.Item>
</Menu>
}
overlayStyle={{ maxHeight: 400, overflow: 'auto' }}
>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
<PlusCircleOutlined />
</a>
</Dropdown>
</div>
<div>
聚合函数:
<Dropdown
overlay={
<Menu onClick={(e) => setAggrFunc(e.key)} selectedKeys={[aggrFunc]}>
<Menu.Item key='sum'>sum</Menu.Item>
<Menu.Item key='avg'>avg</Menu.Item>
<Menu.Item key='max'>max</Menu.Item>
<Menu.Item key='min'>min</Menu.Item>
</Menu>
}
>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
{aggrFunc} <DownOutlined />
</a>
</Dropdown>
</div>
{aggrFunc ? (
<div className='graph-config-inner-item'>
聚合维度:
{aggrGroups.map((ag) => (
<Tag
key={ag}
closable
onClose={() => {
setAggrGroups(_.without(aggrGroups, ag));
}}
>
{ag}
</Tag>
))}
<Dropdown
overlay={
<Menu
onClick={(e) => {
if (aggrGroups.indexOf(e.key) === -1) {
setAggrGroups([...aggrGroups, e.key]);
} else {
setAggrGroups(_.without(aggrGroups, e.key));
}
}}
selectedKeys={aggrGroups}
>
{_.map(
_.filter(labels, (n) => n !== '__name__'),
(ag) => (
<Menu.Item key={ag}>{ag}</Menu.Item>
),
)}
</Menu>
}
overlayStyle={{ maxHeight: 400, overflow: 'auto' }}
>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
<PlusCircleOutlined />
</a>
</Dropdown>
</div>
) : null}
{chartType === 'hexbin' && (
<div>
取值计算:
<Dropdown
overlay={
<Menu onClick={(e) => setReduceFunc(e.key)} selectedKeys={[reduceFunc]}>
{_.map(calcsOptions, (val, key) => {
return <Menu.Item key={key}>{val.name}</Menu.Item>;
})}
</Menu>
}
>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
{calcsOptions[reduceFunc]?.name} <DownOutlined />
</a>
</Dropdown>
</div>
)}
</Space>
</div>
<div>
{chartType === 'line' && <Timeseries inDashboard={false} values={lineGraphProps as any} series={series} />}
{chartType === 'hexbin' && (
<div style={{ padding: '20px 0 0 0', height: highLevelConfig.chartheight }}>
<Hexbin values={hexbinGraphProps as any} series={series} />
</div>
)}
</div>
</Card>
);
}
Example #18
Source File: Form.tsx From fe-v5 with Apache License 2.0 | 4 votes |
function FormCpt(props: ModalWrapProps & IProps) {
const { t } = useTranslation();
const { action, visible, initialValues, destroy, range, onOk, admin } = props;
const [form] = Form.useForm();
const [labels, setLabels] = useState<string[]>([]);
const [filteredLabels, setFilteredLabels] = useState<string[]>([]);
const [previewVisible, setPreviewVisible] = useState(false);
const [previewLoading, setPreviewLoading] = useState(false);
const [previewData, setPreviewData] = useState([]);
const [activeKey, setActiveKey] = useState('form');
const getLablesOptions = (_labels) => {
return _.map(_labels, (label) => {
return (
<Select.Option key={label} value={label}>
{label}
</Select.Option>
);
});
};
useEffect(() => {
getLabels('', range).then((res) => {
setLabels(res);
setFilteredLabels(res);
});
}, [JSON.stringify(range)]);
return (
<Modal
className='n9e-metric-views-modal'
title={
<Tabs className='custom-import-title' activeKey={activeKey} onChange={setActiveKey}>
<TabPane tab={titleMap[action]} key='form' />
{action === 'add' && <TabPane tab='导入快捷视图' key='import' />}
</Tabs>
}
visible={visible}
onCancel={() => {
destroy();
}}
onOk={() => {
form.validateFields().then((values) => {
let _values = _.cloneDeep(values);
if (activeKey === 'form') {
_values.dynamicLabels = _.map(_values.dynamicLabels, (item) => {
return {
label: item,
value: '',
};
});
_values.dimensionLabels = _.map(_values.dimensionLabels, (item) => {
return {
label: item,
value: '',
};
});
}
if (activeKey === 'import') {
try {
const config = JSON.parse(values.import);
_values = {
name: values.name,
cate: values.cate,
...config,
};
} catch (e) {
console.log(e);
return;
}
}
const { name, cate } = _values;
const configs = JSON.stringify(_.omit(_values, ['name', 'cate']));
const data: any = {
name,
cate: cate ? 0 : 1,
configs,
};
if (action === 'add') {
addMetricView(data).then((res) => {
message.success('添加成功');
onOk(res);
destroy();
});
} else if (action === 'edit') {
data.id = initialValues.id;
updateMetricView(data).then(() => {
message.success('修改成功');
onOk();
destroy();
});
}
});
}}
>
{activeKey === 'form' && (
<Form
layout='vertical'
initialValues={
initialValues || {
cate: false,
}
}
form={form}
onValuesChange={(changedValues, allValues) => {
if (changedValues.filters) {
const filtersStr = getFiltersStr(allValues.filters);
getLabels(`${filtersStr ? `{${filtersStr}}` : ''}`, range).then((res) => {
setFilteredLabels(res);
});
}
}}
>
<Form.Item label='视图名称' name='name' rules={[{ required: true }]}>
<Input />
</Form.Item>
{admin && (
<Form.Item label='是否公开' name='cate' rules={[{ required: true }]} valuePropName='checked'>
<Switch />
</Form.Item>
)}
<Form.List name='filters'>
{(fields, { add, remove }) => (
<>
<div style={{ paddingBottom: 8 }}>
前置过滤条件{' '}
<PlusCircleOutlined
onClick={() => {
add({
oper: '=',
});
}}
/>
</div>
{fields.map(({ key, name }) => {
return (
<Space key={key}>
<Form.Item name={[name, 'label']} rules={[{ required: true }]}>
<Select suffixIcon={<CaretDownOutlined />} allowClear showSearch style={{ width: 170 }}>
{getLablesOptions(labels)}
</Select>
</Form.Item>
<Form.Item name={[name, 'oper']} rules={[{ required: true }]}>
<Select suffixIcon={<CaretDownOutlined />} style={{ width: 60 }}>
<Select.Option value='='>=</Select.Option>
<Select.Option value='!='>!=</Select.Option>
<Select.Option value='=~'>=~</Select.Option>
<Select.Option value='!~'>!~</Select.Option>
</Select>
</Form.Item>
<Form.Item name={[name, 'value']} rules={[{ required: true }]}>
<Input style={{ width: 200 }} />
</Form.Item>
<Form.Item>
<MinusCircleOutlined
onClick={() => {
remove(name);
}}
/>
</Form.Item>
</Space>
);
})}
</>
)}
</Form.List>
<Form.Item label='动态过滤标签' name='dynamicLabels'>
<Select allowClear showSearch mode='multiple'>
{getLablesOptions(filteredLabels)}
</Select>
</Form.Item>
<Form.Item label='展开维度标签' name='dimensionLabels' rules={[{ required: true }]}>
<Select allowClear showSearch mode='multiple'>
{getLablesOptions(filteredLabels)}
</Select>
</Form.Item>
<div style={{ textAlign: 'right', marginBottom: 10 }}>
<Button
onClick={() => {
const values = form.getFieldsValue();
setPreviewVisible(true);
setPreviewLoading(true);
const filtersStr = getFiltersStr(values.filters);
const _labels = _.compact(_.concat(values.dynamicLabels, values.dimensionLabels));
const requests = _.map(_labels, (item) => {
return getLabelValues(item, range, filtersStr ? `{${filtersStr}}` : '');
});
Promise.all(requests).then((res) => {
const data = _.map(_labels, (item, idx) => {
return {
label: item,
values: res[idx],
};
});
setPreviewData(data);
setPreviewLoading(false);
});
}}
>
预览
</Button>
</div>
{previewVisible && (
<Table
size='small'
rowKey='label'
columns={[
{
title: 'Lable Key',
dataIndex: 'label',
},
{
title: 'Lable Value 数量',
dataIndex: 'values',
render: (text) => {
return text.length;
},
},
{
title: 'Lable Value 样例',
dataIndex: 'values',
render: (text) => {
return (
<Tooltip
placement='right'
title={
<div>
{_.map(text, (item) => {
return <div key={item}>{item}</div>;
})}
</div>
}
>{`${_.head(text)}...`}</Tooltip>
);
},
},
]}
dataSource={previewData}
loading={previewLoading}
/>
)}
</Form>
)}
{activeKey === 'import' && (
<Form
form={form}
preserve={false}
layout='vertical'
initialValues={
initialValues || {
cate: false,
}
}
>
<Form.Item label='视图名称' name='name' rules={[{ required: true }]}>
<Input />
</Form.Item>
{admin && (
<Form.Item label='是否公开' name='cate' rules={[{ required: true }]} valuePropName='checked'>
<Switch />
</Form.Item>
)}
<Form.Item
label='配置JSON:'
name='import'
rules={[
{
required: true,
message: t('请输入配置'),
validateTrigger: 'trigger',
},
]}
>
<Input.TextArea className='code-area' placeholder={t('请输入配置')} rows={4} />
</Form.Item>
</Form>
)}
</Modal>
);
}
Example #19
Source File: chartConfigModal.tsx From fe-v5 with Apache License 2.0 | 4 votes |
// 新增图表和编辑图表均在此组件
export default function ChartConfigModal(props: Props) {
const { t } = useTranslation();
const { busiId, groupId, show, onVisibleChange, initialValue, variableConfig, cluster, id } = props;
const layout = initialValue?.configs.layout;
const [innerVariableConfig, setInnerVariableConfig] = useState<VariableType | undefined>(variableConfig);
const [chartForm] = Form.useForm();
const [initialQL, setInitialQL] = useState([{ PromQL: '' }]);
const [legend, setLegend] = useState<boolean>(initialValue?.configs.legend || false);
const [step, setStep] = useState<number | null>(null);
const [highLevelConfig, setHighLevelConfig] = useState<HighLevelConfigType>(
initialValue?.configs.highLevelConfig || {
shared: true,
sharedSortDirection: 'desc',
precision: 'short',
formatUnit: 1000,
},
);
const [range, setRange] = useState<Range>({
start: 0,
end: 0,
});
useEffect(() => {
if (initialValue) {
chartForm.setFieldsValue(initialValue.configs);
setInitialQL(initialValue.configs.QL);
}
}, [initialValue]);
const handleAddChart = async (e) => {
try {
await chartForm.validateFields();
let formData: ChartConfig = Object.assign(
chartForm.getFieldsValue(),
{ legend, highLevelConfig },
{
version: 1, // Temporarily, hardcode 1
layout,
},
);
if (initialValue && initialValue.id) {
await updateCharts(busiId, [
{
configs: formData,
weight: 0,
group_id: groupId,
id: initialValue.id,
},
]);
} else {
await createChart(busiId, {
configs: JSON.stringify(formData),
weight: 0,
group_id: groupId,
});
}
onVisibleChange(true);
} catch (errorInfo) {
console.log('Failed:', errorInfo);
}
};
const PromqlEditorField = ({ onChange = (e: any) => {}, value = '', fields, remove, add, index, name }) => {
return (
<div style={{ display: 'flex', alignItems: 'center' }}>
<PromqlEditor
xCluster='Default'
onChange={onChange}
value={value}
style={{
width: '310px',
// flex: 1,
}}
/>
{fields.length > 1 ? (
<MinusCircleOutlined
style={{ marginLeft: 10 }}
onClick={() => {
remove(name);
}}
/>
) : null}
{index === fields.length - 1 && (
<PlusCircleOutlined
style={{ marginLeft: 10 }}
onClick={() => {
add();
}}
/>
)}
</div>
);
};
const handleVariableChange = (value) => {
setInnerVariableConfig(value);
};
const aggrFuncMenu = (
<Menu
onClick={(sort) => {
setHighLevelConfig({ ...highLevelConfig, sharedSortDirection: (sort as { key: 'desc' | 'asc' }).key });
}}
selectedKeys={[highLevelConfig.sharedSortDirection]}
>
<Menu.Item key='desc'>desc</Menu.Item>
<Menu.Item key='asc'>asc</Menu.Item>
</Menu>
);
const precisionMenu = (
<Menu
onClick={(precision) => {
const precisionKey = isNaN(Number(precision.key)) ? precision.key : Number(precision.key);
setHighLevelConfig({ ...highLevelConfig, formatUnit: precisionKey as 1024 | 1000 | 'humantime' });
}}
selectedKeys={[String(highLevelConfig.formatUnit)]}
>
<Menu.Item key={'1000'}>Ki, Mi, Gi by 1000</Menu.Item>
<Menu.Item key={'1024'}>Ki, Mi, Gi by 1024</Menu.Item>
<Menu.Item key={'humantime'}>Human time duration</Menu.Item>
</Menu>
);
const formatUnitInfoMap = {
1024: 'Ki, Mi, Gi by 1024',
1000: 'Ki, Mi, Gi by 1000',
humantime: 'Human time duration',
};
const getContent = () => {
const aggrFuncMenu = (
<Menu
onClick={(sort) => {
setHighLevelConfig({ ...highLevelConfig, sharedSortDirection: (sort as { key: 'desc' | 'asc' }).key });
}}
selectedKeys={[highLevelConfig.sharedSortDirection]}
>
<Menu.Item key='desc'>desc</Menu.Item>
<Menu.Item key='asc'>asc</Menu.Item>
</Menu>
);
const precisionMenu = (
<Menu
onClick={(precision) => {
const precisionKey = isNaN(Number(precision.key)) ? precision.key : Number(precision.key);
setHighLevelConfig({ ...highLevelConfig, formatUnit: precisionKey as 1024 | 1000 | 'humantime' });
}}
selectedKeys={[String(highLevelConfig.formatUnit)]}
>
<Menu.Item key={'1000'}>Ki, Mi, Gi by 1000</Menu.Item>
<Menu.Item key={'1024'}>Ki, Mi, Gi by 1024</Menu.Item>
<Menu.Item key={'humantime'}>Human time duration</Menu.Item>
</Menu>
);
return (
<div>
<Checkbox
checked={highLevelConfig.shared}
onChange={(e) => {
setHighLevelConfig({ ...highLevelConfig, shared: e.target.checked });
}}
>
Multi Series in Tooltip, order value
</Checkbox>
<Dropdown overlay={aggrFuncMenu}>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
{highLevelConfig.sharedSortDirection} <DownOutlined />
</a>
</Dropdown>
<br />
<Checkbox
checked={legend}
onChange={(e) => {
setLegend(e.target.checked);
}}
>
Show Legend
</Checkbox>
<br />
<Checkbox
checked={highLevelConfig.precision === 'short'}
onChange={(e) => {
setHighLevelConfig({ ...highLevelConfig, precision: e.target.checked ? 'short' : 'origin' });
}}
>
Value format with:{' '}
</Checkbox>
<Dropdown overlay={precisionMenu}>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
{formatUnitInfoMap[highLevelConfig.formatUnit]} <DownOutlined />
</a>
</Dropdown>
</div>
);
};
return (
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center' }}>
<div>{initialValue ? t('编辑图表') : t('新建图表')}</div>
<div style={{ flex: 1, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', fontSize: 12, lineHeight: '20px' }}>
<DateRangePicker onChange={(e) => setRange(e)} />
<Resolution onChange={(v) => setStep(v)} initialValue={step} />
<CloseOutlined
style={{ fontSize: 18 }}
onClick={() => {
onVisibleChange(false);
}}
/>
</div>
</div>
}
width={900}
visible={show}
destroyOnClose={true}
onOk={handleAddChart}
closable={false}
onCancel={() => {
onVisibleChange(false);
}}
>
<Form {...layout} form={chartForm} preserve={false}>
<Row>
<Col span={12}>
<VariableConfig onChange={handleVariableChange} value={innerVariableConfig} editable={false} cluster={cluster} range={range} id={id} />
<br />
<Form.Item
label={t('标题')}
name='name'
labelCol={{
span: 4,
}}
wrapperCol={{
span: 20,
}}
rules={[
{
required: true,
message: t('图表名称'),
},
]}
>
<Input />
</Form.Item>
<Form.Item label={t('下钻链接')} name='link' labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
<Input />
</Form.Item>
<Form.Item
wrapperCol={{
span: 24,
}}
style={{
marginBottom: '0px',
}}
>
<Form.List name='QL' initialValue={initialQL}>
{(fields, { add, remove }, { errors }) => {
return (
<>
{fields.length ? (
fields.map(({ key, name, fieldKey, ...restField }, index) => {
return (
<div key={name + fieldKey}>
<Form.Item
label='PromQL'
name={[name, 'PromQL']}
labelCol={{
span: 4,
}}
wrapperCol={{
span: 20,
}}
validateTrigger={['onBlur']}
rules={[
{
required: true,
message: t('请输入PromQL'),
},
]}
>
<PromqlEditorField key={name + fieldKey} name={name} fields={fields} index={index} remove={remove} add={add} />
</Form.Item>
<Form.Item
label='Legend'
name={[name, 'Legend']}
tooltip={{
getPopupContainer: () => document.body,
title:
'Controls the name of the time series, using name or pattern. For example {{hostname}} will be replaced with label value for the label hostname.',
}}
labelCol={{
span: 4,
}}
wrapperCol={{
span: 20,
}}
>
<Input />
</Form.Item>
</div>
);
})
) : (
<PlusCircleOutlined
onClick={() => {
add();
}}
/>
)}
<Form.ErrorList errors={errors} />
</>
);
}}
</Form.List>
</Form.Item>
<Row>
<Col span={11}>
<Form.Item label={t('预警值')} name='yplotline1' labelCol={{ span: 9 }} wrapperCol={{ span: 16 }}>
<InputNumber />
</Form.Item>
</Col>
<Col span={12} offset={1}>
<Form.Item label={t('危险值')} name='yplotline2' labelCol={{ span: 7 }} wrapperCol={{ span: 20 }}>
<InputNumber />
</Form.Item>
</Col>
{/* <Col span={23} offset={1}>
<Form.Item>
<Checkbox
checked={highLevelConfig.shared}
onChange={(e) => {
setHighLevelConfig({ ...highLevelConfig, shared: e.target.checked });
}}
>
Multi Series in Tooltip, order value
</Checkbox>
<Dropdown overlay={aggrFuncMenu}>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
{highLevelConfig.sharedSortDirection} <DownOutlined />
</a>
</Dropdown>
</Form.Item>
</Col>
<Col span={23} offset={1}>
<Form.Item>
<Checkbox
checked={legend}
onChange={(e) => {
setLegend(e.target.checked);
}}
>
Show Legend
</Checkbox>
</Form.Item>
</Col>
<Col span={23} offset={1}>
<Form.Item>
<Checkbox
checked={highLevelConfig.precision === 'short'}
onChange={(e) => {
setHighLevelConfig({ ...highLevelConfig, precision: e.target.checked ? 'short' : 'origin' });
}}
>
Value format with:{' '}
</Checkbox>
<Dropdown overlay={precisionMenu}>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
{formatUnitInfoMap[highLevelConfig.formatUnit]} <DownOutlined />
</a>
</Dropdown>
</Form.Item>
</Col> */}
</Row>
</Col>
<Col span={12}>
<Form.Item
wrapperCol={{ span: 22, offset: 2 }}
shouldUpdate={(prevValues, curValues) =>
prevValues.QL !== curValues.QL || prevValues.multi === curValues.multi || prevValues.legend === curValues.legend || prevValues.format === curValues.format
}
>
{({ getFieldsValue }) => {
const { QL = [], yplotline1, yplotline2 } = getFieldsValue();
const promqls = QL.filter((item) => item && item.PromQL).map((item) =>
innerVariableConfig ? replaceExpressionVars(item.PromQL, innerVariableConfig, innerVariableConfig.var.length, id) : item.PromQL,
);
const legendTitleFormats = QL.map((item) => item && item.Legend);
return (
<div className={legend ? 'graph-container graph-container-hasLegend' : 'graph-container'}>
<div className='graph-header' style={{ height: '35px', lineHeight: '35px', display: 'flex', justifyContent: 'space-between' }}>
<div>预览图表</div>
<div className='graph-extra'>
<span className='graph-operationbar-item' key='info'>
<Popover placement='left' content={getContent()} trigger='click' autoAdjustOverflow={false} getPopupContainer={() => document.body}>
<Button className='' type='link' size='small' onClick={(e) => e.preventDefault()}>
<SettingOutlined />
</Button>
</Popover>
</span>
</div>
</div>
<Graph
showHeader={false}
graphConfigInnerVisible={false}
highLevelConfig={highLevelConfig}
data={{
yAxis: {
plotLines: [
{
value: yplotline1 ? yplotline1 : undefined,
color: 'orange',
},
{
value: yplotline2 ? yplotline2 : undefined,
color: 'red',
},
],
},
legend: legend,
step,
range,
promqls,
legendTitleFormats,
}}
/>
</div>
);
// ) : null;
}}
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
);
}
Example #20
Source File: index.tsx From fe-v5 with Apache License 2.0 | 4 votes |
UserForm = React.forwardRef<ReactNode, UserAndPasswordFormProps>((props, ref) => {
const { t } = useTranslation();
const { userId } = props;
const [form] = Form.useForm();
const [initialValues, setInitialValues] = useState<User>();
const [loading, setLoading] = useState<boolean>(true);
const [contactsList, setContactsList] = useState<ContactsItem[]>([]);
const [roleList, setRoleList] = useState<{ name: string; note: string }[]>([]);
useImperativeHandle(ref, () => ({
form: form,
}));
useEffect(() => {
if (userId) {
getUserInfoDetail(userId);
} else {
setLoading(false);
}
getContacts();
getRoles().then((res) => setRoleList(res));
}, []);
const getContacts = () => {
getNotifyChannels().then((data: Array<ContactsItem>) => {
setContactsList(data);
});
};
const getUserInfoDetail = (id: string) => {
getUserInfo(id).then((data: User) => {
let contacts: Array<Contacts> = [];
if (data.contacts) {
Object.keys(data.contacts).forEach((item: string) => {
let val: Contacts = {
key: item,
value: data.contacts[item],
};
contacts.push(val);
});
}
setInitialValues(
Object.assign({}, data, {
contacts,
}),
);
setLoading(false);
});
};
return !loading ? (
<Form {...layout} form={form} initialValues={initialValues} preserve={false}>
{!userId && (
<Form.Item
label={t('用户名')}
name='username'
rules={[
{
required: true,
message: t('用户名不能为空!'),
},
]}
>
<Input />
</Form.Item>
)}
<Form.Item label={t('显示名')} name='nickname'>
<Input />
</Form.Item>
{!userId && (
<>
<Form.Item
name='password'
label={t('密码')}
rules={[
{
required: true,
message: t('请输入密码!'),
},
]}
hasFeedback
>
<Input.Password />
</Form.Item>
<Form.Item
name='confirm'
label={t('确认密码')}
dependencies={['password']}
hasFeedback
rules={[
{
required: true,
message: t('请确认密码!'),
},
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('password') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('密码不一致!'));
},
}),
]}
>
<Input.Password />
</Form.Item>
</>
)}
<Form.Item
label={t('角色')}
name='roles'
rules={[
{
required: true,
message: t('角色不能为空!'),
},
]}
>
<Select mode='multiple'>
{roleList.map((item, index) => (
<Option value={item.name} key={index}>
<div>
<div>{item.name}</div>
<div style={{ color: '#8c8c8c' }}>{item.note}</div>
</div>
</Option>
))}
</Select>
</Form.Item>
<Form.Item label={t('邮箱')} name='email'>
<Input />
</Form.Item>
<Form.Item label={t('手机')} name='phone'>
<Input />
</Form.Item>
<Form.Item label={t('更多联系方式')}>
<Form.List name='contacts'>
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, fieldKey, ...restField }) => (
<Space
key={key}
style={{
display: 'flex',
}}
align='baseline'
>
<Form.Item
style={{
width: '180px',
}}
{...restField}
name={[name, 'key']}
fieldKey={[fieldKey, 'key']}
rules={[
{
required: true,
message: t('联系方式不能为空'),
},
]}
>
<Select suffixIcon={<CaretDownOutlined />} placeholder={t('请选择联系方式')}>
{contactsList.map((item, index) => (
<Option value={item.key} key={index}>
{item.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
{...restField}
style={{
width: '170px',
}}
name={[name, 'value']}
fieldKey={[fieldKey, 'value']}
rules={[
{
required: true,
message: t('值不能为空'),
},
]}
>
<Input placeholder={t('请输入值')} />
</Form.Item>
<MinusCircleOutlined className='control-icon-normal' onClick={() => remove(name)} />
</Space>
))}
<PlusCircleOutlined className='control-icon-normal' onClick={() => add()} />
</>
)}
</Form.List>
</Form.Item>
</Form>
) : null;
})
Example #21
Source File: operateForm.tsx From fe-v5 with Apache License 2.0 | 4 votes |
OperateForm: React.FC<Props> = ({ detail = {}, type, tagsObj = {} }) => {
const btimeDefault = new Date().getTime();
const etimeDefault = new Date().getTime() + 1 * 60 * 60 * 1000; // 默认时长1h
const { t, i18n } = useTranslation();
const { clusters: clusterList } = useSelector<RootState, CommonStoreState>((state) => state.common);
const layout = {
labelCol: {
span: 24,
},
wrapperCol: {
span: 24,
},
};
const tailLayout = {
labelCol: {
span: 0,
},
wrapperCol: {
span: 24,
},
};
const [form] = Form.useForm(null as any);
const history = useHistory();
const [btnLoading, setBtnLoading] = useState<boolean>(false);
const [timeLen, setTimeLen] = useState('1h');
const { curBusiItem, busiGroups } = useSelector<RootState, CommonStoreState>((state) => state.common);
useEffect(() => {
const btime = form.getFieldValue('btime');
const etime = form.getFieldValue('etime');
if (!!etime && !!btime) {
const d = moment.duration(etime - btime).days();
const h = moment.duration(etime - btime).hours();
const m = moment.duration(etime - btime).minutes();
const s = moment.duration(etime - btime).seconds();
}
if (curBusiItem) {
form.setFieldsValue({ busiGroup: curBusiItem.id });
} else if (busiGroups.length > 0) {
form.setFieldsValue({ busiGroup: busiGroups[0].id });
} else {
message.warning('无可用业务组');
history.push('/alert-mutes');
}
return () => {};
}, [form]);
useEffect(() => {
// 只有add 的时候才传入tagsObj
if (tagsObj?.tags && tagsObj?.tags.length > 0) {
const tags = tagsObj?.tags?.map((item) => {
return {
...item,
value: item.func === 'in' ? item.value.split(' ') : item.value,
};
});
form.setFieldsValue({
tags: tags || [{}],
cluster: tagsObj.cluster,
});
}
}, [tagsObj]);
const timeChange = () => {
const btime = form.getFieldValue('btime');
const etime = form.getFieldValue('etime');
if (!!etime && !!btime) {
const d = Math.floor(moment.duration(etime - btime).asDays());
const h = Math.floor(moment.duration(etime - btime).hours());
const m = Math.floor(moment.duration(etime - btime).minutes());
const s = Math.floor(moment.duration(etime - btime).seconds());
const timeLen = `${d ? `${d}d ` : ''}${h ? `${h}h ` : ''}${m ? `${m}m ` : ''}${s ? `${s}s` : ''}`;
setTimeLen(timeLen);
}
};
const onFinish = (values) => {
setBtnLoading(true);
const tags = values?.tags?.map((item) => {
return {
...item,
value: Array.isArray(item.value) ? item.value.join(' ') : item.value,
};
});
const params = {
...values,
btime: moment(values.btime).unix(),
etime: moment(values.etime).unix(),
tags,
};
const curBusiItemId = form.getFieldValue('busiGroup');
addShield(params, curBusiItemId)
.then((_) => {
message.success(t('新建告警屏蔽成功'));
history.push('/alert-mutes');
})
.finally(() => {
setBtnLoading(false);
});
};
const onFinishFailed = () => {
setBtnLoading(false);
};
const timeLenChange = (val: string) => {
setTimeLen(val);
const time = new Date().getTime();
if (val === 'forever') {
const longTime = 7 * 24 * 3600 * 1000 * 10000;
form.setFieldsValue({
btime: moment(time),
etime: moment(time).add({
seconds: longTime,
}),
});
return;
}
const unit = val.charAt(val.length - 1);
const num = val.substr(0, val.length - 1);
form.setFieldsValue({
btime: moment(time),
etime: moment(time).add({
[unit]: num,
}),
});
};
const content = (
<Form
form={form}
{...layout}
layout='vertical'
className='operate-form'
onFinish={onFinish}
onFinishFailed={onFinishFailed}
initialValues={{
...detail,
btime: detail?.btime ? moment(detail.btime * 1000) : moment(btimeDefault),
etime: detail?.etime ? moment(detail.etime * 1000) : moment(etimeDefault),
cluster: clusterList[0] || 'Default',
}}
>
<Card>
<Form.Item label={t('业务组:')} name='busiGroup'>
<Select suffixIcon={<CaretDownOutlined />}>
{busiGroups?.map((item) => (
<Option value={item.id} key={item.id}>
{item.name}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t('生效集群:')}
name='cluster'
rules={[
{
required: true,
message: t('生效集群不能为空'),
},
]}
>
<Select suffixIcon={<CaretDownOutlined />}>
{clusterList?.map((item) => (
<Option value={item} key={item}>
{item}
</Option>
))}
</Select>
</Form.Item>
<Row gutter={10}>
<Col span={8}>
<Form.Item label={t('屏蔽开始时间:')} name='btime'>
<DatePicker showTime onChange={timeChange} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label={t('屏蔽时长:')}>
<Select suffixIcon={<CaretDownOutlined />} placeholder={t('请选择屏蔽时长')} onChange={timeLenChange} value={timeLen}>
{timeLensDefault.map((item: any, index: number) => (
<Option key={index} value={item.value}>
{item.value}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label={t('屏蔽结束时间:')} name='etime'>
<DatePicker showTime onChange={timeChange} />
</Form.Item>
</Col>
</Row>
<Row gutter={[10, 10]} style={{ marginBottom: '8px' }}>
<Col span={5}>
{t('屏蔽事件标签Key:')}
<Tooltip title={t(`这里的标签是指告警事件的标签,通过如下标签匹配规则过滤告警事件`)}>
<QuestionCircleFilled />
</Tooltip>
</Col>
<Col span={3}>{t('运算符:')}</Col>
<Col span={16}>{t('标签Value:')}</Col>
</Row>
<Form.List name='tags' initialValue={[{}]}>
{(fields, { add, remove }) => (
<>
{fields.map((field, index) => (
<TagItem field={field} key={index} remove={remove} form={form} />
))}
<Form.Item>
<PlusCircleOutlined className='control-icon-normal' onClick={() => add()} />
</Form.Item>
</>
)}
</Form.List>
{/* <Form.Item label={t('屏蔽时间')} name='time'>
<RangeDatePicker />
</Form.Item> */}
<Form.Item
label={t('屏蔽原因')}
name='cause'
rules={[
{
required: true,
message: t('请填写屏蔽原因'),
},
]}
>
<TextArea rows={3} />
</Form.Item>
<Form.Item {...tailLayout}>
<Row gutter={[10, 10]}>
<Col span={1}>
<Button type='primary' htmlType='submit'>
{type === 2 ? t('克隆') : t('创建')}
</Button>
</Col>
<Col
span={1}
style={{
marginLeft: 40,
}}
>
<Button onClick={() => window.history.back()}>{t('取消')}</Button>
</Col>
</Row>
</Form.Item>
</Card>
</Form>
);
return <div className='operate-form-index'>{content}</div>;
}
Example #22
Source File: FunctionDebuggerToolbar.tsx From next-basics with GNU General Public License v3.0 | 4 votes |
export function FunctionDebuggerToolbar({
type,
status,
saveDisabled,
onButtonClick,
}: FunctionDebuggerToolbarProps): React.ReactElement {
const refinedType = type ?? "input";
const isInput = refinedType === "input" || refinedType === "test-input";
const handleRunClick = useCallback(() => {
onButtonClick?.({ action: "run" });
}, [onButtonClick]);
const handleSaveClick = useCallback(() => {
if (!saveDisabled) {
onButtonClick?.({ action: "save" });
}
}, [onButtonClick, saveDisabled]);
const handleDeleteClick = useCallback(() => {
onButtonClick?.({ action: "delete" });
}, [onButtonClick]);
return (
<div
className={classNames(
styles.debuggerToolbar,
status && styles[status],
refinedType === "input" || refinedType === "output"
? styles.debug
: styles.test,
isInput ? styles.input : styles.output
)}
data-override-theme="dark"
>
<div className={styles.header}>
{refinedType === "input"
? "Input"
: refinedType === "test-input"
? "Test Input"
: refinedType === "test-output"
? "Expect Output"
: "Output"}
{isInput && (
<span className={styles.headerSuffix}>
(argument list in JSON format)
</span>
)}
</div>
{isInput ? (
<div className={styles.buttons}>
<Tooltip title="Run">
<div className={styles.debuggerButton} onClick={handleRunClick}>
<PlayCircleOutlined />
</div>
</Tooltip>
<Tooltip
title={refinedType === "input" ? "Add as a test case" : "Update"}
>
<div
className={classNames(styles.debuggerButton, {
[styles.disabled]: saveDisabled,
})}
onClick={handleSaveClick}
>
{refinedType === "input" ? (
<PlusCircleOutlined />
) : (
<SaveOutlined />
)}
</div>
</Tooltip>
{refinedType === "test-input" && (
<Tooltip title="Delete">
<div
className={styles.debuggerButton}
onClick={handleDeleteClick}
>
<DeleteOutlined />
</div>
</Tooltip>
)}
</div>
) : (
refinedType === "test-output" && (
<div className={styles.secondHeader}>
{status === "ok" ? (
<>
<span className={styles.secondHeaderIcon}>
<CheckOutlined />
</span>
<span>Test: passed</span>
</>
) : status === "failed" ? (
<>
<span className={styles.secondHeaderIcon}>
<CloseOutlined />
</span>
<span>Test: failed</span>
</>
) : (
<>
<span className={styles.secondHeaderIcon}>
<QuestionOutlined />
</span>
<span>Test: expired</span>
</>
)}
</div>
)
)}
</div>
);
}
Example #23
Source File: operateForm.tsx From fe-v5 with Apache License 2.0 | 4 votes |
operateForm: React.FC<Props> = ({ type, detail = {} }) => {
const { t, i18n } = useTranslation();
const history = useHistory(); // 创建的时候默认选中的值
const [form] = Form.useForm();
const { clusters: clusterList } = useSelector<RootState, CommonStoreState>((state) => state.common);
const { curBusiItem } = useSelector<RootState, CommonStoreState>((state) => state.common);
const [contactList, setInitContactList] = useState([]);
const [notifyGroups, setNotifyGroups] = useState<any[]>([]);
const [initVal, setInitVal] = useState<any>({});
const [refresh, setRefresh] = useState(true);
useEffect(() => {
getNotifyChannel();
getGroups('');
return () => {};
}, []);
useEffect(() => {
const data = {
...detail,
enable_time: detail?.enable_stime ? [detail.enable_stime, detail.enable_etime] : [],
enable_status: detail?.disabled === undefined ? true : !detail?.disabled,
};
setInitVal(data);
if (type == 1) {
const groups = (detail.notify_groups_obj ? detail.notify_groups_obj.filter((item) => !notifyGroups.find((i) => item.id === i.id)) : []).concat(notifyGroups);
setNotifyGroups(groups);
}
}, [JSON.stringify(detail)]);
const enableDaysOfWeekOptions = [t('周日'), t('周一'), t('周二'), t('周三'), t('周四'), t('周五'), t('周六')].map((v, i) => {
return <Option value={String(i)} key={i}>{`${v}`}</Option>;
});
const contactListCheckboxes = contactList.map((c: { key: string; label: string }) => (
<Checkbox value={c.key} key={c.label}>
{c.label}
</Checkbox>
));
const notifyGroupsOptions = notifyGroups.map((ng: any) => (
<Option value={String(ng.id)} key={ng.id}>
{ng.name}
</Option>
));
const getNotifyChannel = async () => {
const res = await getNotifiesList();
let contactList = res || [];
setInitContactList(contactList);
};
const getGroups = async (str) => {
const res = await getTeamInfoList({ query: str });
const data = res.dat || res;
const combineData = (detail.notify_groups_obj ? detail.notify_groups_obj.filter((item) => !data.find((i) => item.id === i.id)) : []).concat(data);
setNotifyGroups(combineData || []);
};
const addSubmit = () => {
form.validateFields().then(async (values) => {
const res = await prometheusQuery({ query: values.prom_ql }, values.cluster);
if (res.error) {
notification.error({
message: res.error,
});
return false;
}
const callbacks = values.callbacks.map((item) => item.url);
const data = {
...values,
enable_stime: values.enable_time[0].format('HH:mm'),
enable_etime: values.enable_time[1].format('HH:mm'),
disabled: !values.enable_status ? 1 : 0,
notify_recovered: values.notify_recovered ? 1 : 0,
enable_in_bg: values.enable_in_bg ? 1 : 0,
callbacks,
};
let reqBody,
method = 'Post';
if (type === 1) {
reqBody = data;
method = 'Put';
const res = await EditStrategy(reqBody, curBusiItem.id, detail.id);
if (res.err) {
message.error(res.error);
} else {
message.success(t('编辑成功!'));
history.push('/alert-rules');
}
} else {
reqBody = [data];
const { dat } = await addOrEditStrategy(reqBody, curBusiItem.id, method);
let errorNum = 0;
const msg = Object.keys(dat).map((key) => {
dat[key] && errorNum++;
return dat[key];
});
if (!errorNum) {
message.success(`${type === 2 ? t('告警规则克隆成功') : t('告警规则创建成功')}`);
history.push('/alert-rules');
} else {
message.error(t(msg));
}
}
});
};
const debounceFetcher = useCallback(debounce(getGroups, 800), []);
return (
<div className='operate_con'>
<Form
{...layout}
form={form}
className='strategy-form'
layout={refresh ? 'horizontal' : 'horizontal'}
initialValues={{
prom_eval_interval: 15,
prom_for_duration: 60,
severity: 2,
disabled: 0, // 0:立即启用 1:禁用 待修改
// notify_recovered: 1, // 1:启用
cluster: clusterList[0] || 'Default', // 生效集群
enable_days_of_week: ['1', '2', '3', '4', '5', '6', '0'],
...detail,
enable_in_bg: detail?.enable_in_bg === 1,
enable_time: detail?.enable_stime ? [moment(detail.enable_stime, 'HH:mm'), moment(detail.enable_etime, 'HH:mm')] : [moment('00:00', 'HH:mm'), moment('23:59', 'HH:mm')],
enable_status: detail?.disabled === undefined ? true : !detail?.disabled,
notify_recovered: detail?.notify_recovered === 1 || detail?.notify_recovered === undefined ? true : false, // 1:启用 0:禁用
recover_duration: detail?.recover_duration || 0,
callbacks: !!detail?.callbacks
? detail.callbacks.map((item) => ({
url: item,
}))
: [{}],
}}
>
<Space direction='vertical' style={{ width: '100%' }}>
<Card title={t('基本配置')}>
<Form.Item
label={t('规则标题:')}
name='name'
rules={[
{
required: true,
message: t('规则标题不能为空'),
},
]}
>
<Input placeholder={t('请输入规则标题')} />
</Form.Item>
<Form.Item
label={t('规则备注:')}
name='note'
rules={[
{
required: false,
},
]}
>
<Input placeholder={t('请输入规则备注')} />
</Form.Item>
<Form.Item
label={t('告警级别')}
name='severity'
rules={[
{
required: true,
message: t('告警级别不能为空'),
},
]}
>
<Radio.Group>
<Radio value={1}>{t('一级报警')}</Radio>
<Radio value={2}>{t('二级报警')}</Radio>
<Radio value={3}>{t('三级报警')}</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
label={t('生效集群')}
name='cluster'
rules={[
{
required: true,
message: t('生效集群不能为空'),
},
]}
>
<Select suffixIcon={<CaretDownOutlined />}>
{clusterList?.map((item) => (
<Option value={item} key={item}>
{item}
</Option>
))}
</Select>
</Form.Item>
<AdvancedWrap>
<AbnormalDetection form={form} />
</AdvancedWrap>
<Form.Item noStyle shouldUpdate={(prevValues, curValues) => prevValues.cluster !== curValues.cluster}>
{() => {
return (
<Form.Item label='PromQL' className={'Promeql-content'} required>
<Form.Item name='prom_ql' validateTrigger={['onBlur']} trigger='onChange' rules={[{ required: true, message: t('请输入PromQL') }]}>
<PromQLInput
url='/api/n9e/prometheus'
headers={{
'X-Cluster': form.getFieldValue('cluster'),
Authorization: `Bearer ${localStorage.getItem('access_token') || ''}`,
}}
/>
</Form.Item>
</Form.Item>
);
}}
</Form.Item>
<Form.Item required label={t('执行频率')}>
<Space>
<Form.Item
style={{ marginBottom: 0 }}
name='prom_eval_interval'
initialValue={15}
wrapperCol={{ span: 10 }}
rules={[
{
required: true,
message: t('执行频率不能为空'),
},
]}
>
<InputNumber
min={1}
onChange={(val) => {
setRefresh(!refresh);
}}
/>
</Form.Item>
秒
<Tooltip title={t(`每隔${form.getFieldValue('prom_eval_interval')}秒,把PromQL作为查询条件,去查询后端存储,如果查到了数据就表示当次有监控数据触发了规则`)}>
<QuestionCircleFilled />
</Tooltip>
</Space>
</Form.Item>
<Form.Item
required
label={t('持续时长')}
rules={[
{
required: true,
message: t('持续时长不能为空'),
},
]}
>
<Space>
<Form.Item style={{ marginBottom: 0 }} name='prom_for_duration' wrapperCol={{ span: 10 }}>
<InputNumber min={0} />
</Form.Item>
秒
<Tooltip
title={t(
`通常持续时长大于执行频率,在持续时长内按照执行频率多次执行PromQL查询,每次都触发才生成告警;如果持续时长置为0,表示只要有一次PromQL查询触发阈值,就生成告警`,
)}
>
<QuestionCircleFilled />
</Tooltip>
</Space>
</Form.Item>
<Form.Item label='附加标签' name='append_tags' rules={[{ required: false, message: '请填写至少一项标签!' }, isValidFormat]}>
<Select mode='tags' tokenSeparators={[' ']} open={false} placeholder={'标签格式为 key=value ,使用回车或空格分隔'} tagRender={tagRender} />
</Form.Item>
<Form.Item label={t('预案链接')} name='runbook_url'>
<Input />
</Form.Item>
</Card>
<Card title={t('生效配置')}>
<Form.Item
label={t('立即启用')}
name='enable_status'
rules={[
{
required: true,
message: t('立即启用不能为空'),
},
]}
valuePropName='checked'
>
<Switch />
</Form.Item>
<Form.Item
label={t('生效时间')}
name='enable_days_of_week'
rules={[
{
required: true,
message: t('生效时间不能为空'),
},
]}
>
<Select mode='tags'>{enableDaysOfWeekOptions}</Select>
</Form.Item>
<Form.Item
name='enable_time'
{...tailLayout}
rules={[
{
required: true,
message: t('生效时间不能为空'),
},
]}
>
<TimePicker.RangePicker
format='HH:mm'
onChange={(val, val2) => {
form.setFieldsValue({
enable_stime: val2[0],
enable_etime: val2[1],
});
}}
/>
</Form.Item>
<Form.Item label={t('仅在本业务组生效')} name='enable_in_bg' valuePropName='checked'>
<SwitchWithLabel label='根据告警事件中的ident归属关系判断' />
</Form.Item>
</Card>
<Card title={t('通知配置')}>
<Form.Item label={t('通知媒介')} name='notify_channels'>
<Checkbox.Group>{contactListCheckboxes}</Checkbox.Group>
</Form.Item>
<Form.Item label={t('告警接收组')} name='notify_groups'>
<Select mode='multiple' showSearch optionFilterProp='children' filterOption={false} onSearch={(e) => debounceFetcher(e)} onBlur={() => getGroups('')}>
{notifyGroupsOptions}
</Select>
</Form.Item>
<Form.Item label={t('启用恢复通知')}>
<Space>
<Form.Item name='notify_recovered' valuePropName='checked' style={{ marginBottom: 0 }}>
<Switch />
</Form.Item>
<Tooltip title={t(`告警恢复时也发送通知`)}>
<QuestionCircleFilled />
</Tooltip>
</Space>
</Form.Item>
<Form.Item label={t('留观时长')} required>
<Space>
<Form.Item style={{ marginBottom: 0 }} name='recover_duration' initialValue={0} wrapperCol={{ span: 10 }}>
<InputNumber
min={0}
onChange={(val) => {
setRefresh(!refresh);
}}
/>
</Form.Item>
秒
<Tooltip title={t(`持续${form.getFieldValue('recover_duration')}秒没有再次触发阈值才发送恢复通知`)}>
<QuestionCircleFilled />
</Tooltip>
</Space>
</Form.Item>
<Form.Item label={t('重复发送频率')} required>
<Space>
<Form.Item
style={{ marginBottom: 0 }}
name='notify_repeat_step'
initialValue={60}
wrapperCol={{ span: 10 }}
rules={[
{
required: true,
message: t('重复发送频率不能为空'),
},
]}
>
<InputNumber
min={0}
onChange={(val) => {
setRefresh(!refresh);
}}
/>
</Form.Item>
分钟
<Tooltip title={t(`如果告警持续未恢复,间隔${form.getFieldValue('notify_repeat_step')}分钟之后重复提醒告警接收组的成员`)}>
<QuestionCircleFilled />
</Tooltip>
</Space>
</Form.Item>
<Form.Item label={t('回调地址')}>
<Form.List name='callbacks' initialValue={[{}]}>
{(fields, { add, remove }) => (
<>
{fields.map((field) => (
<Row gutter={[10, 0]} key={field.key}>
<Col span={22}>
<Form.Item name={[field.name, 'url']} fieldKey={[field.fieldKey, 'url']}>
<Input />
</Form.Item>
</Col>
<Col span={1}>
<MinusCircleOutlined className='control-icon-normal' onClick={() => remove(field.name)} />
</Col>
</Row>
))}
<PlusCircleOutlined className='control-icon-normal' onClick={() => add()} />
</>
)}
</Form.List>
</Form.Item>
</Card>
<Form.Item
// {...tailLayout}
style={{
marginTop: 20,
}}
>
<Button type='primary' onClick={addSubmit} style={{ marginRight: '8px' }}>
{type === 1 ? t('编辑') : type === 2 ? t('克隆') : t('创建')}
</Button>
{type === 1 && (
<Button
danger
style={{ marginRight: '8px' }}
onClick={() => {
Modal.confirm({
title: t('是否删除该告警规则?'),
onOk: () => {
deleteStrategy([detail.id], curBusiItem.id).then(() => {
message.success(t('删除成功'));
history.push('/alert-rules');
});
},
onCancel() {},
});
}}
>
{t('删除')}
</Button>
)}
<Button
onClick={() => {
history.push('/alert-rules');
}}
>
{t('取消')}
</Button>
</Form.Item>
</Space>
</Form>
</div>
);
}
Example #24
Source File: operateForm.tsx From fe-v5 with Apache License 2.0 | 4 votes |
OperateForm: React.FC<Props> = ({ detail = {}, type }) => {
const { t, i18n } = useTranslation();
const { clusters: clusterList } = useSelector<RootState, CommonStoreState>((state) => state.common);
const layout = {
labelCol: {
span: 24,
},
wrapperCol: {
span: 24,
},
};
const tailLayout = {
labelCol: {
span: 24,
},
wrapperCol: {
span: 24,
},
};
const [form] = Form.useForm(null as any);
const history = useHistory();
const { curBusiItem } = useSelector<RootState, CommonStoreState>((state) => state.common);
const [btnLoading, setBtnLoading] = useState<boolean>(false);
const [ruleModalShow, setRuleModalShow] = useState<boolean>(false);
const [ruleCur, setRuleCur] = useState<any>();
const [contactList, setInitContactList] = useState([]);
const [littleAffect, setLittleAffect] = useState(true);
const [notifyGroups, setNotifyGroups] = useState<any[]>([]);
useEffect(() => {
getNotifyChannel();
getGroups('');
}, []);
useEffect(() => {
setRuleCur({
id: detail.rule_id || 0,
name: detail.rule_name,
});
}, [detail.rule_id]);
const notifyGroupsOptions = (detail.user_groups ? detail.user_groups.filter((item) => !notifyGroups.find((i) => item.id === i.id)) : []).concat(notifyGroups).map((ng: any) => (
<Option value={String(ng.id)} key={ng.id}>
{ng.name}
</Option>
));
const getNotifyChannel = async () => {
const res = await getNotifiesList();
let contactList = res || [];
setInitContactList(contactList);
};
const getGroups = async (str) => {
const res = await getTeamInfoList({ query: str });
const data = res.dat || res;
setNotifyGroups(data || []);
};
const debounceFetcher = useCallback(_.debounce(getGroups, 800), []);
const onFinish = (values) => {
setBtnLoading(true);
const tags = values?.tags?.map((item) => {
return {
...item,
value: Array.isArray(item.value) ? item.value.join(' ') : item.value,
};
});
const params = {
...values,
tags,
redefine_severity: values.redefine_severity ? 1 : 0,
redefine_channels: values.redefine_channels ? 1 : 0,
rule_id: ruleCur.id,
user_group_ids: values.user_group_ids ? values.user_group_ids.join(' ') : '',
new_channels: values.new_channels ? values.new_channels.join(' ') : '',
};
if (type === 1) {
editSubscribe([{ ...params, id: detail.id }], curBusiItem.id)
.then((_) => {
message.success(t('编辑订阅规则成功'));
history.push('/alert-subscribes');
})
.finally(() => {
setBtnLoading(false);
});
} else {
addSubscribe(params, curBusiItem.id)
.then((_) => {
message.success(t('新建订阅规则成功'));
history.push('/alert-subscribes');
})
.finally(() => {
setBtnLoading(false);
});
}
};
const onFinishFailed = () => {
setBtnLoading(false);
};
const chooseRule = () => {
setRuleModalShow(true);
};
const subscribeRule = (val) => {
setRuleModalShow(false);
setRuleCur(val);
form.setFieldsValue({
rile_id: val.id || 0,
});
};
return (
<>
<div className='operate-form-index' id={littleAffect ? 'littleAffect' : ''}>
<Form
form={form}
{...layout}
layout='vertical'
className='operate-form'
onFinish={onFinish}
onFinishFailed={onFinishFailed}
initialValues={{
...detail,
cluster: clusterList[0] || 'Default',
redefine_severity: detail?.redefine_severity ? true : false,
redefine_channels: detail?.redefine_channels ? true : false,
user_group_ids: detail?.user_group_ids ? detail?.user_group_ids?.split(' ') : [],
new_channels: detail?.new_channels?.split(' '),
}}
>
<Card>
<Form.Item
label={t('生效集群:')}
name='cluster'
rules={[
{
required: true,
message: t('生效集群不能为空'),
},
]}
>
<Select suffixIcon={<CaretDownOutlined />}>
{clusterList?.map((item, index) => (
<Option value={item} key={index}>
{item}
</Option>
))}
</Select>
</Form.Item>
<Form.Item label={t('订阅告警规则:')}>
{!!ruleCur?.id && (
<Button
type='primary'
ghost
style={{ marginRight: '8px' }}
onClick={() => {
ruleCur?.id && history.push(`/alert-rules/edit/${ruleCur?.id}`);
}}
>
{ruleCur?.name}
</Button>
)}
<EditOutlined style={{ cursor: 'pointer', fontSize: '18px' }} onClick={chooseRule} />
{!!ruleCur?.id && <DeleteOutlined style={{ cursor: 'pointer', fontSize: '18px', marginLeft: 5 }} onClick={() => subscribeRule({})} />}
</Form.Item>
<Row gutter={[10, 10]} style={{ marginBottom: '8px' }}>
<Col span={5}>
{t('订阅事件标签Key:')}
<Tooltip title={t(`这里的标签是指告警事件的标签,通过如下标签匹配规则过滤告警事件`)}>
<QuestionCircleFilled />
</Tooltip>
</Col>
<Col span={3}>{t('运算符:')}</Col>
<Col span={16}>{t('标签Value:')}</Col>
</Row>
<Form.List name='tags' initialValue={[{}]}>
{(fields, { add, remove }) => (
<>
{fields.map((field, index) => (
<TagItem field={field} fields={fields} key={index} remove={remove} add={add} form={form} />
))}
<Form.Item>
<PlusCircleOutlined className='control-icon-normal' onClick={() => add()} />
</Form.Item>
</>
)}
</Form.List>
<Form.Item label={t('告警级别:')} name='redefine_severity' valuePropName='checked'>
<Checkbox
value={1}
style={{ lineHeight: '32px' }}
onChange={(e) => {
form.setFieldsValue({
redefine_severity: e.target.checked ? 1 : 0,
});
setLittleAffect(!littleAffect);
}}
>
{t('重新定义')}
</Checkbox>
</Form.Item>
<Form.Item label={t('新的告警级别:')} name='new_severity' initialValue={2} style={{ display: form.getFieldValue('redefine_severity') ? 'block' : 'none' }}>
<Radio.Group>
<Radio key={1} value={1}>
{t('一级报警')}
</Radio>
<Radio key={2} value={2}>
{t('二级报警')}
</Radio>
<Radio key={3} value={3}>
{t('三级报警')}
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label={t('通知媒介:')} name='redefine_channels' valuePropName='checked'>
<Checkbox
value={1}
style={{ lineHeight: '32px' }}
onChange={(e) => {
form.setFieldsValue({
redefine_channels: e.target.checked ? 1 : 0,
});
setLittleAffect(!littleAffect);
}}
>
{t('重新定义')}
</Checkbox>
</Form.Item>
<Form.Item label={t('新的通知媒介:')} name='new_channels' style={{ display: form.getFieldValue('redefine_channels') ? 'block' : 'none' }}>
<Checkbox.Group>
{contactList.map((c: { key: string; label: string }) => (
<Checkbox value={c.key} key={c.label}>
{c.label}
</Checkbox>
))}
</Checkbox.Group>
</Form.Item>
<Form.Item label={t('订阅告警接收组:')} name='user_group_ids' rules={[{ required: true, message: t('告警接收组不能为空') }]}>
<Select mode='multiple' showSearch optionFilterProp='children' filterOption={false} onSearch={(e) => debounceFetcher(e)} onBlur={() => getGroups('')}>
{notifyGroupsOptions}
</Select>
</Form.Item>
<Form.Item {...tailLayout}>
<Button type='primary' htmlType='submit' style={{ marginRight: '8px' }}>
{type === 1 ? t('编辑') : type === 2 ? t('克隆') : t('创建')}
</Button>
{type === 1 && (
<Button
danger
style={{ marginRight: '8px' }}
onClick={() => {
Modal.confirm({
title: t('是否删除该告警规则?'),
onOk: () => {
detail?.id &&
deleteSubscribes({ ids: [detail.id] }, curBusiItem.id).then(() => {
message.success(t('删除成功'));
history.push('/alert-subscribes');
});
},
onCancel() {},
});
}}
>
{t('删除')}
</Button>
)}
<Button onClick={() => window.history.back()}>{t('取消')}</Button>
</Form.Item>
</Card>
</Form>
<RuleModal
visible={ruleModalShow}
ruleModalClose={() => {
setRuleModalShow(false);
}}
subscribe={subscribeRule}
/>
</div>
</>
);
}
Example #25
Source File: index.tsx From condo with MIT License | 4 votes |
ContactsEditor: React.FC<IContactEditorProps> = (props) => {
const intl = useIntl()
const FullNameLabel = intl.formatMessage({ id: 'contact.Contact.ContactsEditor.Name' })
const PhoneLabel = intl.formatMessage({ id: 'contact.Contact.ContactsEditor.Phone' })
const AddNewContactLabel = intl.formatMessage({ id: 'contact.Contact.ContactsEditor.AddNewContact' })
const AnotherContactLabel = intl.formatMessage({ id: 'contact.Contact.ContactsEditor.AnotherContact' })
const CannotCreateContactMessage = intl.formatMessage({ id: 'contact.Contact.ContactsEditor.CannotCreateContact' })
const TicketFromResidentMessage = intl.formatMessage({ id: 'pages.condo.ticket.title.TicketFromResident' })
const TicketNotFromResidentMessage = intl.formatMessage({ id: 'pages.condo.ticket.title.TicketNotFromResident' })
const { form, fields, value: initialValue, onChange, organization, role, property, unitName, unitType, allowLandLine } = props
const isNotContact = useMemo(() => !initialValue.id && initialValue.phone, [initialValue.id, initialValue.phone])
const [selectedContact, setSelectedContact] = useState(null)
const [value, setValue] = useState(initialValue)
const [editableFieldsChecked, setEditableFieldsChecked] = useState(false)
// We need this to keep manually typed information preserved between rerenders
// with different set of prefetched contacts. For example, when a different unitName is selected,
// manually typed information should not be lost.
const [manuallyTypedContact, setManuallyTypedContact] = useState({ id: undefined, name: '', phone: '' })
const [displayEditableContactFields, setDisplayEditableContactFields] = useState(false)
const [isInitialContactsLoaded, setIsInitialContactsLoaded] = useState<boolean>()
const [initialContacts, setInitialContacts] = useState<IContactUIState[]>([])
const initialContactsQuery = useMemo(() => ({
organization: { id: organization },
property: { id: property ? property : null },
unitName: unitName ? unitName : undefined,
unitType: unitType ? unitType : undefined,
}), [organization, property, unitName, unitType])
const initialEmployeesQuery = useMemo(() => ({
organization: { id: organization },
}), [organization])
const {
objs: fetchedContacts,
loading: contactsLoading,
error,
refetch: refetchContacts,
} = Contact.useObjects({
where: initialContactsQuery,
first: 100,
})
const {
objs: fetchedEmployees,
refetch: refetchEmployees,
} = OrganizationEmployee.useObjects({
where: initialEmployeesQuery,
first: 100,
})
const { phoneValidator } = useValidations({ allowLandLine })
const validations = {
phone: [phoneValidator],
}
useEffect(() => {
if (!isInitialContactsLoaded && !contactsLoading) {
setInitialContacts(fetchedContacts)
setIsInitialContactsLoaded(true)
}
}, [contactsLoading, fetchedContacts, isInitialContactsLoaded])
// It's not enough to have `value` props of `Input` set.
useEffect(() => {
if (initialValue) {
form.setFieldsValue({
[fields.id]: initialValue.id,
[fields.name]: initialValue.name,
[fields.phone]: initialValue.phone,
})
}
}, [])
// When `unitName` was changed from outside, selection is not relevant anymore
useEffect(() => {
setIsInitialContactsLoaded(false)
setSelectedContact(null)
setManuallyTypedContact(null)
}, [unitName, unitType])
const handleClickOnPlusButton = () => {
setDisplayEditableContactFields(true)
setSelectedContact(null)
setEditableFieldsChecked(true)
}
const handleClickOnMinusButton = () => {
setDisplayEditableContactFields(false)
setSelectedContact(fetchedContacts[0])
setEditableFieldsChecked(false)
}
const triggerOnChange = (contact: ContactValue, isNew) => {
form.setFieldsValue({
[fields.id]: contact.id,
[fields.name]: contact.name,
[fields.phone]: contact.phone,
})
setValue(contact)
setSelectedContact(contact)
onChange && onChange(contact, isNew)
}
const handleSelectContact = (contact) => {
setSelectedContact(contact)
setEditableFieldsChecked(false)
triggerOnChange(contact, false)
}
const handleChangeContact = debounce((contact) => {
// User can manually type phone and name, that will match already existing contact,
// so, it should be connected with ticket
const fetchedContact = find(fetchedContacts, { ...contact, unitName: unitName || null })
const contactToSet = fetchedContact || contact
triggerOnChange(contactToSet, !fetchedContact)
setManuallyTypedContact(contact)
setEditableFieldsChecked(true)
setSelectedContact(null)
}, DEBOUNCE_TIMEOUT)
const handleSyncedFieldsChecked = () => {
setSelectedContact(null)
setEditableFieldsChecked(true)
if (isNotContact) {
handleChangeContact(initialValue)
}
}
const handleChangeEmployee = debounce((contact) => {
form.setFieldsValue({
[fields.id]: null,
[fields.name]: contact.name,
[fields.phone]: contact.phone,
})
const employeeContact = { ...contact, id: null }
setValue(employeeContact)
setManuallyTypedContact(employeeContact)
setEditableFieldsChecked(true)
onChange && onChange(employeeContact, false)
}, DEBOUNCE_TIMEOUT)
const initialValueIsPresentedInFetchedContacts = useMemo(() => initialContacts
&& initialValue && initialValue.name && initialValue.phone &&
initialContacts.find(contact => contact.name === initialValue.name && contact.phone === initialValue.phone),
[initialContacts, initialValue])
const isContactSameAsInitial = (contact) => (
initialValue && initialValue.name === contact.name && initialValue.phone === contact.phone && initialValue.id === contact.id
)
const isContactSelected = useCallback((contact) => {
if (selectedContact) return selectedContact.id === contact.id
if (!editableFieldsChecked) {
if (isContactSameAsInitial(contact)) return true
}
return false
}, [editableFieldsChecked, isContactSameAsInitial, selectedContact])
const contactOptions = useMemo(() => initialContacts.map((contact) => (
<ContactOption
key={contact.id}
contact={contact}
onSelect={handleSelectContact}
selected={isContactSelected(contact)}
/>
)), [handleSelectContact, initialContacts, isContactSelected])
const handleTabChange = useCallback((tab) => {
setSelectedContact(null)
setEditableFieldsChecked(false)
if (tab === CONTACT_EDITOR_TABS.NOT_FROM_RESIDENT) {
handleChangeEmployee(value)
}
}, [handleChangeEmployee, value])
const className = props.disabled ? 'disabled' : ''
if (error) {
console.warn(error)
throw error
}
return (
<Col span={24}>
<ContactsInfoFocusContainer className={className}>
<Tabs
defaultActiveKey={isNotContact ? CONTACT_EDITOR_TABS.NOT_FROM_RESIDENT : CONTACT_EDITOR_TABS.FROM_RESIDENT}
style={TABS_STYLE}
onChange={handleTabChange}
>
<TabPane tab={TicketFromResidentMessage} key={CONTACT_EDITOR_TABS.FROM_RESIDENT}>
<Row gutter={TAB_PANE_ROW_GUTTERS}>
<Labels
left={PhoneLabel}
right={FullNameLabel}
/>
{isEmpty(initialContacts) || !unitName ? (
<ContactSyncedAutocompleteFields
refetch={refetchContacts}
initialQuery={initialContactsQuery}
initialValue={initialValue.id ? initialValue : manuallyTypedContact}
onChange={handleChangeContact}
contacts={fetchedContacts}
/>
) : (
<>
{contactOptions}
<>
{(displayEditableContactFields || (initialValue.id && !initialValueIsPresentedInFetchedContacts)) ? (
<>
<Labels
left={AnotherContactLabel}
/>
<ContactSyncedAutocompleteFields
initialQuery={initialContactsQuery}
refetch={refetchContacts}
initialValue={initialValue.id ? initialValue : manuallyTypedContact}
onChange={handleChangeContact}
onChecked={handleSyncedFieldsChecked}
checked={editableFieldsChecked}
contacts={fetchedContacts}
displayMinusButton={true}
onClickMinusButton={handleClickOnMinusButton}
/>
{(!get(role, 'canManageContacts')) && (
<Col span={24}>
<ErrorsWrapper>
{CannotCreateContactMessage}
</ErrorsWrapper>
</Col>
)}
</>
) : (
<Col span={24}>
<Button
type="link"
style={BUTTON_STYLE}
onClick={handleClickOnPlusButton}
icon={<PlusCircleOutlined style={BUTTON_ICON_STYLE}/>}
>
{AddNewContactLabel}
</Button>
</Col>
)}
</>
</>
)}
</Row>
</TabPane>
<TabPane
tab={TicketNotFromResidentMessage}
key={CONTACT_EDITOR_TABS.NOT_FROM_RESIDENT}
>
<Row gutter={TAB_PANE_ROW_GUTTERS}>
<Labels
left={PhoneLabel}
right={FullNameLabel}
/>
<ContactSyncedAutocompleteFields
initialQuery={initialEmployeesQuery}
refetch={refetchEmployees}
initialValue={!initialValue.id ? initialValue : manuallyTypedContact}
onChange={handleChangeEmployee}
contacts={fetchedEmployees}
/>
</Row>
</TabPane>
</Tabs>
</ContactsInfoFocusContainer>
{/*
This is a place for items of external form, this component is embedded into.
Why not to use them in place of actual inputs?
Because we have many inputs ;)
1. Input pairs, imitating radio group for select
2. Text inputs for manual typing
Logic of displaying `Form.Item`, depending on what is currently selected:
radio-like pair, or manual input pair, — will be complex.
The simplest solution, i currently know, — is to keep it in one place.
So, we use hidden inputs here, but reveal validation errors.
*/}
<Row gutter={TAB_PANE_ROW_GUTTERS}>
<Col span={10}>
<Form.Item name={fields.id} hidden>
<Input value={get(value, 'id')}/>
</Form.Item>
<ErrorContainerOfHiddenControl>
<Form.Item
name={fields.phone}
validateFirst
rules={validations.phone}>
<Input value={get(value, 'phone')}/>
</Form.Item>
</ErrorContainerOfHiddenControl>
</Col>
<Col span={10}>
<ErrorContainerOfHiddenControl>
<Form.Item name={fields.name}>
<Input value={get(value, 'name')}/>
</Form.Item>
</ErrorContainerOfHiddenControl>
</Col>
<Col span={2}/>
<Col span={2}/>
</Row>
</Col>
)
}
Example #26
Source File: index.tsx From dashboard with Apache License 2.0 | 4 votes |
AutoReply: React.FC<AutoReplyProps> = (props) => {
const {welcomeMsg, setWelcomeMsg, isFetchDone} = props;
const [modalVisible, setModalVisible] = useState(false);
const [attachments, setAttachments] = useState<Attachment[]>([]);
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [currentMode, setCurrentMode] = useState<MsgType>('image');
const [linkFetching, setLinkFetching] = useState(false);
const [content, setContent] = useState('');
const contentRef = useRef<React.RefObject<HTMLElement>>();
const imageModalFormRef = useRef<FormInstance>();
const linkModalFormRef = useRef<FormInstance>();
const miniAppModalFormRef = useRef<FormInstance>();
const UploadFileFn = async (req: UploadRequestOption, ref: MutableRefObject<any | undefined>, inputName: string) => {
const file = req.file as File;
if (!file.name) {
message.error('非法参数');
return;
}
const hide = message.loading('上传中');
try {
const res = await GetSignedURL(file.name)
const data = res.data as GetSignedURLResult
if (res.code === 0) {
const uploadRes = (await fetch(data.upload_url, {
method: 'PUT',
body: file
}));
hide();
if (uploadRes.ok && ref) {
ref.current?.setFieldsValue({[inputName]: data.download_url});
return;
}
message.error('上传图片失败');
return;
}
hide();
message.error('获取上传地址失败');
return;
} catch (e) {
message.error('上传图片失败');
console.log(e);
}
};
useEffect(() => {
const formData = itemDataToFormData(welcomeMsg);
setAttachments(formData.attachments || []);
setContent(formData.text || '');
}, [isFetchDone]);
useEffect(() => {
setWelcomeMsg({
text: content || '',
attachments: attachments || [],
});
}, [content, attachments]);
return (
<>
<div className={styles.replyEditor}>
<div className={'preview-container'}>
<div className={styles.replyEditorPreview}>
<img src={phoneImage} className='bg'/>
<div className='content'>
<ul className='reply-list'>
{content && (
<li><img
src={avatarDefault}/>
<div className='msg text' dangerouslySetInnerHTML={{__html: content}}/>
</li>
)}
{attachments && attachments.length > 0 && (
attachments.map((attachment) => {
if (attachment.msgtype === 'image') {
return (
<li key={attachment.id}>
<img src={avatarDefault}/>
<div className={`msg image`}>
<img src={attachment.image?.pic_url}/>
</div>
</li>
);
}
if (attachment.msgtype === 'link') {
return (
<li key={attachment.id}>
<img src={avatarDefault}/>
<div className='msg link'><p className='title'>{attachment.link?.title}</p>
<div className='link-inner'><p
className='desc'>{attachment.link?.desc}</p>
<img src={attachment.link?.picurl}/>
</div>
</div>
</li>
);
}
if (attachment.msgtype === 'miniprogram') {
return (
<li key={attachment.id}>
<img src={avatarDefault}/>
<div className='msg miniprogram'>
<p className='m-title'>
<IconFont
type={'icon-weixin-mini-app'}
style={{marginRight: 4, fontSize: 14}}
/>
{attachment.miniprogram?.title}
</p>
<img src={attachment.miniprogram?.pic_media_id}/>
<p className='l-title'>
<IconFont type={'icon-weixin-mini-app'} style={{marginRight: 4}}/>
小程序
</p>
</div>
</li>
);
}
return '';
})
)}
</ul>
</div>
</div>
</div>
<div className='text-area-container'>
<div className={styles.msgTextareaContainer} style={{border: 'none'}}>
{props.enableQuickInsert && (
<div className='insert-btn '>
<span
className='clickable no-select'
onClick={() => {
setContent(`${content}[客户昵称]`);
}}
>[插入客户昵称]</span>
</div>
)}
<div className='textarea-container '>
<ContentEditable
// @ts-ignore
innerRef={contentRef}
onKeyDown={(event) => {
if (event.key === 'Enter') {
document.execCommand('insertLineBreak');
event.preventDefault();
}
}}
className={'textarea'}
html={content}
onChange={(e) => {
setContent(e.target.value);
}}/>
<div className='flex-row align-side'>
<p className='text-cnt'>{content.length}/600</p>
</div>
</div>
</div>
</div>
<div className='option-area-container'>
{attachments && attachments.length > 0 && (
<ReactSortable handle={'.draggable-button'} tag='ul' className={'select-msg-options'} list={attachments} setList={setAttachments}>
{attachments.map((attachment, index) => (
<li key={attachment.id} className='flex-row'>
<span>
<MinusCircleOutlined
onClick={() => {
const items = [...attachments];
items.splice(index, 1);
setAttachments(items);
}}
/>
【{msgTypes[attachment.msgtype]}】:
<span
className='col-1'>{attachment?.name}</span>
</span>
<span className='d-action-container'>
<EditOutlined
onClick={() => {
setCurrentMode(attachment.msgtype);
imageModalFormRef.current?.setFieldsValue(attachment.image);
linkModalFormRef.current?.setFieldsValue(attachment.link);
miniAppModalFormRef.current?.setFieldsValue(attachment.miniprogram);
setCurrentIndex(index);
setModalVisible(true);
}}
/>
<DragOutlined
className={'draggable-button'}
style={{cursor: 'grabbing'}}
/>
</span>
</li>
))}
</ReactSortable>
)}
<div className='option-container'>
<Dropdown
placement='topLeft'
trigger={['click']}
overlay={(
<Menu style={{minWidth: 120}}>
<Menu.Item
key={'image'}
icon={<FileImageOutlined/>}
onClick={() => {
setCurrentMode('image');
setCurrentIndex(attachments.length);
imageModalFormRef.current?.resetFields();
setModalVisible(true);
}}
>
图片
</Menu.Item>
<Menu.Item
key={'link'}
icon={<LinkOutlined/>}
onClick={() => {
setCurrentMode('link');
setCurrentIndex(attachments.length);
setModalVisible(true);
}}
>
链接
</Menu.Item>
<Menu.Item
key={'miniApp'}
icon={<IconFont type={'icon-weixin-mini-app'}/>}
onClick={() => {
setCurrentMode('miniprogram');
setCurrentIndex(attachments.length);
setModalVisible(true);
}}
>
小程序
</Menu.Item>
</Menu>
)}
>
<a className='ant-dropdown-link' onClick={e => e.preventDefault()}>
<PlusCircleOutlined/> 添加附件
</a>
</Dropdown>
</div>
</div>
</div>
<ModalForm
formRef={imageModalFormRef}
className={'dialog from-item-label-100w'}
layout={'horizontal'}
width={'560px'}
visible={currentMode === 'image' && modalVisible}
onVisibleChange={setModalVisible}
onFinish={async (params: { title: string, pic_url: string, msgtype: MsgType }) => {
attachments[currentIndex] = {
id: new Date().getTime().toString(),
msgtype: params.msgtype,
name: params.title,
image: {...params},
};
setAttachments(attachments);
return true;
}}
>
<h2 className='dialog-title'> 添加图片附件 </h2>
<ProForm.Item initialValue={'image'} name={'msgtype'} noStyle={true}>
<input type={'hidden'}/>
</ProForm.Item>
<ProFormText
name='title'
label='图片名称'
placeholder={'请输入图片名称'}
width='md'
rules={[
{
required: true,
message: '请输入图片名称!',
},
]}
/>
<Form.Item
label='上传图片'
name='pic_url'
rules={[
{
required: true,
message: '请上传图片!',
},
]}
>
<ImageUploader
customRequest={async (req) => {
await UploadFileFn(req, imageModalFormRef, 'pic_url')
}}
/>
</Form.Item>
</ModalForm>
<ModalForm
formRef={linkModalFormRef}
className={'dialog from-item-label-100w'}
layout={'horizontal'}
width={'560px'}
visible={currentMode === 'link' && modalVisible}
onVisibleChange={setModalVisible}
onFinish={async (params) => {
attachments[currentIndex] = {
id: new Date().getTime().toString(),
msgtype: params.msgtype,
name: params.title,
// @ts-ignore
link: {...params},
};
setAttachments(attachments);
return true;
}}
>
<Spin spinning={linkFetching}>
<h2 className='dialog-title'> 添加链接附件 </h2>
<ProForm.Item initialValue={'link'} name={'msgtype'} noStyle={true}>
<input type={'hidden'}/>
</ProForm.Item>
<ProFormText
name='url'
label='链接地址'
width='md'
fieldProps={{
disabled: linkFetching,
addonAfter: (
<Tooltip title="点击抓取远程链接,自动填充标题,描述,图片">
<div
onClick={async () => {
setLinkFetching(true);
const res = await ParseURL(linkModalFormRef.current?.getFieldValue('url'))
setLinkFetching(false);
if (res.code !== 0) {
message.error(res.message);
} else {
message.success('解析链接成功');
linkModalFormRef?.current?.setFieldsValue({
customer_link_enable: 1,
title: res.data.title,
desc: res.data.desc,
picurl: res.data.img_url,
})
}
}}
style={{
cursor: "pointer",
width: 32,
height: 30,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<SyncOutlined/>
</div>
</Tooltip>
)
}}
rules={[
{
required: true,
message: '请输入链接地址',
},
{
type: 'url',
message: '请填写正确的的URL,必须是http或https开头',
},
]}
/>
<ProFormSwitch
label={'高级设置'}
checkedChildren='开启'
unCheckedChildren='关闭'
name='customer_link_enable'
tooltip={'开启后可以自定义链接所有信息'}
/>
<ProFormDependency name={['customer_link_enable']}>
{({customer_link_enable}) => {
if (customer_link_enable) {
return (
<>
<ProFormText
name='title'
label='链接标题'
width='md'
rules={[
{
required: true,
message: '请输入链接标题',
},
]}
/>
<ProFormTextArea
name='desc'
label='链接描述'
width='md'
/>
<Form.Item
label='链接封面'
name='picurl'
rules={[
{
required: true,
message: '请上传链接图片!',
},
]}
>
<ImageUploader
customRequest={async (req) => {
await UploadFileFn(req, linkModalFormRef, 'picurl')
}}
/>
</Form.Item>
</>
);
}
return <></>;
}}
</ProFormDependency>
</Spin>
</ModalForm>
<ModalForm
formRef={miniAppModalFormRef}
className={'dialog from-item-label-100w'}
layout={'horizontal'}
width={'560px'}
labelCol={{
md: 6,
}}
visible={currentMode === 'miniprogram' && modalVisible}
onVisibleChange={setModalVisible}
onFinish={async (params) => {
attachments[currentIndex] = {
id: new Date().getTime().toString(),
msgtype: params.msgtype,
name: params.title,
// @ts-ignore
miniprogram: {...params},
};
setAttachments(attachments);
return true;
}}
>
<h2 className='dialog-title'> 添加小程序附件 </h2>
<Alert
showIcon={true}
type='info'
message={
'请填写企业微信后台绑定的小程序id和路径,否则会造成发送失败'
}
style={{marginBottom: 20}}
/>
<ProForm.Item initialValue={'miniprogram'} name={'msgtype'} noStyle={true}>
<input type={'hidden'}/>
</ProForm.Item>
<ProFormText
name='title'
label='小程序标题'
width='md'
rules={[
{
required: true,
message: '请输入链接标题',
},
]}
/>
<ProFormText
// 帮助指引
name='app_id'
label='小程序AppID'
width='md'
rules={[
{
required: true,
message: '请输入小程序AppID',
},
]}
/>
<ProFormText
name='page'
label='小程序路径'
width='md'
rules={[
{
required: true,
message: '请输入小程序路径',
},
]}
/>
<Form.Item
label='小程序封面'
name='pic_media_id'
rules={[
{
required: true,
message: '请小程序封面!',
},
]}
>
<ImageUploader
customRequest={async (req) => {
await UploadFileFn(req, miniAppModalFormRef, 'pic_media_id')
}}
/>
</Form.Item>
</ModalForm>
</>
);
}
Example #27
Source File: OrderForm.tsx From mayoor with MIT License | 4 votes |
OrderForm: React.FC<Props> = (props) => {
const { t } = useTranslation();
const { currencyFormatter } = useCurrencyFormatter();
return (
<Formik<OrderFormValues>
initialValues={props.initialValues}
onSubmit={async (values, { resetForm }) => {
await props.onSubmit(values, resetForm);
}}
validationSchema={getOrderValidationSchema(t)}
enableReinitialize
>
{({ handleSubmit, values, handleChange, setFieldValue }) => (
<StyledForm onSubmit={handleSubmit}>
<Row gutter={8}>
<Col lg={4}>
<StyledOrderNumberWrapper>
<FormInput
name="number"
label={t('Order number')}
icon={<NumberOutlined />}
withLabel
type="number"
disabled={!props.isNumberEditable}
/>
</StyledOrderNumberWrapper>
</Col>
<Col lg={7}>
<CustomerPicker extraCustomer={props.extraCustomer} />
</Col>
<Col lg={6}>
<OrderStatusSelect />
</Col>
</Row>
<StyledDivider />
<Row gutter={6}>
<Col sm={4}>
<StyledLabel>{t('Material')}</StyledLabel>
</Col>
<Col sm={7}>
<StyledLabel>{t('Name')}</StyledLabel>
</Col>
<Col sm={2}>
<StyledLabel>{t('Width')}</StyledLabel>
</Col>
<Col sm={2}>
<StyledLabel>{t('Height')}</StyledLabel>
</Col>
<Col sm={2}>
<StyledLabel>{t('Pieces')}</StyledLabel>
</Col>
<Col sm={1}></Col>
<Col sm={3}>
<StyledLabel>{t('Price')}</StyledLabel>
</Col>
<Col sm={3}>
<StyledLabel>{t('Tax')}</StyledLabel>
</Col>
</Row>
<FieldArray
name="items"
render={(arrayHelpers) => (
<>
{values.items.length > 0 &&
values.items.map((item, index) => (
<OrderItemField
key={item.id || index}
index={index}
arrayHelpers={arrayHelpers}
/>
))}
<Row>
<Col>
<Button
icon={<PlusCircleOutlined />}
onClick={() => arrayHelpers.push(dummyMaterialItem)}
>
{t('Add item')}
</Button>
</Col>
<Col style={{ textAlign: 'right' }}>
<Button
icon={<CalculatorOutlined />}
onClick={() => {
const { totalPrice, totalTax } = calculateSummary(
values,
);
setFieldValue('totalPrice', totalPrice);
setFieldValue('totalTax', totalTax);
}}
style={{ marginLeft: 10 }}
data-test-id="order-sum-items-button"
>
{t('Sum items')}
</Button>
</Col>
</Row>
</>
)}
/>
<Row gutter={8} style={{ marginTop: 15 }}>
<Col sm={10}>
<StyledFormItem>
<StyledLabel>{t('Note')}</StyledLabel>
<Input.TextArea
rows={4}
name="note"
placeholder={t('note_placeholder')}
onChange={handleChange}
data-test-id="order-form-note"
value={values.note || ''}
/>
</StyledFormItem>
</Col>
<Col sm={8}>
<Row>
<Col sm={18} offset={3}>
<UrgentSlider />
</Col>
</Row>
</Col>
<Col sm={6}>
<OrderSummaryWrapper>
<FormInput
name="totalPrice"
label={t('Total price')}
type="number"
suffix={CURRENCY_SUFFIX}
withLabel
/>
<FormInput
name="totalTax"
label={t('Total tax')}
type="number"
suffix={CURRENCY_SUFFIX}
withLabel
/>
{!!getTotalPriceIncludingTax(values) && (
<div>
<StyledLabel>{t('Total price including tax')}</StyledLabel>
<span>
{currencyFormatter(
getTotalPriceIncludingTax(values) || 0,
)}
</span>
</div>
)}
</OrderSummaryWrapper>
</Col>
</Row>
{props.submitButton}
</StyledForm>
)}
</Formik>
);
}
Example #28
Source File: info.tsx From fe-v5 with Apache License 2.0 | 4 votes |
export default function Info() {
const { t } = useTranslation();
const [form] = Form.useForm();
const [isModalVisible, setIsModalVisible] = useState(false);
const [contactsList, setContactsList] = useState<ContactsItem[]>([]);
let { profile } = useSelector<RootState, accountStoreState>((state) => state.account);
const [selectAvatar, setSelectAvatar] = useState<string>(profile.portrait || '/image/avatar1.png');
const [customAvatar, setCustomAvatar] = useState('');
const dispatch = useDispatch();
useEffect(() => {
const { id, nickname, email, phone, contacts, portrait } = profile;
form.setFieldsValue({
nickname,
email,
phone,
contacts,
});
if (portrait.startsWith('http')) {
setCustomAvatar(portrait);
}
}, [profile]);
useEffect(() => {
getNotifyChannels().then((data: Array<ContactsItem>) => {
setContactsList(data);
});
}, []);
const handleSubmit = async () => {
try {
await form.validateFields();
updateProfile();
} catch (err) {
console.log(t('输入有误'), err);
}
};
const handleOk = () => {
if (customAvatar) {
if (!customAvatar.startsWith('http')) {
message.error(t('自定义头像需以http开头'));
return;
}
fetch(customAvatar, { mode: 'no-cors' })
.then((res) => {
setIsModalVisible(false);
handleSubmit();
})
.catch((err) => {
message.error(t('自定义头像') + err);
});
} else {
setIsModalVisible(false);
handleSubmit();
}
};
const handleCancel = () => {
setIsModalVisible(false);
};
const updateProfile = () => {
const { nickname, email, phone, moreContacts } = form.getFieldsValue();
let { contacts } = form.getFieldsValue();
if (moreContacts && moreContacts.length > 0) {
moreContacts.forEach((item) => {
const { key, value } = item;
if (key && value) {
if (contacts) {
contacts[key] = value;
} else {
contacts = {
[key]: value,
};
}
}
});
}
for (let key in contacts) {
if (!contacts[key]) {
delete contacts[key];
}
}
dispatch({
type: 'account/updateProfile',
data: {
...profile,
portrait: customAvatar || selectAvatar,
nickname,
email,
phone,
contacts,
},
});
message.success(t('信息保存成功'));
};
const avatarList = new Array(8).fill(0).map((_, i) => i + 1);
const handleImgClick = (i) => {
setSelectAvatar(`/image/avatar${i}.png`);
};
return (
<>
<Form form={form} layout='vertical'>
<Row
gutter={16}
style={{
marginBottom: '24px',
}}
>
<Col span={20}>
<Row
gutter={16}
style={{
marginBottom: '24px',
}}
>
<Col span={4}>
<div>
<label>{t('用户名')}:</label>
<span>{profile.username}</span>
</div>
</Col>
<Col span={4}>
<div>
<label>{t('角色')}:</label>
<span>{profile.roles.join(', ')}</span>
</div>
</Col>
</Row>
<Form.Item label={<span>{t('显示名')}:</span>} name='nickname'>
<Input placeholder={t('请输入显示名')} />
</Form.Item>
<Form.Item label={<span>{t('邮箱')}:</span>} name='email'>
<Input placeholder={t('请输入邮箱')} />
</Form.Item>
<Form.Item label={<span>{t('手机')}:</span>} name='phone'>
<Input placeholder={t('请输入手机号')} />
</Form.Item>
{profile.contacts &&
Object.keys(profile.contacts)
.sort()
.map((key, i) => {
let contact = contactsList.find((item) => item.key === key);
return (
<>
{contact ? (
<Form.Item label={contact.label + ':'} name={['contacts', key]} key={i}>
<Input placeholder={`${t('请输入')}${key}`} />
</Form.Item>
) : null}
</>
);
})}
<Form.Item label={t('更多联系方式')}>
<Form.List name='moreContacts'>
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, fieldKey, ...restField }) => (
<Space
key={key}
style={{
display: 'flex',
}}
align='baseline'
>
<Form.Item
style={{
width: '180px',
}}
{...restField}
name={[name, 'key']}
fieldKey={[fieldKey, 'key']}
rules={[
{
required: true,
message: t('联系方式不能为空'),
},
]}
>
<Select suffixIcon={<CaretDownOutlined />} placeholder={t('请选择联系方式')}>
{contactsList.map((item, index) => (
<Option value={item.key} key={index}>
{item.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
{...restField}
style={{
width: '330px',
}}
name={[name, 'value']}
fieldKey={[fieldKey, 'value']}
rules={[
{
required: true,
message: t('值不能为空'),
},
]}
>
<Input placeholder={t('请输入值')} />
</Form.Item>
<MinusCircleOutlined className='control-icon-normal' onClick={() => remove(name)} />
</Space>
))}
<PlusCircleOutlined className='control-icon-normal' onClick={() => add()} />
</>
)}
</Form.List>
</Form.Item>
<Form.Item>
<Button type='primary' onClick={handleSubmit}>
{t('确认修改')}
</Button>
</Form.Item>
</Col>
<Col span={4}>
<div className='avatar'>
<img src={profile.portrait || '/image/avatar1.png'} />
<Button type='primary' className='update-avatar' onClick={() => setIsModalVisible(true)}>
{t('更换头像')}
</Button>
</div>
</Col>
</Row>
</Form>
<Modal title={t('更换头像')} visible={isModalVisible} onOk={handleOk} onCancel={handleCancel} wrapClassName='avatar-modal'>
<div className='avatar-content'>
{avatarList.map((i) => {
return (
<div key={i} className={`/image/avatar${i}.png` === selectAvatar ? 'avatar active' : 'avatar'} onClick={() => handleImgClick(i)}>
<img src={`/image/avatar${i}.png`} />
</div>
);
})}
</div>
<Input addonBefore={<span>{t('头像URL')}:</span>} onChange={(e) => setCustomAvatar(e.target.value)} value={customAvatar} />
</Modal>
</>
);
}
Example #29
Source File: GraphConfigInner.tsx From fe-v5 with Apache License 2.0 | 4 votes |
render() {
const { data, onChange } = this.props;
const { now, start, end, comparison } = data;
const handleAggrFuncClick = (e) => {
this.handleAggrFuncChange(e.key);
};
const aggrFuncMenu = (
<Menu onClick={handleAggrFuncClick} selectedKeys={[this.state.curAggrFunc]}>
<Menu.Item key='sum'>sum</Menu.Item>
<Menu.Item key='avg'>avg</Menu.Item>
<Menu.Item key='max'>max</Menu.Item>
<Menu.Item key='min'>min</Menu.Item>
</Menu>
);
const calcFuncMenu = (
<Menu onClick={(e) => this.handleCalcFuncChange(e.key === 'clear' ? '' : e.key)} selectedKeys={[this.state.calcFunc]}>
<Menu.Item key='rate_1m'>rate_1m</Menu.Item>
<Menu.Item key='rate_5m'>rate_5m</Menu.Item>
<Menu.Item key='increase_1m'>increase_1m</Menu.Item>
<Menu.Item key='increase_5m'>increase_5m</Menu.Item>
<Menu.Divider></Menu.Divider>
<Menu.Item key='clear'>clear</Menu.Item>
</Menu>
);
const handleAggrGroupsClick = (ag) => {
const index = this.state.curAggrGroups.findIndex((cag) => cag === ag.key);
let newCurAggrGroups;
if (index === -1) {
newCurAggrGroups = [...this.state.curAggrGroups, ag.key];
this.setState({
curAggrGroups: newCurAggrGroups,
});
} else {
let curComparisonCopy = [...this.state.curAggrGroups];
curComparisonCopy.splice(index, 1);
newCurAggrGroups = curComparisonCopy;
this.setState({
curAggrGroups: curComparisonCopy,
});
}
this.handleAggrGroupsChange(newCurAggrGroups);
};
const aggrGroupsMenu = (
<Menu onClick={handleAggrGroupsClick} selectedKeys={this.state.curAggrGroups}>
{this.state.aggrGroups
.filter((n) => n !== '__name__')
.map((ag) => (
<Menu.Item key={ag}>{ag}</Menu.Item>
))}
</Menu>
);
const handleDeleteAggrGroupClick = (ag) => {
let newCurAggrGroups = [...this.state.curAggrGroups];
let idx = newCurAggrGroups.findIndex((cag) => cag === ag);
if (idx >= 0) newCurAggrGroups.splice(idx, 1);
this.handleAggrGroupsChange(newCurAggrGroups);
};
return (
<div className='graph-config-inner'>
<div className='graph-config-inner-item'>
计算函数 :
<Dropdown overlay={calcFuncMenu}>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
{this.state.calcFunc} <DownOutlined />
</a>
</Dropdown>
</div>
<div className='graph-config-inner-item'>
环比:
<Comparison
comparison={comparison}
relativeTimeComparison={data.relativeTimeComparison}
comparisonOptions={data.comparisonOptions}
graphConfig={data}
onChange={this.handleComparisonChange}
/>
<input
style={{
position: 'fixed',
left: -10000,
}}
id={`hiddenInput${data.id}`}
/>
</div>
<div className='graph-config-inner-item'>
聚合函数 :
<Dropdown overlay={aggrFuncMenu}>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
{this.state.curAggrFunc} <DownOutlined />
</a>
</Dropdown>
</div>
{this.state.curAggrFunc ? (
<div className='graph-config-inner-item'>
<span>聚合维度 :</span>
{/* <Select
mode="multiple"
size="small"
style={{ minWidth: 60 }}
dropdownMatchSelectWidth={false}
value={this.state.curAggrGroups}
onChange={this.handleAggrGroupsChange}
>
{this.state.aggrGroups.map(ag => <Option key={ag} value={ag}>{ag}</Option>)}
</Select> */}
{this.state.curAggrGroups.map((ag) => (
<Tag
key={ag}
closable
onClose={(e) => {
handleDeleteAggrGroupClick(ag);
}}
>
{ag}
</Tag>
))}
<Dropdown overlay={aggrGroupsMenu} overlayStyle={{ maxHeight: 400, overflow: 'auto' }}>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
<PlusCircleOutlined />
</a>
</Dropdown>
</div>
) : null}
</div>
);
}