@ant-design/icons#CaretDownOutlined TypeScript Examples
The following examples show how to use
@ant-design/icons#CaretDownOutlined.
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: Experiment.tsx From posthog-foss with MIT License | 6 votes |
export function CodeLanguageSelect(): JSX.Element {
return (
<Select defaultValue="JavaScript" suffixIcon={<CaretDownOutlined />}>
<Select.Option value="JavaScript">
<Row align="middle">
<IconJavascript style={{ marginRight: 6 }} /> JavaScript
</Row>
</Select.Option>
</Select>
)
}
Example #2
Source File: TaskStatusCell.tsx From RareCamp with Apache License 2.0 | 6 votes |
export default function TaskStatusCell({ task, programId }) {
const [showEdit, setShowEdit] = useState(false)
return (
<td
onFocus={() => setShowEdit(true)}
onBlur={() => setShowEdit(false)}
onMouseOver={() => setShowEdit(true)}
onMouseOut={() => setShowEdit(false)}
className="ant-table-cell"
style={{ width: 130 }}
>
<TaskStatus task={task} programId={programId} />
<CaretDownOutlined
style={{
position: 'absolute',
visibility: showEdit ? 'visible' : 'hidden',
}}
/>
</td>
)
}
Example #3
Source File: index.tsx From fe-v5 with Apache License 2.0 | 6 votes |
export default function OrderSort(props: Props) {
const { t } = useTranslation();
const { onChange, showLabel } = props;
const [isDesc, setIsDesc] = useState<Boolean>(true);
const handleClick = (e) => {
setIsDesc(!isDesc);
onChange(!isDesc);
e.preventDefault()
};
return (
<div className='desc-sort'>
{showLabel && (isDesc ? t('降序') : t('升序'))}
<div className='desc-sort-icon' onClick={handleClick}>
<CaretUpOutlined
style={{
color: isDesc === false ? 'blue' : '',
}}
/>
<CaretDownOutlined
style={{
color: isDesc === true ? 'blue' : '',
marginTop: '-0.3em',
}}
/>
</div>
</div>
);
}
Example #4
Source File: makeDropdownWidget.tsx From imove with MIT License | 6 votes |
makeDropdownWidget = (options: IOptions) => {
const Widget: React.FC<IDropdownWidgetProps> = (props) => {
const { flowChart } = props;
const { tooltip, getIcon, getOverlay, handler } = options;
const iconWrapperCls = [styles.btnWidget];
let { disabled = false } = options;
if (typeof disabled === 'function') {
disabled = disabled(flowChart);
disabled && iconWrapperCls.push(styles.disabled);
}
const onChange = (data: any): void => {
if (disabled) return;
handler(flowChart, data);
flowChart.trigger('toolBar:forceUpdate');
};
return (
<Tooltip title={tooltip}>
<Dropdown
disabled={disabled}
overlay={getOverlay(flowChart, onChange)}
trigger={['click']}
>
<div className={iconWrapperCls.join(' ')}>
{getIcon(flowChart)} <CaretDownOutlined className={styles.caret} />
</div>
</Dropdown>
</Tooltip>
);
};
return Widget;
}
Example #5
Source File: index.tsx From fe-v5 with Apache License 2.0 | 5 votes |
export default function index(props: IProps) {
const { preNamePrefix = [], namePrefix = ['options', 'standardOptions'] } = props;
return (
<Panel header='高级设置'>
<>
<Form.Item
label={
<div>
单位{' '}
<Tooltip
overlayInnerStyle={{
width: 500,
}}
title={
<div>
<div>默认会做 SI Prefixes 处理,如不想默认的处理可选择 none 关闭</div>
<div>Data(SI): 基数为 1000, 单位为 B、kB、MB、GB、TB、PB、EB、ZB、YB</div>
<div>Data(IEC): 基数为 1024, 单位为 B、KiB、MiB、GiB、TiB、PiB、EiB、ZiB、YiB</div>
<div>bits: b</div>
<div>bytes: B</div>
</div>
}
>
<InfoCircleOutlined />
</Tooltip>
</div>
}
name={[...namePrefix, 'util']}
>
<Select suffixIcon={<CaretDownOutlined />} placeholder='auto' allowClear>
<Option value='none'>none</Option>
<OptGroup label='Data(SI)'>
<Option value='bitsSI'>bits(SI)</Option>
<Option value='bytesSI'>bytes(SI)</Option>
</OptGroup>
<OptGroup label='Data(IEC)'>
<Option value='bitsIEC'>bits(IEC)</Option>
<Option value='bytesIEC'>bytes(IEC)</Option>
</OptGroup>
<OptGroup label='百分比'>
<Option value='percent'>百分比(0-100)</Option>
<Option value='percentUnit'>百分比(0.0-1.0)</Option>
</OptGroup>
<OptGroup label='时间'>
<Option value='seconds'>seconds</Option>
<Option value='milliseconds'>milliseconds</Option>
<Option value='humantimeSeconds'>humanize(seconds)</Option>
<Option value='humantimeMilliseconds'>humanize(milliseconds)</Option>
</OptGroup>
</Select>
</Form.Item>
<Row gutter={10}>
<Col span={8}>
<Form.Item label='最小值' name={[...namePrefix, 'min']}>
<InputNumber placeholder='auto' style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label='最大值' name={[...namePrefix, 'max']}>
<InputNumber placeholder='auto' style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label='小数点' name={[...namePrefix, 'decimals']}>
<InputNumber placeholder='auto' style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
</>
</Panel>
);
}
Example #6
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 #7
Source File: GraphStyles.tsx From fe-v5 with Apache License 2.0 | 5 votes |
export default function GraphStyles() {
const namePrefix = ['custom'];
return (
<Panel header='图表样式'>
<>
<Row gutter={10}>
<Col span={12}>
<Form.Item label='取值计算' name={[...namePrefix, 'calc']}>
<Select suffixIcon={<CaretDownOutlined />}>
{_.map(calcsOptions, (item, key) => {
return (
<Select.Option key={key} value={key}>
{item.name}
</Select.Option>
);
})}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label='图例位置' name={[...namePrefix, 'legengPosition']}>
<Select suffixIcon={<CaretDownOutlined />}>
{legendPostion.map((item) => {
return (
<Select.Option key={item} value={item}>
{item}
</Select.Option>
);
})}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label='最多展示块数' name={[...namePrefix, 'max']} tooltip='超过的块数则合并展示为其他'>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
</>
</Panel>
);
}
Example #8
Source File: GraphStyles.tsx From fe-v5 with Apache License 2.0 | 5 votes |
export default function GraphStyles() {
const namePrefix = ['custom'];
return (
<Panel header='图表样式'>
<>
<Row gutter={10}>
<Col span={12}>
<Form.Item label='显示内容' name={[...namePrefix, 'textMode']}>
<Radio.Group buttonStyle='solid'>
<Radio.Button value='valueAndName'>名称和值</Radio.Button>
<Radio.Button value='value'>值</Radio.Button>
</Radio.Group>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label='颜色模式' name={[...namePrefix, 'colorMode']}>
<Radio.Group buttonStyle='solid'>
<Radio.Button value='value'>值</Radio.Button>
<Radio.Button value='background'>背景</Radio.Button>
</Radio.Group>
</Form.Item>
</Col>
</Row>
<Row gutter={10}>
<Col span={12}>
<Form.Item label='取值计算' name={[...namePrefix, 'calc']}>
<Select suffixIcon={<CaretDownOutlined />}>
{_.map(calcsOptions, (item, key) => {
return (
<Select.Option key={key} value={key}>
{item.name}
</Select.Option>
);
})}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label='每行最多显示' name={[...namePrefix, 'colSpan']}>
<Select suffixIcon={<CaretDownOutlined />}>
{_.map(colSpans, (item) => {
return (
<Select.Option key={item} value={item}>
{item}
</Select.Option>
);
})}
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={10}>
<Col span={12}>
<Form.Item label='标题字体大小' name={[...namePrefix, 'textSize', 'title']}>
<InputNumber placeholder='auto' style={{ width: '100%' }} min={12} max={100} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label='值字体大小' name={[...namePrefix, 'textSize', 'value']}>
<InputNumber placeholder='auto' style={{ width: '100%' }} min={12} max={100} />
</Form.Item>
</Col>
</Row>
</>
</Panel>
);
}
Example #9
Source File: GraphStyles.tsx From fe-v5 with Apache License 2.0 | 5 votes |
export default function GraphStyles() {
const namePrefix = ['custom'];
return (
<Panel header='图表样式'>
<>
<Row gutter={10}>
<Col span={12}>
<Form.Item label='取值计算' name={[...namePrefix, 'calc']}>
<Select suffixIcon={<CaretDownOutlined />}>
{_.map(calcsOptions, (item, key) => {
return (
<Select.Option key={key} value={key}>
{item.name}
</Select.Option>
);
})}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label='图例位置' name={[...namePrefix, 'legengPosition']}>
<Select suffixIcon={<CaretDownOutlined />}>
{legendPostion.map((item) => {
return (
<Select.Option key={item} value={item}>
{item}
</Select.Option>
);
})}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label='最多展示块数' name={[...namePrefix, 'max']} tooltip='超过的块数则合并展示为其他'>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label='label是否包含名称' name={[...namePrefix, 'labelWithName']} valuePropName='checked'>
<Switch />
</Form.Item>
</Col>
</Row>
</>
</Panel>
);
}
Example #10
Source File: GraphStyles.tsx From fe-v5 with Apache License 2.0 | 5 votes |
export default function GraphStyles() {
const namePrefix = ['custom'];
return (
<Panel header='图表样式'>
<>
<Row gutter={10}>
<Col span={12}>
<Form.Item label='显示表头' name={[...namePrefix, 'showHeader']} valuePropName='checked'>
<Switch size='small' />
</Form.Item>
</Col>
</Row>
<Form.Item label='取值计算' name={[...namePrefix, 'calc']}>
<Select suffixIcon={<CaretDownOutlined />}>
{_.map(calcsOptions, (item, key) => {
return (
<Select.Option key={key} value={key}>
{item.name}
</Select.Option>
);
})}
</Select>
</Form.Item>
<Row gutter={10}>
<Col span={12}>
<Form.Item label='显示模式' name={[...namePrefix, 'displayMode']}>
<Select suffixIcon={<CaretDownOutlined />}>
<Select.Option value='seriesToRows'>每行展示 serie 的值</Select.Option>
<Select.Option value='labelValuesToRows'>每行展示指定聚合维度的值</Select.Option>
</Select>
</Form.Item>
</Col>
<Form.Item noStyle shouldUpdate={(prevValues, curValues) => _.get(prevValues, [...namePrefix, 'displayMode']) !== _.get(curValues, [...namePrefix, 'displayMode'])}>
{({ getFieldValue }) => {
if (getFieldValue([...namePrefix, 'displayMode']) === 'labelValuesToRows') {
return (
<Col span={12}>
<Form.Item label='显示维度' name={[...namePrefix, 'aggrDimension']}>
<Input />
</Form.Item>
</Col>
);
}
return null;
}}
</Form.Item>
</Row>
</>
</Panel>
);
}
Example #11
Source File: index.tsx From fe-v5 with Apache License 2.0 | 5 votes |
export default function index(props: IProps) {
const { preNamePrefix = [], namePrefix = ['options', 'standardOptions'] } = props;
return (
<Panel header='高级设置'>
<>
<Form.Item
label={
<div>
单位{' '}
<Tooltip
overlayInnerStyle={{
width: 500,
}}
getTooltipContainer={() => document.body}
title={
<div>
<div>默认会做 SI Prefixes 处理,如不想默认的处理可选择 none 关闭</div>
<div>Data(SI): 基数为 1000, 单位为 B、kB、MB、GB、TB、PB、EB、ZB、YB</div>
<div>Data(IEC): 基数为 1024, 单位为 B、KiB、MiB、GiB、TiB、PiB、EiB、ZiB、YiB</div>
<div>bits: b</div>
<div>bytes: B</div>
</div>
}
>
<InfoCircleOutlined />
</Tooltip>
</div>
}
name={[...namePrefix, 'util']}
>
<Select suffixIcon={<CaretDownOutlined />} placeholder='auto' allowClear>
<Option value='none'>none</Option>
<OptGroup label='Data(SI)'>
<Option value='bitsSI'>bits(SI)</Option>
<Option value='bytesSI'>bytes(SI)</Option>
</OptGroup>
<OptGroup label='Data(IEC)'>
<Option value='bitsIEC'>bits(IEC)</Option>
<Option value='bytesIEC'>bytes(IEC)</Option>
</OptGroup>
<OptGroup label='百分比'>
<Option value='percent'>百分比(0-100)</Option>
<Option value='percentUnit'>百分比(0.0-1.0)</Option>
</OptGroup>
<OptGroup label='时间'>
<Option value='seconds'>seconds</Option>
<Option value='milliseconds'>milliseconds</Option>
<Option value='humantimeSeconds'>humanize(seconds)</Option>
<Option value='humantimeMilliseconds'>humanize(milliseconds)</Option>
</OptGroup>
</Select>
</Form.Item>
<Row gutter={10}>
<Col span={8}>
<Form.Item label='最小值' name={[...namePrefix, 'min']}>
<InputNumber placeholder='auto' style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label='最大值' name={[...namePrefix, 'max']}>
<InputNumber placeholder='auto' style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label='小数点' name={[...namePrefix, 'decimals']}>
<InputNumber placeholder='auto' style={{ width: '100%' }} />
</Form.Item>
</Col>
</Row>
</>
</Panel>
);
}
Example #12
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 #13
Source File: tagItem.tsx From fe-v5 with Apache License 2.0 | 5 votes |
TagItem: React.FC<Itag> = ({ field, remove, form }) => {
const { t } = useTranslation();
const [valuePlaceholder, setValuePlaceholder] = useState<string>('');
const [funcCur, setfuncCur] = useState('==');
useEffect(() => {
const tags = form.getFieldValue('tags');
funcChange(tags[field.name].func);
}, [field]);
const funcChange = (val) => {
let text = '';
if (val === 'in') {
text = '可以输入多个值,用回车分割';
} else if (val === '=~') {
text = '请输入正则表达式匹配标签value';
}
setfuncCur(val);
setValuePlaceholder(text);
};
return (
<>
<Row gutter={[10, 10]} style={{ marginBottom: '10px' }}>
<Col span={5}>
<Form.Item style={{ marginBottom: 0 }} name={[field.name, 'key']} fieldKey={[field.name, 'key']} rules={[{ required: true, message: t('key不能为空') }]}>
<Input placeholder={t('请输入屏蔽事件标签key')} />
</Form.Item>
</Col>
<Col span={3}>
<Form.Item style={{ marginBottom: 0 }} name={[field.name, 'func']} fieldKey={[field.name, 'func']} initialValue='=='>
<Select suffixIcon={<CaretDownOutlined />} onChange={funcChange}>
<Option value='=='>==</Option>
<Option value='=~'>=~</Option>
<Option value='in'>in</Option>
</Select>
</Form.Item>
</Col>
<Col span={15}>
<Form.Item style={{ marginBottom: 0 }} name={[field.name, 'value']} fieldKey={[field.name, 'value']} rules={[{ required: true, message: t('value不能为空') }]}>
{funcCur == 'in' ? (
<Select mode='tags' open={false} style={{ width: '100%' }} placeholder={t(valuePlaceholder)} tokenSeparators={[' ']}></Select>
) : (
<Input className='ant-input' placeholder={t(valuePlaceholder)} />
)}
</Form.Item>
</Col>
<Col>
<MinusCircleOutlined style={{ marginTop: '8px' }} onClick={() => remove(field.name)} />
</Col>
</Row>
</>
);
}
Example #14
Source File: index.tsx From fe-v5 with Apache License 2.0 | 5 votes |
export default function ColumnSelect(props: Props) {
const { onSeverityChange, onEventTypeChange, onBusiGroupChange, onClusterChange, noLeftPadding, noRightPadding = true } = props;
const { clusters, busiGroups } = useSelector<RootState, CommonStoreState>((state) => state.common);
const [filteredBusiGroups, setFilteredBusiGroups] = useState(busiGroups);
const fetchBusiGroup = (e) => {
getBusiGroups(e).then((res) => {
setFilteredBusiGroups(res.dat || []);
});
};
const handleSearch = useCallback(debounce(fetchBusiGroup, 800), []);
return (
<Space style={{ marginLeft: noLeftPadding ? 0 : 8, marginRight: noRightPadding ? 0 : 8 }}>
{onClusterChange && (
<Select mode='multiple' allowClear style={{ minWidth: 80 }} placeholder='集群' onChange={onClusterChange} getPopupContainer={() => document.body}>
{clusters.map((k) => (
<Select.Option value={k} key={k}>
{k}
</Select.Option>
))}
</Select>
)}
{onBusiGroupChange && (
<Select
allowClear
showSearch
style={{ minWidth: 120 }}
placeholder='业务组'
dropdownClassName='overflow-586'
filterOption={false}
onSearch={handleSearch}
getPopupContainer={() => document.body}
onFocus={() => {
getBusiGroups('').then((res) => {
setFilteredBusiGroups(res.dat || []);
});
}}
onClear={() => {
getBusiGroups('').then((res) => {
setFilteredBusiGroups(res.dat || []);
});
}}
onChange={onBusiGroupChange}
>
{filteredBusiGroups.map((item) => (
<Select.Option value={item.id} key={item.id}>
{item.name}
</Select.Option>
))}
</Select>
)}
{onSeverityChange && (
<Select suffixIcon={<CaretDownOutlined />} allowClear style={{ minWidth: 80 }} placeholder='事件级别' onChange={onSeverityChange} getPopupContainer={() => document.body}>
<Select.Option value={1}>一级告警</Select.Option>
<Select.Option value={2}>二级告警</Select.Option>
<Select.Option value={3}>三级告警</Select.Option>
</Select>
)}
{onEventTypeChange && (
<Select suffixIcon={<CaretDownOutlined />} allowClear style={{ minWidth: 80 }} placeholder='事件类别' onChange={onEventTypeChange} getPopupContainer={() => document.body}>
<Select.Option value={0}>Triggered</Select.Option>
<Select.Option value={1}>Recovered</Select.Option>
</Select>
)}
</Space>
);
}
Example #15
Source File: FormDataItem.tsx From yugong with MIT License | 5 votes |
FormDataItem: React.FC<Props> = ({ onMinus, value, order }) => {
const { onChangeRunningData, runningData, dataPath } = useContext(FormModuleContext)
const onChange = useCallback(
(data: {[keys: string]: any}) => {
if (!runningData) return;
const operateData = cloneDeep(runningData)
const itemPath = `${dataPath}[${order - 1}]`;
const itemData = get(operateData, itemPath);
const newItemData = {
...itemData,
...data
}
set(operateData, itemPath, newItemData);
onChangeRunningData?.(operateData)
},
[dataPath, onChangeRunningData, order, runningData],
)
const [showOptions, setShowOptions] = useState(false);
const disabled = false;
return (
<div className={s.root}>
<LineItem
label={
<div className={s.dragwrap}>
<span className={s.drag}>
<DragHandle />
</span>
第{order}项
</div>
}
>
<Input
disabled={disabled}
className={s.inp}
onChange={(e) => onChange({title: e.target.value})}
value={value.title}
placeholder="名称"
/>
<Input name="rowMap"
disabled={disabled}
className={classNames(s.inp, s.nbl, s.nbrad)}
onChange={(e) => onChange({dataIndex: e.target.value})}
value={value?.dataIndex}
placeholder="字段(必填)"
/>
<Button
disabled={disabled}
className={classNames(s.btn, s.nbl, s.nbr)}
icon={showOptions ? <CaretDownOutlined /> : <CaretRightOutlined />}
onClick={() => setShowOptions(!showOptions)}
/>
<Button
disabled={disabled}
className={s.btn}
icon={<MinusOutlined />}
onClick={() => onMinus?.()}
/>
</LineItem>
<div style={{ display: showOptions ? 'block' : 'none' }}>
<LineItem label="">
<SubItem value={value} onChange={onChange} />
</LineItem>
</div>
</div>
);
}
Example #16
Source File: TaskDateCell.tsx From RareCamp with Apache License 2.0 | 5 votes |
export default function TaskDateCell({ task, programId, dateKey }) {
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false)
const [showEdit, setShowEdit] = useState(false)
const updateTaskMutation = useEditTaskMutation({
programId,
taskId: task.taskId,
projectId: task.projectId,
})
return (
<DateCell
onFocus={() => setShowEdit(true)}
onBlur={() => setShowEdit(false)}
onMouseOver={() => setShowEdit(true)}
onMouseOut={() => setShowEdit(false)}
className="ant-table-cell"
onClick={() => setIsDatePickerOpen(!isDatePickerOpen)}
onKeyPress={() => setIsDatePickerOpen(!isDatePickerOpen)}
tabIndex={task.taskId}
>
<Space>
<span>
{task[dateKey]
? dayjs(task[dateKey]).format('DD/MM/YYYY')
: ''}
</span>
<CaretDownOutlined
style={{
visibility: showEdit ? 'visible' : 'hidden',
top: 22,
}}
/>
<LoadingOutlined
style={{
position: 'absolute',
visibility: updateTaskMutation.isLoading
? 'visible'
: 'hidden',
top: 22,
}}
/>
</Space>
<DatePicker
open={isDatePickerOpen}
onOpenChange={(open) => setIsDatePickerOpen(open)}
onChange={(date) => {
if (date) {
updateTaskMutation.mutate({
[dateKey]: date?.toDate(),
})
}
}}
/>
</DateCell>
)
}
Example #17
Source File: tagItem.tsx From fe-v5 with Apache License 2.0 | 5 votes |
TagItem: React.FC<Itag> = ({ field, remove, form }) => {
const { t } = useTranslation();
const [valuePlaceholder, setValuePlaceholder] = useState<string>('');
const [funcCur, setfuncCur] = useState('==');
useEffect(() => {
const tags = form.getFieldValue('tags');
funcChange(tags[field.name].func);
}, []);
const funcChange = (val) => {
let text = '';
if (val === 'in') {
text = '可以输入多个值,用回车分割';
} else if (val === '=~') {
text = '请输入正则表达式匹配标签value';
}
setfuncCur(val);
setValuePlaceholder(text);
};
return (
<>
<Row gutter={[10, 10]} style={{ marginBottom: '10px' }}>
<Col span={5}>
<Form.Item style={{ marginBottom: 0 }} name={[field.name, 'key']} fieldKey={[field.name, 'key']} rules={[{ required: true, message: t('key不能为空') }]}>
<Input placeholder={t('请输入订阅事件标签key')} />
</Form.Item>
</Col>
<Col span={3}>
<Form.Item style={{ marginBottom: 0 }} name={[field.name, 'func']} fieldKey={[field.name, 'func']} initialValue='=='>
<Select suffixIcon={<CaretDownOutlined />} onChange={funcChange}>
<Option value='=='>==</Option>
<Option value='=~'>=~</Option>
<Option value='in'>in</Option>
</Select>
</Form.Item>
</Col>
<Col span={15}>
<Form.Item style={{ marginBottom: 0 }} name={[field.name, 'value']} fieldKey={[field.name, 'value']} rules={[{ required: true, message: t('value不能为空') }]}>
{funcCur == 'in' ? (
<Select mode='tags' open={false} style={{ width: '100%' }} placeholder={t(valuePlaceholder)} tokenSeparators={[' ']}></Select>
) : (
<Input className='ant-input' placeholder={t(valuePlaceholder)} />
)}
</Form.Item>
</Col>
<Col>
<MinusCircleOutlined style={{ marginTop: '8px' }} onClick={() => remove(field.name)} />
</Col>
</Row>
</>
);
}
Example #18
Source File: UpgradeSection.tsx From posthog-foss with MIT License | 5 votes |
export function UpgradeSection(): JSX.Element {
const { checkForUpdates, toggleSectionOpen } = useActions(pluginsLogic)
const { sectionsOpen } = useValues(pluginsLogic)
const { user } = useValues(userLogic)
const {
filteredPluginsNeedingUpdates,
pluginsNeedingUpdates,
checkingForUpdates,
installedPluginUrls,
updateStatus,
rearranging,
hasUpdatablePlugins,
} = useValues(pluginsLogic)
const upgradeButton = canInstallPlugins(user?.organization) && hasUpdatablePlugins && (
<Button
type="default"
icon={pluginsNeedingUpdates.length > 0 ? <SyncOutlined /> : <CloudDownloadOutlined />}
onClick={(e) => {
e.stopPropagation()
checkForUpdates(true)
}}
loading={checkingForUpdates}
>
{checkingForUpdates
? `Checking plugin ${Object.keys(updateStatus).length + 1} out of ${
Object.keys(installedPluginUrls).length
}`
: pluginsNeedingUpdates.length > 0
? 'Check again for updates'
: 'Check for updates'}
</Button>
)
return (
<>
<div
className="plugins-installed-tab-section-header"
onClick={() => toggleSectionOpen(PluginSection.Upgrade)}
>
<Subtitle
subtitle={
<>
{sectionsOpen.includes(PluginSection.Upgrade) ? (
<CaretDownOutlined />
) : (
<CaretRightOutlined />
)}
{` Plugins to update (${filteredPluginsNeedingUpdates.length})`}
</>
}
buttons={!rearranging && sectionsOpen.includes(PluginSection.Upgrade) && upgradeButton}
/>
</div>
{sectionsOpen.includes(PluginSection.Upgrade) ? (
<>
{pluginsNeedingUpdates.length > 0 ? (
<Row gutter={16} style={{ marginTop: 16 }}>
{filteredPluginsNeedingUpdates.length > 0 ? (
<>
{filteredPluginsNeedingUpdates.map((plugin) => (
<InstalledPlugin key={plugin.id} plugin={plugin} showUpdateButton />
))}
</>
) : (
<p style={{ margin: 10 }}>No plugins match your search.</p>
)}
</Row>
) : (
<p style={{ margin: 10 }}>All your plugins are up to date. Great work!</p>
)}
</>
) : null}
</>
)
}
Example #19
Source File: DisabledPluginsSection.tsx From posthog-foss with MIT License | 5 votes |
export function DisabledPluginSection(): JSX.Element {
const { filteredDisabledPlugins, sectionsOpen, disabledPlugins } = useValues(pluginsLogic)
const { toggleSectionOpen } = useActions(pluginsLogic)
if (disabledPlugins.length === 0) {
return <></>
}
return (
<>
<div
className="plugins-installed-tab-section-header"
onClick={() => toggleSectionOpen(PluginSection.Disabled)}
>
<Subtitle
subtitle={
<>
{sectionsOpen.includes(PluginSection.Disabled) ? (
<CaretDownOutlined />
) : (
<CaretRightOutlined />
)}
{` Installed plugins (${filteredDisabledPlugins.length})`}
</>
}
/>
</div>
{sectionsOpen.includes(PluginSection.Disabled) ? (
<>
{filteredDisabledPlugins.length > 0 ? (
<Row gutter={16} style={{ marginTop: 16 }}>
{filteredDisabledPlugins.map((plugin) => (
<InstalledPlugin key={plugin.id} plugin={plugin} />
))}
</Row>
) : (
<p style={{ margin: 10 }}>No plugins match your search.</p>
)}
</>
) : null}
</>
)
}
Example #20
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 #21
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 #22
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 #23
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 #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 fe-v5 with Apache License 2.0 | 4 votes |
TeamForm = React.forwardRef<ReactNode, TeamProps>((props, ref) => {
const { t } = useTranslation();
const { businessId, action } = props;
const [form] = Form.useForm();
const [userTeam, setUserTeam] = useState<Team[]>([]);
const [initialValues, setInitialValues] = useState({
label_enable: false,
label_value: '',
members: [{ perm_flag: true }],
name: '',
});
const [loading, setLoading] = useState<boolean>(true);
const [refresh, setRefresh] = useState(true);
useImperativeHandle(ref, () => ({
form: form,
}));
useEffect(() => {
if (businessId && action === ActionType.EditBusiness) {
getTeamInfoDetail(businessId);
} else {
setLoading(false);
}
}, []);
const getTeamInfoDetail = (id: string) => {
getBusinessTeamInfo(id).then((data: { name: string; label_enable: number; label_value: string; user_groups: { perm_flag: string; user_group: { id: number } }[] }) => {
setInitialValues({
name: data.name,
label_enable: data.label_enable === 1,
label_value: data.label_value,
members: data.user_groups.map((item) => ({
perm_flag: item.perm_flag === 'rw',
user_group_id: item.user_group.id,
})),
});
setLoading(false);
});
};
useEffect(() => {
getList('');
}, []);
const getList = (str: string) => {
getTeamInfoList({ query: str }).then((res) => {
setUserTeam(res.dat);
});
};
const debounceFetcher = useCallback(debounce(getList, 800), []);
return !loading ? (
<Form {...layout} form={form} initialValues={initialValues} preserve={false} layout={refresh ? 'horizontal' : 'horizontal'}>
{action !== ActionType.AddBusinessMember && (
<>
<Form.Item
label={t('业务组名称')}
name='name'
rules={[
{
required: true,
message: t('业务组名称不能为空!'),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t('作为标签使用')}
name='label_enable'
valuePropName='checked'
tooltip={{ title: '系统会自动把业务组的英文标识作为标签附到该业务组下辖监控对象的时序数据上', getPopupContainer: () => document.body }}
>
<Switch />
</Form.Item>
<Form.Item noStyle shouldUpdate={(prevValues, curValues) => prevValues.label_enable !== curValues.label_enable}>
{({ getFieldValue }) => {
return (
getFieldValue('label_enable') && (
<Form.Item
label={t('英文标识')}
name='label_value'
rules={[
{
required: true,
},
]}
tooltip={{
title: (
<span>
尽量用英文,不能与其他业务组标识重复,系统会自动生成 <Tag color='blue'>busigroup={form.getFieldValue('label_value')}</Tag> 的标签
</span>
),
getPopupContainer: () => document.body,
}}
>
<Input
onChange={(val) => {
setRefresh(!refresh);
}}
/>
</Form.Item>
)
);
}}
</Form.Item>
</>
)}
{(action === ActionType.CreateBusiness || action === ActionType.AddBusinessMember) && (
<Form.Item
label={t('团队')}
required
// tooltip={{
// title: '默认可读勾选可写',
// }}
>
<Form.List name='members'>
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, fieldKey, ...restField }) => (
<Space key={key} style={{ display: 'flex', marginBottom: 8 }} align='baseline'>
<Form.Item
style={{ width: 450 }}
{...restField}
name={[name, 'user_group_id']}
fieldKey={[fieldKey, 'user_group_id']}
rules={[{ required: true, message: t('业务组团队不能为空!') }]}
>
<Select
suffixIcon={<CaretDownOutlined />}
style={{ width: '100%' }}
filterOption={false}
onSearch={(e) => debounceFetcher(e)}
showSearch
onBlur={() => getList('')}
>
{userTeam.map((team) => (
<Option key={team.id} value={team.id}>
{team.name}
</Option>
))}
</Select>
</Form.Item>
<Form.Item {...restField} name={[name, 'perm_flag']} fieldKey={[fieldKey, 'perm_flag']} valuePropName='checked'>
<Switch checkedChildren='读写' unCheckedChildren='只读' />
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} />
</Space>
))}
<Form.Item>
<Button type='dashed' onClick={() => add()} block icon={<PlusOutlined />}>
添加团队
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form.Item>
)}
</Form>
) : null;
})
Example #26
Source File: structure.tsx From ui with GNU Affero General Public License v3.0 | 4 votes |
Structure: FunctionComponent<Props> = (props) => {
const { t, i18n } = useTranslation()
const size = useWindowSize()
const [userMenu, setUserMenu] = React.useState(false)
const [open, setOpen] = React.useState<string[]>()
const [selected, setSelected] = React.useState<string[]>()
const [sidebar, setSidebar] = React.useState(size.width < 700)
const router = useRouter()
const user = useMeQuery()
React.useEffect(() => {
if (sidebar !== size.width < 700) {
setSidebar(size.width < 700)
}
}, [size.width])
React.useEffect(() => {
if (props.selected) {
const parts = props.selected.split('.')
const last = parts.pop()
if (parts.length > 0) {
setOpen(parts)
}
setSelected([last])
}
}, [props.selected])
const buildMenu = (data: SideMenuElement[]): JSX.Element[] => {
return data
.filter((element) => {
if (!element.role) {
return true
}
if (user.loading) {
return false
}
return user.data?.me.roles.includes(element.role)
})
.map(
(element): JSX.Element => {
if (element.items && element.items.length > 0) {
if (element.group) {
return (
<ItemGroup
key={element.key}
title={
<Space
style={{
textTransform: 'uppercase',
paddingTop: 16,
fontWeight: 'bold',
color: '#444',
}}
>
{element.icon}
<div>
{t(element.name)}
</div>
</Space>
}
>
{buildMenu(element.items)}
</ItemGroup>
)
}
return (
<SubMenu
key={element.key}
title={
<Space>
{element.icon}
<div>
{t(element.name)}
</div>
</Space>
}
>
{buildMenu(element.items)}
</SubMenu>
)
}
return (
<Menu.Item
onClick={async () => {
if (element.href) {
await router.push(element.href)
}
}}
key={element.key}
>
<Space>
{element.icon}
<div>
{t(element.name)}
</div>
</Space>
</Menu.Item>
)
}
)
}
const signOut = (): void => {
clearAuth()
router.reload()
}
return (
<Layout style={{ height: '100vh' }} className={'admin'}>
<Header
style={{
paddingLeft: 0,
}}
>
<Space
style={{
float: 'left',
color: '#FFF',
fontSize: 14,
marginRight: 26,
fontWeight: 'bold',
}}
>
{React.createElement(sidebar ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'sidebar-toggle',
onClick: () => setSidebar(!sidebar),
})}
<div style={{
display: 'flex',
alignItems: 'center',
}}>
<img
height={40}
src={require('../assets/images/logo_white.png?resize&size=256')}
alt={'OhMyForm'}
/>
</div>
</Space>
<div style={{ float: 'right', display: 'flex', height: '100%' }}>
<Dropdown
overlay={
<Menu>
<Menu.Item key={'profile'} onClick={() => router.push('/admin/profile')}>Profile</Menu.Item>
<Menu.Divider key={'d1'} />
<Menu.Item key={'logout'} onClick={signOut}>Logout</Menu.Item>
</Menu>
}
onVisibleChange={setUserMenu}
visible={userMenu}
>
<Space
style={{
color: '#FFF',
alignItems: 'center',
display: 'inline-flex',
}}
>
<div>Hi {user.data && user.data.me.username},</div>
<UserOutlined style={{ fontSize: 24 }} />
<CaretDownOutlined />
</Space>
</Dropdown>
</div>
</Header>
<Layout
style={{
height: '100%',
}}
>
<Sider
collapsed={sidebar}
trigger={null}
collapsedWidth={0}
breakpoint={'xs'}
width={200}
style={{
background: '#fff',
maxHeight: '100%',
overflow: 'auto',
}}
className={'sidemenu'}
>
<Menu
mode="inline"
style={{ flex: 1 }}
defaultSelectedKeys={['1']}
selectedKeys={selected}
onSelect={(s): void => setSelected(s.keyPath )}
openKeys={open}
onOpenChange={(open): void => setOpen(open )}
>
{buildMenu(sideMenu)}
</Menu>
<Menu mode="inline" selectable={false}>
<Menu.Item className={'language-selector'} key={'language-selector'}>
<Select
bordered={false}
value={i18n.language.replace(/-.*/, '')}
onChange={(next) => i18n.changeLanguage(next)}
style={{
width: '100%',
}}
>
{languages.map((language) => (
<Select.Option value={language} key={language}>
{t(`language:${language}`)}
</Select.Option>
))}
</Select>
</Menu.Item>
<Menu.Item style={{ display: 'flex', alignItems: 'center' }} key={'github'}>
<GitHubButton type="stargazers" namespace="ohmyform" repo="ohmyform" />
</Menu.Item>
<Menu.Item key={'version'}>
Version: <Tag color="gold">{process.env.version}</Tag>
</Menu.Item>
</Menu>
</Sider>
<Layout
style={{ padding: '0 24px 24px', minHeight: 500, height: '100%', overflow: 'auto' }}
>
{props.title && (
<PageHeader
title={props.title}
subTitle={props.subTitle}
extra={props.extra}
breadcrumb={{
routes: [
...(props.breadcrumbs || []).map((b) => ({
breadcrumbName: b.name,
path: '',
})),
{
breadcrumbName: props.title,
path: '',
},
],
params: props.breadcrumbs,
itemRender(route, params: BreadcrumbEntry[], routes) {
if (routes.indexOf(route) === routes.length - 1) {
return <span>{route.breadcrumbName}</span>
}
const entry = params[routes.indexOf(route)]
return (
<Link href={entry.href} as={entry.as || entry.href}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a>{entry.name}</a>
</Link>
)
},
}}
/>
)}
{props.error && (
<Alert message={props.error} type={'error'} style={{ marginBottom: 24 }} />
)}
<Spin spinning={!!props.loading}>
<Content
style={{
background: props.padded ? '#fff' : null,
padding: props.padded ? 24 : 0,
...props.style,
}}
>
{props.children}
</Content>
</Spin>
</Layout>
</Layout>
</Layout>
)
}
Example #27
Source File: Experiment.tsx From posthog-foss with MIT License | 4 votes |
export function Experiment(): JSX.Element {
const {
newExperimentData,
experimentId,
experimentData,
experimentInsightId,
minimumSampleSizePerVariant,
recommendedExposureForCountData,
variants,
expectedRunningTime,
experimentResults,
conversionRateForVariant,
countDataForVariant,
editingExistingExperiment,
experimentInsightType,
experimentResultsLoading,
areCountResultsSignificant,
areConversionResultsSignificant,
} = useValues(experimentLogic)
const {
setNewExperimentData,
createExperiment,
launchExperiment,
setFilters,
setEditExperiment,
endExperiment,
addExperimentGroup,
updateExperimentGroup,
removeExperimentGroup,
setExperimentInsightType,
} = useActions(experimentLogic)
const [form] = Form.useForm()
const [currentVariant, setCurrentVariant] = useState('control')
const [showWarning, setShowWarning] = useState(true)
const { insightProps } = useValues(
insightLogic({
dashboardItemId: experimentInsightId,
syncWithUrl: false,
})
)
const {
isStepsEmpty,
filterSteps,
filters: funnelsFilters,
results,
conversionMetrics,
} = useValues(funnelLogic(insightProps))
const { filters: trendsFilters, results: trendResults } = useValues(trendsLogic(insightProps))
const conversionRate = conversionMetrics.totalRate * 100
const sampleSizePerVariant = minimumSampleSizePerVariant(conversionRate)
const sampleSize = sampleSizePerVariant * variants.length
const trendCount = trendResults[0]?.count
const entrants = results?.[0]?.count
const runningTime = expectedRunningTime(entrants, sampleSize)
const exposure = recommendedExposureForCountData(trendCount)
const statusColors = { running: 'green', draft: 'default', complete: 'purple' }
const status = (): string => {
if (!experimentData?.start_date) {
return 'draft'
} else if (!experimentData?.end_date) {
return 'running'
}
return 'complete'
}
return (
<>
{experimentId === 'new' || editingExistingExperiment ? (
<>
<Row
align="middle"
justify="space-between"
style={{ borderBottom: '1px solid var(--border)', marginBottom: '1rem', paddingBottom: 8 }}
>
<PageHeader title={'New Experiment'} />
</Row>
<Form
name="new-experiment"
layout="vertical"
className="experiment-form"
form={form}
onValuesChange={(values) => setNewExperimentData(values)}
initialValues={{
name: newExperimentData?.name,
feature_flag_key: newExperimentData?.feature_flag_key,
description: newExperimentData?.description,
}}
onFinish={() => createExperiment(true, exposure, sampleSize)}
scrollToFirstError
>
<div>
<Row>
<Col span={12} style={{ paddingRight: 24 }}>
<Form.Item
label="Name"
name="name"
rules={[{ required: true, message: 'You have to enter a name.' }]}
>
<Input data-attr="experiment-name" className="ph-ignore-input" />
</Form.Item>
<Form.Item
label="Feature flag key"
name="feature_flag_key"
rules={[
{
required: true,
message: 'You have to enter a feature flag key name.',
},
]}
help={
<span className="text-small text-muted">
{editingExistingExperiment
? ''
: 'Enter a new and unique name for the feature flag key to be associated with this experiment.'}
</span>
}
>
<Input
data-attr="experiment-feature-flag-key"
disabled={editingExistingExperiment}
placeholder="examples: new-landing-page-experiment, betaFeatureExperiment, ab_test_1_experiment"
/>
</Form.Item>
<Form.Item label="Description" name="description">
<Input.TextArea
data-attr="experiment-description"
className="ph-ignore-input"
placeholder="Adding a helpful description can ensure others know what this experiment is about."
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="Select participants" name="person-selection">
<Col>
<div className="text-muted">
Select the entities who will participate in this experiment. If no
filters are set, 100% of participants will be targeted.
</div>
<div style={{ flex: 3, marginRight: 5 }}>
<PropertyFilters
endpoint="person"
pageKey={'EditFunnel-property'}
propertyFilters={
(experimentInsightType === InsightType.FUNNELS
? funnelsFilters.properties
: trendsFilters.properties) || []
}
onChange={(anyProperties) => {
setNewExperimentData({
filters: {
properties: anyProperties as PropertyFilter[],
},
})
setFilters({
properties: anyProperties.filter(isValidPropertyFilter),
})
}}
style={{ margin: '1rem 0 0' }}
taxonomicGroupTypes={[
TaxonomicFilterGroupType.PersonProperties,
TaxonomicFilterGroupType.CohortsWithAllUsers,
]}
popoverPlacement="top"
taxonomicPopoverPlacement="auto"
/>
</div>
</Col>
</Form.Item>
{newExperimentData?.parameters?.feature_flag_variants && (
<Col>
<label>
<b>Experiment groups</b>
</label>
<div className="text-muted">
Participants are divided into experiment groups. All experiments must
consist of a control group and at least one test group.
</div>
<Col>
{newExperimentData.parameters.feature_flag_variants.map(
(variant: MultivariateFlagVariant, idx: number) => (
<Form
key={`${variant}-${idx}`}
initialValues={
newExperimentData.parameters?.feature_flag_variants
}
onValuesChange={(changedValues) => {
updateExperimentGroup(changedValues, idx)
}}
validateTrigger={['onChange', 'onBlur']}
>
<Row className="feature-flag-variant">
<Form.Item
name="key"
rules={[
{
required: true,
message: 'Key should not be empty.',
},
{
pattern: /^([A-z]|[a-z]|[0-9]|-|_)+$/,
message:
'Only letters, numbers, hyphens (-) & underscores (_) are allowed.',
},
]}
>
<Input
disabled={idx === 0}
defaultValue={variant.key}
data-attr="feature-flag-variant-key"
data-key-index={idx.toString()}
className="ph-ignore-input"
style={{ maxWidth: 150 }}
placeholder={`example-variant-${idx + 1}`}
autoComplete="off"
autoCapitalize="off"
autoCorrect="off"
spellCheck={false}
/>
</Form.Item>
<div className="ml-05">
{' '}
Roll out to{' '}
<InputNumber
disabled={true}
defaultValue={variant.rollout_percentage}
value={variant.rollout_percentage}
formatter={(value) => `${value}%`}
/>{' '}
of <b>participants</b>
</div>
<div className="float-right">
{!(idx === 0 || idx === 1) && (
<Tooltip
title="Delete this variant"
placement="bottomLeft"
>
<Button
type="link"
icon={<DeleteOutlined />}
onClick={() =>
removeExperimentGroup(idx)
}
style={{
color: 'var(--danger)',
float: 'right',
}}
/>
</Tooltip>
)}
</div>
</Row>
</Form>
)
)}
{newExperimentData.parameters.feature_flag_variants.length < 4 && (
<Button
style={{
color: 'var(--primary)',
border: 'none',
boxShadow: 'none',
marginTop: '1rem',
}}
icon={<PlusOutlined />}
onClick={() => addExperimentGroup()}
>
Add test group
</Button>
)}
</Col>
</Col>
)}
</Col>
</Row>
<div>
<Row className="metrics-selection">
<BindLogic logic={insightLogic} props={insightProps}>
<Row style={{ width: '100%' }}>
<Col span={8} style={{ paddingRight: 8 }}>
<div className="mb">
<b>Goal type</b>
</div>
<Select
style={{ display: 'flex' }}
defaultValue={experimentInsightType}
onChange={setExperimentInsightType}
suffixIcon={<CaretDownOutlined />}
dropdownMatchSelectWidth={false}
>
<Select.Option value={InsightType.TRENDS}>
<Col>
<span>
<b>Trend</b>
</span>
<div>
Track how many participants complete a specific event or
action
</div>
</Col>
</Select.Option>
<Select.Option value={InsightType.FUNNELS}>
<Col>
<span>
<b>Funnel</b>
</span>
<div>Track conversion rates between events and actions</div>
</Col>
</Select.Option>
</Select>
<div className="mb mt">
<b>Experiment goal</b>
</div>
<Row>
<Card
className="action-filters-bordered"
style={{ width: '100%', marginRight: 8 }}
bodyStyle={{ padding: 0 }}
>
{experimentInsightType === InsightType.FUNNELS && (
<ActionFilter
filters={funnelsFilters}
setFilters={(payload) => {
setNewExperimentData({ filters: payload })
setFilters(payload)
}}
typeKey={`EditFunnel-action`}
hideMathSelector={true}
hideDeleteBtn={filterSteps.length === 1}
buttonCopy="Add funnel step"
showSeriesIndicator={!isStepsEmpty}
seriesIndicatorType="numeric"
fullWidth
sortable
showNestedArrow={true}
propertiesTaxonomicGroupTypes={[
TaxonomicFilterGroupType.EventProperties,
TaxonomicFilterGroupType.PersonProperties,
TaxonomicFilterGroupType.Cohorts,
TaxonomicFilterGroupType.Elements,
]}
rowClassName="action-filters-bordered"
/>
)}
{experimentInsightType === InsightType.TRENDS && (
<ActionFilter
horizontalUI
filters={trendsFilters}
setFilters={(payload: Partial<FilterType>) => {
setNewExperimentData({ filters: payload })
setFilters(payload)
}}
typeKey={`experiment-trends`}
buttonCopy="Add graph series"
showSeriesIndicator
singleFilter={true}
hideMathSelector={true}
propertiesTaxonomicGroupTypes={[
TaxonomicFilterGroupType.EventProperties,
TaxonomicFilterGroupType.PersonProperties,
TaxonomicFilterGroupType.Cohorts,
TaxonomicFilterGroupType.Elements,
]}
customRowPrefix={
trendsFilters.insight === InsightType.LIFECYCLE ? (
<>
Showing <b>Unique users</b> who did
</>
) : undefined
}
/>
)}
</Card>
</Row>
</Col>
<Col span={16}>
<InsightContainer
disableHeader={experimentInsightType === InsightType.TRENDS}
disableTable={true}
/>
</Col>
</Row>
</BindLogic>
</Row>
</div>
<Card className="experiment-preview">
<Row className="preview-row">
<Col>
<div className="card-secondary">Preview</div>
<div>
<span className="mr-05">
<b>{newExperimentData?.name}</b>
</span>
{newExperimentData?.feature_flag_key && (
<CopyToClipboardInline
explicitValue={newExperimentData.feature_flag_key}
iconStyle={{ color: 'var(--text-muted-alt)' }}
description="feature flag key"
>
<span className="text-muted">
{newExperimentData.feature_flag_key}
</span>
</CopyToClipboardInline>
)}
</div>
</Col>
</Row>
<Row className="preview-row">
{experimentInsightType === InsightType.TRENDS ? (
<>
<Col span={12}>
<div className="card-secondary">Baseline Count</div>
<div className="l4">{trendCount}</div>
</Col>
<Col span={12}>
<div className="card-secondary">Recommended Duration</div>
<div>
<span className="l4">~{exposure}</span> days
</div>
</Col>
</>
) : (
<>
<Col span={8}>
<div className="card-secondary">Baseline Conversion Rate</div>
<div className="l4">{conversionRate.toFixed(1)}%</div>
</Col>
<Col span={8}>
<div className="card-secondary">Recommended Sample Size</div>
<div className="pb">
<span className="l4">~{sampleSizePerVariant}</span> persons
</div>
</Col>
<Col span={8}>
<div className="card-secondary">Expected Duration</div>
<div>
<span className="l4">~{runningTime}</span> days
</div>
</Col>
</>
)}
</Row>
</Card>
</div>
<Button icon={<SaveOutlined />} className="float-right" type="primary" htmlType="submit">
Save
</Button>
</Form>
</>
) : experimentData ? (
<div className="view-experiment">
<Row className="draft-header">
<Row justify="space-between" align="middle" className="full-width pb">
<Col>
<Row>
<PageHeader
style={{ margin: 0, paddingRight: 8 }}
title={`${experimentData?.name}`}
/>
<CopyToClipboardInline
explicitValue={experimentData.feature_flag_key}
iconStyle={{ color: 'var(--text-muted-alt)' }}
>
<span className="text-muted">{experimentData.feature_flag_key}</span>
</CopyToClipboardInline>
<Tag
style={{ alignSelf: 'center', marginLeft: '1rem' }}
color={statusColors[status()]}
>
<b className="uppercase">{status()}</b>
</Tag>
</Row>
<span className="description">
{experimentData.description || 'There is no description for this experiment.'}
</span>
</Col>
{experimentData && !experimentData.start_date && (
<div>
<Button className="mr-05" onClick={() => setEditExperiment(true)}>
Edit
</Button>
<Button type="primary" onClick={() => launchExperiment()}>
Launch
</Button>
</div>
)}
{experimentData && experimentData.start_date && !experimentData.end_date && (
<Button className="stop-experiment" onClick={() => endExperiment()}>
Stop experiment
</Button>
)}
</Row>
</Row>
<Row className="mb">
<Col span={10} style={{ paddingRight: '1rem' }}>
{showWarning &&
experimentResults &&
((experimentInsightType == InsightType.TRENDS && areCountResultsSignificant) ||
(experimentInsightType == InsightType.FUNNELS &&
areConversionResultsSignificant)) && (
<Row className="significant-results">
<Col span={19} style={{ color: '#497342' }}>
Experiment results are significant. You can end your experiment now or let
it run until completion.
</Col>
<Col span={5}>
<Button style={{ color: '#497342' }} onClick={() => setShowWarning(false)}>
Dismiss
</Button>
</Col>
</Row>
)}
<Col>
<div className="card-secondary">Participants</div>
<div>
{!!experimentData.filters.properties?.length ? (
<div>
{experimentData.filters.properties.map((item) => {
return (
<PropertyFilterButton
key={item.key}
item={item}
greyBadges={true}
/>
)
})}
</div>
) : (
'100% of users'
)}
</div>
</Col>
{experimentInsightType === InsightType.TRENDS ? (
<Col>
<div className="card-secondary mt">Recommended running time</div>
<span>
~{experimentData.parameters?.recommended_running_time}{' '}
<span className="text-muted">days</span>
</span>
</Col>
) : (
<Col>
<div className="card-secondary mt">Recommended sample size</div>
<span>
~{experimentData.parameters?.recommended_sample_size}{' '}
<span className="text-muted">persons</span>
</span>
</Col>
)}
<Col>
<div className="card-secondary mt">Variants</div>
<ul className="variants-list">
{experimentData.parameters?.feature_flag_variants?.map(
(variant: MultivariateFlagVariant, idx: number) => (
<li key={idx}>{variant.key}</li>
)
)}
</ul>
</Col>
<Row>
<Col className="mr">
<div className="card-secondary mt">Start date</div>
{experimentData.start_date ? (
<span>{dayjs(experimentData.start_date).format('D MMM YYYY')}</span>
) : (
<span className="description">Not started yet</span>
)}
</Col>
{experimentData.end_date && (
<Col className="ml">
<div className="card-secondary mt">Completed date</div>
<span>{dayjs(experimentData.end_date).format('D MMM YYYY')}</span>
</Col>
)}
</Row>
</Col>
<Col span={14}>
<div style={{ borderBottom: '1px solid (--border)' }}>
<b>Test that your code works properly for each variant</b>
</div>
<Row justify="space-between">
<div>
Feature flag override for{' '}
<Select
onChange={setCurrentVariant}
defaultValue={'control'}
suffixIcon={<CaretDownOutlined />}
>
{experimentData.parameters.feature_flag_variants?.map(
(variant: MultivariateFlagVariant, idx: number) => (
<Select.Option key={idx} value={variant.key}>
{variant.key}
</Select.Option>
)
)}
</Select>
</div>
<div>
Language <CodeLanguageSelect />
</div>
</Row>
<CodeSnippet language={Language.JavaScript}>
{`posthog.feature_flags.override({'${experimentData.feature_flag_key}': '${currentVariant}'})`}
</CodeSnippet>
<CodeSnippet language={Language.JavaScript} wrap>
{`if (posthog.getFeatureFlag('${
experimentData.feature_flag_key ?? ''
}') === '${currentVariant}') {
// where '${currentVariant}' is the variant, run your code here
}`}
</CodeSnippet>
<a
target="_blank"
rel="noopener noreferrer"
href="https://posthog.com/docs/user-guides/feature-flags"
>
<Row align="middle">
Experiment implementation guide
<IconOpenInNew className="ml-05" />
</Row>
</a>
</Col>
</Row>
<div className="experiment-result">
{experimentResults ? (
<Row justify="space-around" style={{ flexFlow: 'nowrap' }}>
{experimentData.parameters.feature_flag_variants.map(
(variant: MultivariateFlagVariant, idx: number) => (
<Col key={idx} className="pr">
<div style={{ fontSize: 16 }}>
<b>{capitalizeFirstLetter(variant.key)}</b>
</div>
{experimentInsightType === InsightType.FUNNELS
? 'Conversion rate: '
: 'Count: '}
<b>
{experimentInsightType === InsightType.FUNNELS
? `${conversionRateForVariant(variant.key)}%`
: countDataForVariant(variant.key)}
</b>
{experimentInsightType === InsightType.FUNNELS && (
<>
<Progress
percent={Number(conversionRateForVariant(variant.key))}
size="small"
showInfo={false}
strokeColor={getSeriesColor(idx + 1)}
/>
<div>
Probability that this variant has higher conversion than other
variants:{' '}
<b>
{(experimentResults.probability[variant.key] * 100).toFixed(
1
)}
%
</b>
</div>
</>
)}
</Col>
)
)}
</Row>
) : experimentResultsLoading ? (
<div className="text-center">
<Spinner />
</div>
) : (
<span style={{ fontWeight: 500 }}>
There are no results for this experiment yet.{' '}
{!experimentData.start_date && 'Launch this experiment to start it!'}
</span>
)}
{experimentResults ? (
<BindLogic
logic={insightLogic}
props={{
dashboardItemId: experimentResults.itemID,
filters: {
...experimentResults.filters,
insight: experimentInsightType,
display: experimentData.filters.display,
...(experimentInsightType === InsightType.FUNNELS && {
layout: FunnelLayout.vertical,
funnel_viz_type: FunnelVizType.Steps,
}),
...(experimentInsightType === InsightType.TRENDS && {
display: ChartDisplayType.ActionsLineGraphCumulative,
}),
},
cachedResults: experimentResults.insight,
syncWithUrl: false,
doNotLoad: true,
}}
>
<div className="mt">
<InsightContainer
disableHeader={experimentInsightType === InsightType.TRENDS}
disableTable={experimentInsightType === InsightType.FUNNELS}
/>
</div>
</BindLogic>
) : (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: 16,
background: '#FAFAF9',
border: '1px solid var(--border)',
width: '100%',
minHeight: 320,
fontSize: 24,
}}
>
{experimentResultsLoading ? (
<Spinner />
) : (
<b>There are no results for this experiment yet.</b>
)}
</div>
)}
</div>
</div>
) : (
<div>Loading Data...</div>
)}
</>
)
}
Example #28
Source File: index.tsx From fe-v5 with Apache License 2.0 | 4 votes |
export default function index(props: IProps) {
const { preNamePrefix = [], namePrefix = ['options', 'valueMappings'] } = props;
return (
<Panel header='值映射'>
<Form.List name={namePrefix}>
{(fields, { add, remove }) => (
<>
<Button
style={{ width: '100%', marginBottom: 10 }}
onClick={() => {
add({
type: 'special',
});
}}
>
添加
</Button>
{_.isEmpty(fields) ? null : (
<Row gutter={10}>
<Col flex='290px'>
<Tooltip
overlayInnerStyle={{
width: 300,
}}
title={
<div>
<div>范围值说明: from >= value <= to</div>
<div>范围值默认值: from=-Infinity; to=Infinity </div>
<div>特殊值Null说明: 匹配值为 null 或 undefined 或 no data</div>
</div>
}
>
条件 <InfoCircleOutlined />
</Tooltip>
</Col>
<Col flex='210'>显示文字</Col>
<Col flex='45'>颜色</Col>
<Col flex='50'>操作</Col>
</Row>
)}
{fields.map(({ key, name, ...restField }) => {
return (
<Row key={key} gutter={10} style={{ marginBottom: 10 }}>
<Col flex='290px'>
<Row gutter={10}>
<Col flex='80px'>
<Form.Item noStyle {...restField} name={[name, 'type']}>
<Select suffixIcon={<CaretDownOutlined />} style={{ width: 80 }}>
<Select.Option value='special'>固定值</Select.Option>
<Select.Option value='range'>范围值</Select.Option>
<Select.Option value='specialValue'>特殊值</Select.Option>
</Select>
</Form.Item>
</Col>
<Col flex='1'>
<Form.Item noStyle {...restField} shouldUpdate={(prevValues, curValues) => _.get(prevValues, [name, 'type']) !== _.get(curValues, [name, 'type'])}>
{({ getFieldValue }) => {
const type = getFieldValue([...preNamePrefix, ...namePrefix, name, 'type']);
if (type === 'special') {
return (
<Form.Item noStyle {...restField} name={[name, 'match', 'special']}>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
);
}
if (type === 'range') {
return (
<Row gutter={10}>
<Col span={12}>
<Form.Item noStyle {...restField} name={[name, 'match', 'from']}>
<InputNumber placeholder='from' />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item noStyle {...restField} name={[name, 'match', 'to']}>
<InputNumber placeholder='to' />
</Form.Item>
</Col>
</Row>
);
}
if (type === 'specialValue') {
return (
<Form.Item noStyle {...restField} name={[name, 'match', 'specialValue']}>
<Select suffixIcon={<CaretDownOutlined />}>
<Select.Option value='null'>Null</Select.Option>
<Select.Option value='empty'>Empty string</Select.Option>
</Select>
</Form.Item>
);
}
return null;
}}
</Form.Item>
</Col>
</Row>
</Col>
<Col flex='210'>
<Form.Item noStyle {...restField} name={[name, 'result', 'text']}>
<Input placeholder='可选' />
</Form.Item>
</Col>
<Col flex='45'>
<Form.Item noStyle {...restField} name={[name, 'result', 'color']}>
<ColorPicker />
</Form.Item>
</Col>
<Col flex='50'>
<Button
onClick={() => {
remove(name);
}}
icon={<DeleteOutlined />}
/>
</Col>
</Row>
);
})}
</>
)}
</Form.List>
</Panel>
);
}
Example #29
Source File: index.tsx From dashboard with Apache License 2.0 | 4 votes |
ContactWayList: React.FC = () => {
const [currentGroup, setCurrentGroup] = useState<ContactWayGroupItem>({});
const [itemDetailVisible, setItemDetailVisible] = useState(false);
const [currentItem, setCurrentItem] = useState<ContactWayItem>({});
const [selectedItems, setSelectedItems] = useState<ContactWayItem[]>([]);
const [filterGroupID, setFilterGroupID] = useState('0');
const [groupItems, setGroupItems] = useState<ContactWayGroupItem[]>([]);
const [groupItemsTimestamp, setGroupItemsTimestamp] = useState(Date.now);
const [createGroupVisible, setCreateGroupVisible] = useState(false);
const [batchUpdateVisible, setBatchUpdateVisible] = useState(false);
const [editGroupVisible, setEditGroupVisible] = useState(false);
const [allStaffs, setAllStaffs] = useState<StaffOption[]>([]);
const actionRef = useRef<ActionType>();
function showDeleteGroupConfirm(item: ContactWayGroupItem) {
Modal.confirm({
title: `删除分组`,
content: `是否确认删除「${item.name}」分组?`,
// icon: <ExclamationCircleOutlined/>,
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk() {
return HandleRequest({ids: [item.id]}, DeleteGroup, () => {
setGroupItemsTimestamp(Date.now);
});
},
});
}
useEffect(() => {
QuerySimpleStaffs({page_size: 5000}).then((res) => {
if (res.code === 0) {
setAllStaffs(
res?.data?.items?.map((item: SimpleStaffInterface) => {
return {
label: item.name,
value: item.ext_id,
...item,
};
}) || [],
);
} else {
message.error(res.message);
}
});
}, []);
useEffect(() => {
QueryGroup({page_size: 1000, sort_field: 'sort_weight', sort_type: 'asc'})
.then((resp) => {
if (resp && resp.data && resp.data.items) {
setGroupItems(resp.data.items);
}
})
.catch((err) => {
message.error(err);
});
}, [groupItemsTimestamp]);
const columns: ProColumns<ContactWayItem>[] = [
{
title: 'ID',
dataIndex: 'id',
valueType: 'text',
hideInTable: true,
hideInSearch: true,
fixed:'left',
},
{
title: '渠道码',
dataIndex: 'qr_code',
valueType: 'image',
hideInSearch: true,
width: 80,
fixed:'left',
render: (dom, item) => {
return (
<div className={'qrcodeWrapper'}>
<img
src={item.qr_code}
onClick={() => {
setItemDetailVisible(true);
setCurrentItem(item);
}}
className={'qrcode clickable'}
alt={item.name}
/>
</div>
);
},
},
{
title: '名称',
dataIndex: 'name',
valueType: 'text',
fixed:'left',
},
{
title: '使用员工',
dataIndex: 'staffs',
valueType: 'text',
hideInSearch: true,
width: 210,
render: (_, item) => {
let staffs: any[] = [];
item.schedules?.forEach((schedule) => {
if (schedule.staffs) {
staffs = [...staffs, ...schedule.staffs];
}
});
if (item.schedule_enable === True) {
staffs = uniqWith(staffs, (a, b) => a.ext_staff_id === b.ext_staff_id);
return <CollapsedStaffs limit={2} staffs={staffs}/>;
}
return <CollapsedStaffs limit={2} staffs={item.staffs}/>;
},
},
{
title: '使用员工',
dataIndex: 'ext_staff_ids',
valueType: 'text',
hideInTable: true,
renderFormItem: () => {
return <StaffTreeSelect options={allStaffs}/>;
},
},
{
title: '备份员工',
dataIndex: 'backup_staffs',
valueType: 'text',
hideInSearch: true,
width: 210,
render: (_, item) => {
return <CollapsedStaffs limit={2} staffs={item.backup_staffs}/>;
},
},
{
title: '标签',
dataIndex: 'customer_tags',
valueType: 'text',
ellipsis: true,
hideInSearch: true,
width: 210,
render: (_, item) => {
return <CollapsedTags limit={3} tags={item.customer_tags}/>;
},
},
{
title: '添加人次',
dataIndex: 'add_customer_count',
valueType: 'digit',
hideInSearch: true,
sorter: true,
showSorterTooltip: false,
width: 120,
tooltip: '统计添加渠道码的人次,若客户重复添加将会记录多条数据',
},
{
title: '创建时间',
dataIndex: 'created_at',
valueType: 'dateRange',
sorter: true,
filtered: true,
render: (dom, item) => {
return (
<div
dangerouslySetInnerHTML={{
__html: moment(item.created_at).format('YYYY-MM-DD HH:mm').split(' ').join('<br />'),
}}
/>
);
},
},
{
title: '操作',
width: 180,
valueType: 'option',
render: (_, item) => [
<a
key='detail'
onClick={() => {
setItemDetailVisible(true);
setCurrentItem(item);
}}
>
详情
</a>,
<a
key='download'
onClick={() => {
if (item?.qr_code) {
FileSaver.saveAs(item?.qr_code, `${item.name}.png`);
}
}}
>
下载
</a>,
<Dropdown
key='more'
overlay={
<Menu>
<Menu.Item
key='edit'
onClick={() => {
history.push(`/staff-admin/customer-growth/contact-way/edit?id=${item.id}`);
}}
>
修改
</Menu.Item>
<Menu.Item
key='copy'
onClick={() => {
history.push(`/staff-admin/customer-growth/contact-way/copy?id=${item.id}`);
}}
>
复制
</Menu.Item>
{item.ext_creator_id === localStorage.getItem(LSExtStaffAdminID) && (
<Menu.Item
key='delete'
onClick={() => {
Modal.confirm({
title: `删除渠道码`,
content: `是否确认删除「${item.name}」渠道码?`,
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk() {
return HandleRequest({ids: [item.id]}, Delete, () => {
actionRef.current?.clearSelected?.();
actionRef.current?.reload?.();
});
},
});
}}
>删除</Menu.Item>
)}
</Menu>
}
trigger={['hover']}
>
<a style={{display: 'flex', alignItems: 'center'}}>
编辑
<CaretDownOutlined style={{fontSize: '8px', marginLeft: '3px'}}/>
</a>
</Dropdown>,
],
},
];
// @ts-ignore
// @ts-ignore
return (
<PageContainer
fixedHeader
header={{
title: '渠道活码列表',
subTitle: (
<a
target={'_blank'}
className={styles.tipsLink}
// href={'https://www.openscrm.cn/wiki/contact-way'}
>
什么是渠道活码?
</a>
),
}}
extra={[
<Button
key='create'
type='primary'
icon={<PlusOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
onClick={() => {
history.push('/staff-admin/customer-growth/contact-way/create');
}}
>
新建活码
</Button>,
]}
>
<ProTable<ContactWayItem>
actionRef={actionRef}
className={'table'}
scroll={{x: 'max-content'}}
columns={columns}
rowKey='id'
pagination={{
pageSizeOptions: ['5', '10', '20', '50', '100'],
pageSize: 5,
}}
toolBarRender={false}
bordered={false}
tableAlertRender={false}
rowSelection={{
onChange: (_, items) => {
setSelectedItems(items);
},
}}
tableRender={(_, dom) => (
<div className={styles.mixedTable}>
<div className={styles.leftPart}>
<div className={styles.header}>
<Button
key='1'
className={styles.button}
type='text'
onClick={() => setCreateGroupVisible(true)}
icon={<PlusSquareFilled style={{color: 'rgb(154,173,193)', fontSize: 15}}/>}
>
新建分组
</Button>
</div>
<Menu
onSelect={(e) => {
setFilterGroupID(e.key as string);
}}
defaultSelectedKeys={['0']}
mode='inline'
className={styles.menuList}
>
<Menu.Item
icon={<FolderFilled style={{fontSize: '16px', color: '#138af8'}}/>}
key='0'
>
全部
</Menu.Item>
{groupItems.map((item) => (
<Menu.Item
icon={<FolderFilled style={{fontSize: '16px', color: '#138af8'}}/>}
key={item.id}
>
<div className={styles.menuItem}>
{item.name}
<span className={styles.count}
style={{marginRight: item.is_default === True ? 16 : 0}}>{item.count}</span>
</div>
{item.is_default === False && (
<Dropdown
className={'more-actions'}
overlay={
<Menu
onClick={(e) => {
e.domEvent.preventDefault();
e.domEvent.stopPropagation();
}}
>
<Menu.Item
onClick={() => {
setCurrentGroup(item);
setEditGroupVisible(true);
}}
key='edit'
>
修改名称
</Menu.Item>
<Menu.Item
onClick={() => {
showDeleteGroupConfirm(item);
}}
key='delete'
>
删除分组
</Menu.Item>
</Menu>
}
trigger={['hover']}
>
<MoreOutlined style={{color: '#9b9b9b', fontSize: 18}}/>
</Dropdown>
)}
</Menu.Item>
))}
</Menu>
</div>
<div className={styles.rightPart}>
<div className={styles.tableWrap}>{dom}</div>
</div>
</div>
)}
params={{
group_id: filterGroupID !== '0' ? filterGroupID : '',
}}
request={async (params, sort, filter) => {
return ProTableRequestAdapter(params, sort, filter, Query);
}}
dateFormatter='string'
/>
{selectedItems?.length > 0 && (
// 底部选中条目菜单栏
<FooterToolbar>
<span>
已选择 <a style={{fontWeight: 600}}>{selectedItems.length}</a> 项
<span></span>
</span>
<Divider type='vertical'/>
<Button
type='link'
onClick={() => {
actionRef.current?.clearSelected?.();
}}
>
取消选择
</Button>
<Button onClick={() => setBatchUpdateVisible(true)}>批量分组</Button>
<Button
icon={<CloudDownloadOutlined/>}
type={'primary'}
onClick={() => {
Modal.confirm({
title: `批量下载渠道码`,
content: `是否批量下载所选「${selectedItems.length}」个渠道码?`,
okText: '下载',
cancelText: '取消',
onOk: async () => {
const zip = new JSZip();
// eslint-disable-next-line no-restricted-syntax
for (const item of selectedItems) {
if (item?.qr_code) {
// eslint-disable-next-line no-await-in-loop
const img = (await fetch(item?.qr_code)).blob();
zip.file(`${item.name}_${item.id}.png`, img);
}
}
const content = await zip.generateAsync({type: 'blob'});
FileSaver.saveAs(content, `渠道活码_${moment().format('YYYY_MM_DD')}.zip`);
actionRef.current?.clearSelected?.();
return true;
},
});
}}
>
批量下载
</Button>
<Button
icon={<DeleteOutlined/>}
onClick={async () => {
Modal.confirm({
title: `删除渠道码`,
content: `是否批量删除所选「${selectedItems.length}」个渠道码?`,
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk() {
return HandleRequest(
{ids: selectedItems.map((item) => item.id)},
Delete,
() => {
actionRef.current?.clearSelected?.();
actionRef.current?.reload?.();
},
);
},
});
}}
danger={true}
>
批量删除
</Button>
</FooterToolbar>
)}
<ModalForm
width={468}
className={'dialog from-item-label-100w'}
layout={'horizontal'}
visible={batchUpdateVisible}
onVisibleChange={setBatchUpdateVisible}
onFinish={async (values) => {
return await HandleRequest(
{ids: selectedItems.map((item) => item.id), ...values},
BatchUpdate,
() => {
actionRef.current?.clearSelected?.();
actionRef.current?.reload?.();
setGroupItemsTimestamp(Date.now);
},
);
}}
>
<h2 className='dialog-title'> 批量修改渠道码 </h2>
<ProFormSelect
// @ts-ignore
options={groupItems.map((groupItem) => {
return {key: groupItem.id, label: groupItem.name, value: groupItem.id};
})}
labelAlign={'left'}
name='group_id'
label='新分组'
placeholder='请选择分组'
rules={[
{
required: true,
message: '请选择新分组',
},
]}
/>
</ModalForm>
<ModalForm
width={400}
className={'dialog from-item-label-100w'}
layout={'horizontal'}
visible={createGroupVisible}
onVisibleChange={setCreateGroupVisible}
onFinish={async (params) =>
HandleRequest({...currentGroup, ...params}, CreateGroup, () => {
setGroupItemsTimestamp(Date.now);
})
}
>
<h2 className='dialog-title'> 新建分组 </h2>
<ProFormText
name='name'
label='分组名称'
tooltip='最长为 24 个汉字'
placeholder='请输入分组名称'
rules={[
{
required: true,
message: '请填写分组名称',
},
]}
/>
</ModalForm>
<ModalForm
className={'dialog from-item-label-100w'}
layout={'horizontal'}
width={'500px'}
visible={editGroupVisible}
onVisibleChange={setEditGroupVisible}
onFinish={async (params) =>
HandleRequest({...currentGroup, ...params}, UpdateGroup, () => {
setGroupItemsTimestamp(Date.now);
})
}
>
<h2 className='dialog-title'> 修改名称 </h2>
<ProFormText
colon={true}
name='name'
label='分组名称'
tooltip='最长为 24 个汉字'
placeholder='请输入分组名称'
initialValue={currentGroup.name}
rules={[
{
required: true,
message: '请填写分组名称',
},
]}
/>
</ModalForm>
<Modal
className={styles.detailDialog}
width={'800px'}
visible={itemDetailVisible}
onCancel={() => setItemDetailVisible(false)}
footer={null}
>
<h2 className='dialog-title' style={{textAlign: "center", fontSize: 19}}> 渠道码详情 </h2>
<Row>
<Col span={8} className={styles.leftPart}>
<img src={currentItem.qr_code}/>
<h3>{currentItem.name}</h3>
<Button
type={'primary'}
onClick={() => {
if (currentItem?.qr_code) {
FileSaver.saveAs(currentItem?.qr_code, `${currentItem.name}.png`);
}
}}
>
下载渠道码
</Button>
<Button
onClick={() => {
history.push(
`/staff-admin/customer-growth/contact-way/edit?id=${currentItem.id}`,
);
}}
>
修改
</Button>
</Col>
<Col span={16} className={styles.rightPart}>
<div className={styles.section}>
<div className={styles.titleWrapper}>
<div className={styles.divider}/>
<span className={styles.title}>基本设置</span>
</div>
<div className={styles.formItem}>
<span className={styles.title}>创建时间:</span>
<span className='date'>
{moment(currentItem.created_at).format('YYYY-MM-DD HH:mm')}
</span>
</div>
<div className={styles.formItem}>
<span className={styles.title}>绑定员工:</span>
{currentItem.staffs?.map((staff) => (
<Tag
key={staff.id}
className={styles.staffTag}
style={{opacity: staff.online === False ? '0.5' : '1'}}
>
<img className={styles.icon} src={staff.avatar_url} alt={staff.name}/>
<span className={styles.text}>{staff.name}</span>
</Tag>
))}
</div>
<div className={styles.formItem}>
<span className={styles.title}>备份员工:</span>
{currentItem.backup_staffs?.map((staff) => (
<Tag
key={staff.id}
className={styles.staffTag}
style={{opacity: staff.online === False ? '0.5' : '1'}}
>
<img className={styles.icon} src={staff.avatar_url} alt={staff.name}/>
<span className={styles.text}>{staff.name}</span>
</Tag>
))}
</div>
<p className={styles.formItem}>
<span className={styles.title}>自动通过好友:</span>
{currentItem.auto_skip_verify_enable === True && (
<span>
{currentItem.skip_verify_start_time && '~'}
{currentItem.skip_verify_end_time}自动通过
</span>
)}
{currentItem.auto_skip_verify_enable === False && <span>未开启</span>}
</p>
<p className={styles.formItem}>
<span className={styles.title}>客户标签:</span>
{currentItem.customer_tags?.map((tag) => (
<Tag key={tag.id} className={styles.staffTag}>
<span className={styles.text}>{tag.name}</span>
</Tag>
))}
</p>
</div>
</Col>
</Row>
</Modal>
</PageContainer>
);
}