lodash#concat TypeScript Examples
The following examples show how to use
lodash#concat.
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: util.ts From gio-design with Apache License 2.0 | 7 votes |
getResultValue = (value?: (string | number)[], val?: string | number) => {
if (indexOf(value, val) !== -1) {
return difference(value, [val]);
}
if (typeof val === 'string') {
return concat(value, val);
}
return value;
// ? :
}
Example #2
Source File: documentation.ts From ui5-language-assistant with Apache License 2.0 | 6 votes |
function convertJSDoc(
jsdoc: string,
target: ConvertTarget,
model: UI5SemanticModel
): string {
// We add replacements that require the model here (because they cannot be in tagMatcherToReplacement which is defined
// outside of the function).
// Note that they are applied after the replacements defined in tagMatcherToReplacement.
const allTagMatcherToReplacement = concat(tagMatcherToReplacement, [
// Links
// Assuming links are of the form: {@link <type>[ <text>]}
// Where the type doesn't contain whitespace, and neither the type nor text contain the "}" character
{
matcher: /{@link (([^\s}]+)\s)?([^}]+)}/g,
replacement: {
markdown: (all, _, type, text): string => {
return `[${text}](${getLink(model, type ?? text)})`;
},
plaintext: "$3",
},
},
]);
let contents = jsdoc;
forEach(allTagMatcherToReplacement, (_) => {
contents = replace(contents, _.matcher, _.replacement[target]);
});
return contents;
}
Example #3
Source File: section-info-edit.tsx From erda-ui with GNU Affero General Public License v3.0 | 6 votes |
render() {
const { modalVisible, saveDisabled } = this.state;
const { data, fieldsList, hasAuth, readonlyForm, name, desc, formName, extraSections } = this.props;
let sectionList = [
{
title: name,
desc,
titleOperate: hasAuth ? (
<Button type="primary" ghost onClick={this.toggleModal}>
{i18n.t('Edit')}
</Button>
) : null,
children: readonlyForm || this.getReadonlyInfo(),
},
];
if (!isEmpty(extraSections)) {
sectionList = concat(sectionList, extraSections);
}
return (
<React.Fragment>
<ConfigLayout sectionList={sectionList} />
<FormModal
width={700}
name={formName || name || ''}
formData={data}
fieldsList={fieldsList}
visible={modalVisible}
onOk={this.handleSubmit}
onCancel={this.toggleModal}
modalProps={{ destroyOnClose: true }}
okButtonState={saveDisabled}
onValuesChange={() => {
this.setState({ saveDisabled: false });
}}
/>
</React.Fragment>
);
}
Example #4
Source File: scenesComps.tsx From yforms with MIT License | 6 votes |
DiffSetFields = (props: modifyType) => {
const { itemProps, formProps } = props;
const { initialValues, oldValues = {} } = formProps;
const context = React.useContext(YForm.ListContent);
const { name } = itemProps;
const allName = context.prefixName ? concat(context.prefixName, name) : name;
const value = get(initialValues, allName, []);
const oldValue = 'oldValue' in itemProps ? itemProps.oldValue : get(oldValues, allName);
return (
<YForm.Items>
{[
{
noStyle: true,
shouldUpdate: (prevValues, curValues) => {
return get(prevValues, allName, []).length !== get(curValues, allName, []).length;
},
children: (form) => (
<DiffSetFieldsChildren form={form} name={allName} value={value} oldValue={oldValue} />
),
},
]}
</YForm.Items>
);
}
Example #5
Source File: tooltip.ts From S2 with MIT License | 6 votes |
getFieldList = (
spreadsheet: SpreadSheet,
fields: string[],
activeData: TooltipDataItem,
): ListItem[] => {
const currFields = filter(
concat([], fields),
(field) => field !== EXTRA_FIELD && activeData[field],
);
const fieldList = map(currFields, (field: string): ListItem => {
return getListItem(spreadsheet, {
data: activeData,
field,
useCompleteDataForFormatter: false,
});
});
return fieldList;
}
Example #6
Source File: scenesComps.tsx From yforms with MIT License | 6 votes |
DiffSetFieldsChildren = (props: {
value: any[];
oldValue: any[];
name: YFormItemProps['name'];
form: YFormItemsProps['form'];
}) => {
const {
name,
value = [],
form: { setFields },
oldValue = [],
} = props;
const diffLength = oldValue.length - value.length;
useEffect(() => {
if (diffLength > 0) {
setFields([{ name, value: concat(value, new Array(diffLength).fill(null)) }]);
}
}, [name, diffLength, value, setFields]);
return null;
}
Example #7
Source File: Diff.tsx From yforms with MIT License | 5 votes |
DiffDom = (props: modifyType) => {
const { formProps, itemProps, componentProps } = props;
const { oldValues = {} } = formProps;
const { name } = itemProps;
const context = React.useContext(YForm.ListContent);
const allName = context.prefixName ? concat(context.prefixName, name) : name;
const _oldValue = 'oldValue' in itemProps ? itemProps.oldValue : get(oldValues, allName);
return (
<YForm.Items className="diff">
{[
{
noStyle: true,
shouldUpdate: (prevValues, curValues) => {
return get(prevValues, allName) !== get(curValues, allName);
},
children: ({ getFieldValue }) => {
// 如果字段为 undefined 则改为 '',为了字段输入值再删除一样的道理
const value = getFieldValue(allName) === undefined ? '' : getFieldValue(allName);
const oldValue = _oldValue === undefined ? '' : _oldValue;
let equal = value === oldValue;
// 如果有渲染方法,就按照次来对比
if (itemProps.viewProps) {
const { format } = itemProps.viewProps;
if (format) {
// 这里用的 pureValue = true(纯值),直接 === 判断就可以。
equal = format(value, true) === format(oldValue, true);
}
}
if (itemProps.diffProps) {
const { onEqual } = itemProps.diffProps;
if (onEqual) {
// 如果有 onEqual,则最终使用该方法判断
equal = onEqual(value, oldValue);
}
}
if (equal) {
return null;
}
return (
<div style={{ padding: '5px 0' }}>
<YForm.YFormItemContext.Provider value={itemProps}>
<ComponentView
{...componentProps}
oldValue={oldValue}
className={classNames('old-value', componentProps.className)}
/>
</YForm.YFormItemContext.Provider>
</div>
);
},
},
]}
</YForm.Items>
);
}
Example #8
Source File: index.ts From qiankun with MIT License | 5 votes |
// 在生命周期函数中设置和管理 global.__POWERED_BY_QIANKUN__ 和 global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
export default function getAddOns<T extends object>(global: Window, publicPath: string): FrameworkLifeCycles<T> {
return mergeWith({}, getEngineFlagAddon(global), getRuntimePublicPathAddOn(global, publicPath), (v1, v2) =>
concat(v1 ?? [], v2 ?? []),
);
}
Example #9
Source File: project-stats.component.ts From barista with Apache License 2.0 | 5 votes |
ngOnInit() {
this.isLoadingLicenseData = true;
this.isLoadingSeverityData = true;
this.isLoadingVulnerabilityData = true;
if (this.licenseData$) {
this.licenseData$
.pipe(
first(),
map(items => {
const data: ChartElementDto[] = _.map(items, (item: any) => {
return {'name': item.license.name, 'value': Number(item.count)};
});
data.sort((x, y) => {
// inverted so that higher numbers are first
return -(x.value - y.value);
});
return data;
}),
)
.subscribe(data => {
this.licenseData = data;
this.isLoadingLicenseData = false;
});
}
if (this.vulnerabilityData$) {
this.vulnerabilityData$
.pipe(
first(),
map(items => {
const data: ChartElementDto[] = _.map(items, (item: any) => {
return {'name': item.path, 'value': Number(item.count)};
});
data.sort((x, y) => {
// inverted so that higher numbers are first
return -(x.value - y.value);
});
return data;
}),
)
.subscribe(data => {
this.vulnerabilityData = data;
this.isLoadingVulnerabilityData = false;
});
}
let severityLabels: string[] = ['LOW', 'MODERATE', 'MEDIUM', 'HIGH', 'CRITICAL'];
if (this.severityData$) {
this.severityData$
.pipe(
first(),
map(items => {
var data: ChartElementDto[] = _.map(items, (item: any) => {
return {'name': item.severity, 'value': Number(item.count)};
});
if(data.length !== 0){
let dataNames: string[] = data.map((item) => item.name.toUpperCase());
let result: string[] = severityLabels.filter(item => dataNames.indexOf(item) < 0);
return data
.concat(result.map((item) => {
return {'name': item.toUpperCase(), 'value': 0}
}))
.sort((a, b) => {
return -(severityLabels.indexOf(a.name) - severityLabels.indexOf(b.name))
});
}
}),
)
.subscribe(data => {
this.severityData = data;
this.isLoadingSeverityData = false;
});
}
}
Example #10
Source File: root.ts From S2 with MIT License | 5 votes |
public selectHeaderCell = (
selectHeaderCellInfo: SelectHeaderCellInfo = {} as SelectHeaderCellInfo,
) => {
const { cell } = selectHeaderCellInfo;
if (isEmpty(cell)) {
return;
}
const currentCellMeta = cell?.getMeta?.() as Node;
if (!currentCellMeta || isNil(currentCellMeta?.x)) {
return;
}
this.addIntercepts([InterceptType.HOVER]);
const isHierarchyTree = this.spreadsheet.isHierarchyTreeType();
const isColCell = cell?.cellType === CellTypes.COL_CELL;
const lastState = this.getState();
const isSelectedCell = this.isSelectedCell(cell);
const isMultiSelected =
selectHeaderCellInfo?.isMultiSelection && this.isSelectedState();
// 如果是已选中的单元格, 则取消选中, 兼容行列多选 (含叶子节点)
let leafNodes = isSelectedCell ? [] : this.getCellLeafNodes(cell);
let selectedCells = isSelectedCell ? [] : [getCellMeta(cell)];
if (isMultiSelected) {
selectedCells = concat(lastState?.cells, selectedCells);
leafNodes = concat(lastState?.nodes, leafNodes);
if (isSelectedCell) {
selectedCells = selectedCells.filter(
({ id }) => id !== currentCellMeta.id,
);
leafNodes = leafNodes.filter(
(node) => !node?.id.includes(currentCellMeta.id),
);
}
}
if (isEmpty(selectedCells)) {
this.reset();
this.spreadsheet.emit(S2Event.GLOBAL_SELECTED, this.getActiveCells());
return;
}
// 兼容行列多选 (高亮 行/列头 以及相对应的数值单元格)
this.changeState({
cells: selectedCells,
nodes: leafNodes,
stateName: InteractionStateName.SELECTED,
});
const selectedCellIds = selectedCells.map(({ id }) => id);
this.updateCells(this.getRowColActiveCells(selectedCellIds));
// 平铺模式或者是树状模式下的列头单元格, 选中子节点
if (!isHierarchyTree || isColCell) {
leafNodes.forEach((node) => {
node?.belongsCell?.updateByState(
InteractionStateName.SELECTED,
node.belongsCell,
);
});
}
this.spreadsheet.emit(S2Event.GLOBAL_SELECTED, this.getActiveCells());
return true;
};
Example #11
Source File: root.ts From S2 with MIT License | 5 votes |
public getAllCells() {
return concat<S2CellType>(
this.getPanelGroupAllDataCells(),
this.getAllRowHeaderCells(),
this.getAllColHeaderCells(),
);
}
Example #12
Source File: root.ts From S2 with MIT License | 5 votes |
public getRowColActiveCells(ids: string[]) {
return concat<S2CellType>(
this.getAllRowHeaderCells(),
this.getAllColHeaderCells(),
).filter((item) => ids.includes(item.getMeta().id));
}
Example #13
Source File: root.ts From S2 with MIT License | 5 votes |
public setInteractedCells(cell: S2CellType) {
const interactedCells = this.getInteractedCells().concat([cell]);
const state = this.getState();
state.interactedCells = interactedCells;
this.setState(state);
}
Example #14
Source File: Item.tsx From yforms with MIT License | 4 votes |
Item: React.FC<YFormDataSource> = (props) => {
// 这里解析出来的参数最好不要在 scenes 中更改
const { scenes, ...rest } = props;
const { name, children } = rest;
const formProps = useContext(YForm.YFormContext);
const {
itemsType = {},
onDeFormatFieldsValue,
oldValues,
getScene,
onFormatFieldsValue,
} = formProps;
const itemsProps = useContext(YForm.YFormItemsContext);
const { scenes: thisScenes } = itemsProps;
const listContext = useContext(YForm.ListContent);
const { prefixName } = listContext;
// List 会有拼接 name ,这里获取 all name path
const allName = prefixName ? concat(prefixName, name) : name;
const mergeProps = mergeWithDom(
{},
pick(formProps, ['scenes', 'offset', 'disabled']),
itemsProps,
props,
);
if ('isShow' in props && !props.isShow) return null;
const _scenes = mergeWithDom({}, thisScenes, scenes);
let _props = mergeWithDom({}, mergeProps, rest, {
offset: (props.offset || 0) + (itemsProps.offset || 0),
});
let _componentProps = { ...props.componentProps };
const typeProps = get(itemsType, props.type) || {};
// 原类型
typeProps.type = props.type;
const defaultData = {
formProps,
itemsProps: mergeProps,
itemProps: _props,
componentProps: _componentProps,
typeProps,
};
// 参数修改
const _defaultData = defaultData;
const { modifyProps } = typeProps;
if (modifyProps) {
mergeWithDom(_defaultData, modifyProps(defaultData));
}
mapKeys(_scenes, (value: boolean, key: string) => {
if (value && getScene[key] && getScene[key].item) {
const data = getScene[key].item(_defaultData);
if (data) {
_defaultData.itemProps = { ..._defaultData.itemProps, ...data.itemProps };
_defaultData.componentProps = { ..._defaultData.componentProps, ...data.componentProps };
}
}
});
_props = { ..._defaultData.itemProps };
_componentProps = _defaultData.componentProps;
const { type, dataSource, componentProps, format, ...formItemProps } = _props;
const _formItemProps = formItemProps;
const { isShow, shouldUpdate } = _formItemProps;
const { deFormat } = _defaultData.itemProps;
// 获取前格式化
if (deFormat) {
onDeFormatFieldsValue({ name: allName, format: deFormat });
if (oldValues && _scenes.diff) {
_defaultData.itemProps = {
oldValue: deFormat(
get(oldValues, allName),
getParentNameData(oldValues, allName),
oldValues,
),
..._defaultData.itemProps,
};
}
}
// 提交前格式化
if (format) {
let _format = [];
if (typeof format === 'function') {
_format = [{ name: allName, format }];
} else {
_format = map(format, (item) => {
const _item = { ...item };
if (item.name) {
_item.name = prefixName ? concat(prefixName, item.name) : item.name;
}
return _item;
});
}
onFormatFieldsValue(_format);
}
let _children;
// 默认用 FormItem 包裹
let _hasFormItem = true;
const thisComponentProps = _componentProps;
if (type) {
const _fieldData = itemsType[type];
if (_fieldData) {
const { component } = _fieldData;
_hasFormItem = 'hasFormItem' in _fieldData ? _fieldData.hasFormItem : _hasFormItem;
const _component = children || component;
_children = isValidElement(_component)
? React.cloneElement(_component, {
...(_component.props as Record<string, any>),
...thisComponentProps,
})
: _component;
} else {
warning(false, `[YFom.Items] ${type} 类型未找到`);
}
} else {
// 没有 type 单独有 dataSource 情况
if (dataSource) {
_children = (
<Items scenes={_scenes} {...thisComponentProps}>
{dataSource}
</Items>
);
} else {
_children = isValidElement(children)
? React.cloneElement(children, { ...children.props, ...thisComponentProps })
: children;
}
}
const domChildren =
typeof _children === 'function'
? (form: YFormInstance) => {
return (
<Items noStyle scenes={_scenes}>
{(_children as YFormRenderChildren)(form)}
</Items>
);
}
: _children;
let dom = domChildren;
if (_hasFormItem) {
dom = (
<ItemChildren
{...omit(_formItemProps, [
'component',
'scenes',
'viewProps',
'deFormat',
'format',
'oldValue',
'items',
'offset',
'hideLable',
])}
>
{domChildren}
</ItemChildren>
);
}
const render = (props?: any) => {
return (
<YForm.YFormItemContext.Provider value={mergeWithDom(omit(_props, ['children']), props)}>
{dom}
</YForm.YFormItemContext.Provider>
);
};
if (shouldUpdate) {
let reRender = false;
return (
<Form.Item noStyle shouldUpdate={shouldUpdate}>
{(form) => {
if (typeof isShow === 'function') {
const fieldsValue = form.getFieldsValue(true);
const parentValue = getParentNameData(fieldsValue, name);
if (!isShow(parentValue, fieldsValue)) {
return;
}
}
reRender = !reRender;
return render({ reRender });
}}
</Form.Item>
);
}
return render();
}
Example #15
Source File: Form.tsx From yforms with MIT License | 4 votes |
InternalForm = React.memo<YFormProps>((thisProps) => {
const props = { ...globalConfig.defaultFormProps, ...thisProps };
const { scenes, getScene = globalConfig.getScene, offset = 0 } = props;
const _scenes = merge({}, globalConfig.scenes, scenes);
const _defaultData = { formProps: props };
mapKeys(_scenes, (value: boolean, key: string) => {
if (value && getScene[key] && getScene[key].form) {
const data = getScene[key].form(_defaultData);
if (data) {
_defaultData.formProps = { ..._defaultData.formProps, ...data.formProps };
}
}
});
const _props = _defaultData.formProps;
const {
disabled,
loading,
itemsType,
children,
onFinish,
onSave,
formatFieldsValue: formFormatFieldsValue,
onCancel,
params,
form: propsForm,
className,
submitComponentProps,
submit,
initialValues,
minBtnLoadingTime = 500,
getInitialValues,
...rest
} = _props;
const [form] = useForm(propsForm);
const formatRef = useRef([]);
const { resetFields, getFieldsValue } = form;
const _params = submit ? submit.params : paramsType(params);
const { create, edit, view } = _params;
// 同 useSubmit 使用 view 当默认值
const [thisDisabled, setDisabled] = useState(view);
const [submitLoading, setSubmitLoading] = useState(false);
const timeOut = useRef<number | null>(null);
// 下面地方请使用 _thisDisabled
let _thisDisabled = thisDisabled;
if (submit) {
_thisDisabled = submit.disabled;
}
// 改变状态
const handleOnDisabled = useCallback(
(disabled) => {
if (submit) {
submit.onDisabled(disabled);
} else {
setDisabled(disabled);
}
},
[submit],
);
const [_getInitialValues, setGetInitialValues] = useState({});
const [getLoading, setGetLoading] = useState(true);
const immutableGetDetail = usePersistFn<YFormProps['getInitialValues']>(getInitialValues);
// 传了 getInitialValues 则使用该数据,没传则使用 initialValues、loading
const _initialValues = getInitialValues ? _getInitialValues : initialValues;
const _loading = getInitialValues ? getLoading : loading;
const hasGetInitialValues = typeof getInitialValues === 'function';
const loadData = useCallback(
async (params: getInitialValuesParamsType) => {
// 只有传了 getInitialValues 调用
if (hasGetInitialValues) {
setGetInitialValues(await immutableGetDetail(params));
setGetLoading(false);
}
},
[hasGetInitialValues, immutableGetDetail],
);
useEffect(() => {
loadData({ isInit: true });
}, [loadData]);
useEffect(() => {
return () => {
clearTimeout(timeOut.current);
};
}, []);
const goBack = () => {
window.history.back();
};
const handleReset: (p: { type: CancelType }) => void = useCallback(
async ({ type }) => {
if (typeof onCancel === 'function') {
onCancel({ type });
} else {
resetFields();
if (create) {
goBack();
} else if (edit || view) {
handleOnDisabled(true);
}
}
},
[create, edit, handleOnDisabled, onCancel, resetFields, view],
);
const itemsTypeAll = { ...baseItemsType, ...globalConfig.itemsType, ...itemsType };
// 内部格式化功能
const { formatFieldsValue, onFormatFieldsValue } = useFormatFieldsValue();
const handleFormatFieldsValue = (value) => {
const _value = value || getFieldsValue(true);
const _formatFieldsValue = concat(formFormatFieldsValue, formatFieldsValue).filter((x) => x);
// 忽略字段
const omitNames = [];
forEach(_formatFieldsValue, (item) => {
if (item.isOmit) omitNames.push(item.name);
});
const formatValues = { ...submitFormatValues(_value, _formatFieldsValue) };
return omit(formatValues, omitNames);
};
if (!form.getFormatFieldsValue) {
form.getFormatFieldsValue = handleFormatFieldsValue;
}
const handleOnFinish = async (value: KeyValue) => {
if (onFinish) {
if (submitLoading) return;
const begin = new Date().getTime();
setSubmitLoading(true);
try {
await onFinish(form.getFormatFieldsValue(value));
await loadData({ isInit: false });
const end = new Date().getTime();
timeOut.current = window.setTimeout(
() => {
setSubmitLoading(false);
handleReset({ type: 'onSubmit' });
},
// loading 时间不到 0.5s 的加载 0.5s,超过的立刻结束。
end - begin > minBtnLoadingTime ? 0 : minBtnLoadingTime,
);
} catch (error) {
console.log('error', error);
setSubmitLoading(false);
}
}
};
const handleOnEdit = (e) => {
e.preventDefault();
handleOnDisabled(false);
};
const {
formatFieldsValue: deFormatFieldsValue,
onFormatFieldsValue: onDeFormatFieldsValue,
} = useFormatFieldsValue();
// deFormatFieldsValue 第一次为空需要下面 set(deFormatValues, name, value) 设置值
// 当执行 resetFields 后,就需要 deFormatFieldsValue 的格式化。
const deFormatValues = submitFormatValues(_initialValues, deFormatFieldsValue);
const handleDeFormatFieldsValue = useCallback(
(data: FormatFieldsValue) => {
const { name, format } = data;
const parentValue = getParentNameData(_initialValues, name);
const value = format(get(_initialValues, name), parentValue, _initialValues);
if (!find(formatRef.current, { name })) {
form.setFields([{ name, value }]);
formatRef.current.push({ name, value });
// 初始化使用 deFormat 后的数据
set(deFormatValues, name, value);
onDeFormatFieldsValue([{ name, format }]);
}
},
[_initialValues, form, deFormatValues, onDeFormatFieldsValue],
);
const providerProps = mergeWithDom(
{
form,
scenes: _scenes,
disabled: _thisDisabled,
getScene,
onFormatFieldsValue,
onDeFormatFieldsValue: handleDeFormatFieldsValue,
submitComponentProps: {
showBtns: {
// form submit 触发后设置 loading = true
showSubmit: { loading: submitLoading },
showEdit: { onClick: handleOnEdit },
showCancel: { onClick: () => handleReset({ type: 'onCancel' }) },
showSave: { onLoaded: () => handleReset({ type: 'onSave' }) },
showBack: { onClick: goBack },
},
},
},
{ ...omit(_props, ['name', 'initialValues']) },
{ initialValues: deFormatValues },
);
if ('isShow' in _props && !_props.isShow) {
return null;
}
if (_loading) {
return (
<div className="form-spin">
<Spin />
</div>
);
}
return (
<Form
{...omit(rest, ['scenes', 'oldValues'])}
initialValues={deFormatValues}
form={form}
className={classNames('yforms', className)}
onFinish={handleOnFinish}
>
<YFormContext.Provider value={{ ...providerProps, itemsType: itemsTypeAll }}>
<Items offset={offset}>{children}</Items>
</YFormContext.Provider>
</Form>
);
})
Example #16
Source File: index.tsx From erda-ui with GNU Affero General Public License v3.0 | 4 votes |
CustomAlarm = ({ scopeType }: { scopeType: string }) => {
const customAlarmStore = customAlarmStoreMap[scopeType];
const monitorMetaDataStore = monitorMetaDataStoreMap[scopeType];
const [switchCustomAlarmLoading, getPreviewMetaDataLoading, getCustomAlarmsLoading, getCustomAlarmDetailLoading] =
useLoading(customAlarmStore, [
'switchCustomAlarm',
'getPreviewMetaData',
'getCustomAlarms',
'getCustomAlarmDetail',
]);
const [extraLoading] = useLoading(monitorMetaDataStore, ['getMetaData']);
const [metaGroups, metaConstantMap, metaMetrics] = monitorMetaDataStore.useStore((s: any) => [
s.metaGroups,
s.metaConstantMap,
s.metaMetrics,
]);
const { getMetaGroups, getMetaData } = monitorMetaDataStore.effects;
const {
fields,
tags,
metric,
filters: defaultFilters,
} = React.useMemo(() => (metaMetrics || [])[0] || {}, [metaMetrics]);
const { types, filters } = React.useMemo(() => metaConstantMap, [metaConstantMap]);
const fieldsMap = React.useMemo(() => keyBy(fields, 'key'), [fields]);
const [customAlarms, customAlarmPaging, customMetricMap, customAlarmDetail, customAlarmTargets] =
customAlarmStore.useStore((s: any) => [
s.customAlarms,
s.customAlarmPaging,
s.customMetricMap,
s.customAlarmDetail,
s.customAlarmTargets,
]);
const {
getCustomAlarms,
switchCustomAlarm,
deleteCustomAlarm,
getCustomMetrics,
getCustomAlarmDetail,
getCustomAlarmTargets,
createCustomAlarm,
editCustomAlarm,
} = customAlarmStore.effects;
const { clearCustomAlarmDetail } = customAlarmStore.reducers;
const { total, pageSize, pageNo } = customAlarmPaging;
useMount(() => {
getMetaGroups();
getCustomMetrics();
getCustomAlarmTargets();
});
const [
{ modalVisible, editingFilters, editingFields, selectedMetric, activedFormData, previewerKey, layout, searchValue },
updater,
update,
] = useUpdate({
layout: [],
modalVisible: false,
editingFilters: [],
editingFields: [],
selectedMetric: undefined as any,
activedFormData: {},
previewerKey: undefined,
searchValue: '',
});
React.useEffect(() => {
updater.selectedMetric(metric);
}, [metric, updater]);
React.useEffect(() => {
if (isEmpty(customAlarmDetail)) return;
const { rules } = customAlarmDetail;
const { activedMetricGroups } = rules[0];
getMetaData({ groupId: activedMetricGroups[activedMetricGroups.length - 1] });
}, [customAlarmDetail, getMetaData]);
React.useEffect(() => {
const { rules, notifies } = customAlarmDetail;
if (isEmpty(rules) || isEmpty(notifies)) return;
const { functions } = rules[0];
update({
editingFields: map(functions, (item) => {
const aggregations = get(types[get(fieldsMap[item.field], 'type')], 'aggregations');
return {
...item,
uniKey: uniqueId(),
aggregations,
aggregatorType: get(find(aggregations, { aggregation: item.aggregator }), 'result_type'),
};
}),
});
}, [customAlarmDetail, fieldsMap, types, update]);
React.useEffect(() => {
const { name, rules, notifies, id } = customAlarmDetail;
if (isEmpty(rules) || isEmpty(notifies)) return;
const { window, metric: _metric, filters: _filters, group, activedMetricGroups } = rules[0];
const { title, content, targets } = notifies[0];
update({
editingFilters: map(_filters, (item) => ({ ...item, uniKey: uniqueId() })),
activedFormData: {
id,
name,
rule: {
activedMetricGroups,
window,
metric: _metric,
group,
},
notify: {
title,
content,
targets: filter(targets, (target) => target !== 'ticket'),
},
},
selectedMetric: _metric,
});
}, [customAlarmDetail, update]);
React.useEffect(() => {
getCustomAlarms({ name: searchValue, pageNo: 1 });
}, [searchValue]);
const handlePageChange: TableProps<COMMON_CUSTOM_ALARM.CustomAlarms>['onChange'] = (paging) => {
const { current, pageSize: size } = paging;
getCustomAlarms({ pageNo: current, pageSize: size, name: searchValue });
};
const handleDeleteAlarm = (id: number) => {
confirm({
title: i18n.t('are you sure you want to delete this item?'),
content: i18n.t('the item will be permanently deleted!'),
onOk() {
deleteCustomAlarm(id);
},
});
};
const handleEnableRule = (enable: string, record: COMMON_CUSTOM_ALARM.CustomAlarms) => {
switchCustomAlarm({
enable: enable === 'enable',
id: record.id,
}).then(() => {
getCustomAlarms({ pageNo, pageSize, name: searchValue });
});
};
const columns: Array<ColumnProps<COMMON_CUSTOM_ALARM.CustomAlarms>> = [
{
title: i18n.t('Name'),
dataIndex: 'name',
key: 'name',
},
{
title: i18n.t('Status'),
dataIndex: 'enable',
onCell: () => ({ style: { minWidth: 100, maxWidth: 300 } }),
render: (enable: boolean, record) => (
<Dropdown
trigger={['click']}
overlay={
<Menu
onClick={(e) => {
handleEnableRule(e.key, record);
}}
>
<Menu.Item key="enable">
<Badge text={i18n.t('Enable')} status="success" />
</Menu.Item>
<Menu.Item key="unable">
<Badge text={i18n.t('unable')} status="default" />
</Menu.Item>
</Menu>
}
>
<div
onClick={(e) => e.stopPropagation()}
className="group flex items-center justify-between px-2 cursor-pointer absolute top-0 left-0 bottom-0 right-0 hover:bg-default-04"
>
<Badge text={enable ? i18n.t('Enable') : i18n.t('unable')} status={enable ? 'success' : 'default'} />
<ErdaIcon type="caret-down" size={20} fill="black-3" className="opacity-0 group-hover:opacity-100" />
</div>
</Dropdown>
),
},
{
title: i18n.t('Indicator'),
dataIndex: 'metric',
key: 'metric',
},
{
title: i18n.t('Period'),
dataIndex: 'window',
key: 'window',
render: (value: number) => `${value} ${i18n.t('min')}`,
},
{
title: i18n.t('Notification method'),
dataIndex: 'notifyTargets',
key: 'notifyTargets',
render: (value: string[]) => `${value.join('、')}`,
},
{
title: i18n.t('Creator'),
dataIndex: 'creator',
render: (text: string) => <UserInfo id={text} />,
},
];
const filterColumns = [
{
title: i18n.t('label'),
dataIndex: 'tag',
render: (value: string, { uniKey }: COMMON_CUSTOM_ALARM.Filter) => (
<Select
dropdownMatchSelectWidth={false}
defaultValue={value}
className="w-full"
onSelect={(tag) => {
handleEditEditingFilters(uniKey, [
{ key: 'tag', value: tag },
{ key: 'value', value: undefined },
]);
}}
getPopupContainer={() => document.body}
>
{map(tags, ({ key, name }) => (
<Select.Option key={key} value={key}>
{name}
</Select.Option>
))}
</Select>
),
},
{
title: i18n.t('operation'),
dataIndex: 'operator',
render: (value: string, { uniKey }: COMMON_CUSTOM_ALARM.Filter) => (
<Select
dropdownMatchSelectWidth={false}
defaultValue={value}
className="w-full"
onSelect={(operator) => {
handleEditEditingFilters(uniKey, [{ key: 'operator', value: operator }]);
}}
getPopupContainer={() => document.body}
>
{map(filters, ({ operation, name }) => (
<Select.Option key={operation}>{name}</Select.Option>
))}
</Select>
),
},
{
title: i18n.t('cmp:Expected value'),
dataIndex: 'value',
render: (value: any, { uniKey }: COMMON_CUSTOM_ALARM.Filter) => {
let expectedValEle = (
<Input
defaultValue={value}
onBlur={(e: any) => {
handleEditEditingFilters(uniKey, [{ key: 'value', value: e.target.value }]);
}}
/>
);
const selectedFilter = find(editingFilters, { uniKey }) || ({} as any);
const { values: _values } = find(tags, { key: selectedFilter.tag }) || ({} as any);
if (!isEmpty(_values)) {
expectedValEle = (
<Select
dropdownMatchSelectWidth={false}
showSearch
className="w-full"
value={value}
onSelect={(v: any) => {
handleEditEditingFilters(uniKey, [{ key: 'value', value: v }]);
}}
getPopupContainer={() => document.body}
>
{map(_values, ({ value: v, name }) => (
<Select.Option key={v} value={v}>
{name}
</Select.Option>
))}
</Select>
);
}
return expectedValEle;
},
},
];
const filteredTableActions: IActions<COMMON_CUSTOM_ALARM.Filter> = {
render: (record) => [
{
title: i18n.t('Delete'),
onClick: () => {
handleRemoveEditingFilter(record.uniKey);
},
},
],
};
const getFieldColumns = (form: FormInstance) => [
{
title: i18n.t('Field'),
dataIndex: 'field',
render: (value: string, { uniKey }: COMMON_CUSTOM_ALARM.Field) => (
<Select
dropdownMatchSelectWidth={false}
defaultValue={value}
className="w-full"
onSelect={(field: any) => {
handleEditEditingFields(uniKey, [
{ key: 'field', value: field },
{ key: 'aggregations', value: get(types[get(fieldsMap[field], 'type')], 'aggregations') },
]);
}}
getPopupContainer={() => document.body}
>
{map(fields, ({ key, name }) => (
<Select.Option key={key} value={key}>
<Tooltip title={name}>{name}</Tooltip>
</Select.Option>
))}
</Select>
),
},
{
title: i18n.t('cmp:Alias'),
dataIndex: 'alias',
render: (value: string, { uniKey }: COMMON_CUSTOM_ALARM.Field) => (
<Input
defaultValue={value}
onBlur={(e: any) => {
handleEditEditingFields(uniKey, [{ key: 'alias', value: e.target.value }]);
}}
/>
),
},
{
title: i18n.t('cmp:Aggregation'),
dataIndex: 'aggregator',
render: (value: string, { uniKey, aggregations }: COMMON_CUSTOM_ALARM.Field) => (
<Select
dropdownMatchSelectWidth={false}
defaultValue={value}
className="w-full"
onSelect={(aggregator: any) => {
handleEditEditingFields(uniKey, [
{ key: 'aggregator', value: aggregator },
{ key: 'aggregatorType', value: get(find(aggregations, { aggregation: aggregator }), 'result_type') },
]);
}}
getPopupContainer={() => document.body}
>
{map(aggregations, ({ aggregation, name }) => (
<Select.Option key={aggregation}>{name}</Select.Option>
))}
</Select>
),
},
{
title: i18n.t('Operations'),
dataIndex: 'operator',
render: (value: string, { uniKey, aggregatorType }: COMMON_CUSTOM_ALARM.Field) => (
<Select
dropdownMatchSelectWidth={false}
defaultValue={value}
className="w-full"
onSelect={(operator) => {
handleEditEditingFields(uniKey, [{ key: 'operator', value: operator }]);
}}
getPopupContainer={() => document.body}
>
{map(get(types[aggregatorType], 'operations'), ({ operation, name }) => (
<Select.Option key={operation}>{name}</Select.Option>
))}
</Select>
),
},
{
title: i18n.t('cmp:Default threshold'),
dataIndex: 'value',
fixed: 'right',
render: (value: any, { uniKey, aggregatorType }: COMMON_CUSTOM_ALARM.Field) => {
let valueEle = null;
switch (aggregatorType) {
case DataType.STRING:
case DataType.STRING_ARRAY:
valueEle = (
<Input
defaultValue={value}
onBlur={(e: any) => {
handleEditEditingFields(uniKey, [{ key: 'value', value: e.target.value }]);
}}
/>
);
break;
case DataType.NUMBER:
case DataType.NUMBER_ARRAY:
valueEle = (
<InputNumber
min={0}
defaultValue={value}
onChange={(v: any) => {
debounceEditEditingFields(uniKey, [{ key: 'value', value: v }]);
}}
/>
);
break;
case DataType.BOOL:
case DataType.BOOL_ARRAY:
valueEle = (
<Switch
checkedChildren="true"
unCheckedChildren="false"
defaultChecked={value}
onClick={(v: boolean) => {
handleEditEditingFields(uniKey, [{ key: 'value', value: v }]);
}}
/>
);
break;
default:
break;
}
return valueEle;
},
},
];
const fieldsTableActions: IActions<COMMON_CUSTOM_ALARM.Field> = {
render: (record) => [
{
title: i18n.t('Delete'),
onClick: () => {
handleRemoveEditingField(record.uniKey);
},
},
],
};
const handleAddEditingFilters = () => {
updater.editingFilters([
{
uniKey: uniqueId(),
// tag: customMetricMap.metricMap[selectedMetric].tags[0].tag.key,
tag: undefined,
// operator: keys(customMetricMap.filterOperatorMap)[0],
operator: undefined,
},
...editingFilters,
]);
};
const handleAddEditingFields = () => {
updater.editingFields([
{
uniKey: uniqueId(),
field: undefined,
alias: undefined,
aggregator: undefined,
operator: undefined,
},
...editingFields,
]);
};
const editRule = (rules: any, uniKey: any, items: Array<{ key: string; value: any }>) => {
if (!uniKey) return;
const _rules = cloneDeep(rules);
const rule = find(_rules, { uniKey });
const index = findIndex(_rules, { uniKey });
const rest = reduce(items, (acc, { key, value }) => ({ ...acc, [key]: value }), {});
const newRule = {
uniKey,
...rule,
...rest,
} as any;
// // 标签、字段对应不同的 value 类型,改变标签或字段就重置 value
// if (['tag', 'field'].includes(item.key)) {
// newRule = { ...newRule, value: undefined };
// }
fill(_rules, newRule, index, index + 1);
return _rules;
};
const handleShowNotifySample = () => {
Modal.info({
title: i18n.t('cmp:Template Sample'),
content: <span className="prewrap">{customMetricMap.notifySample}</span>,
});
};
const handleEditEditingFilters = (uniKey: any, items: Array<{ key: string; value: any }>) => {
updater.editingFilters(editRule(editingFilters, uniKey, items));
};
const handleEditEditingFields = (uniKey: any, items: Array<{ key: string; value: any }>) => {
updater.editingFields(editRule(editingFields, uniKey, items));
};
const debounceEditEditingFields = debounce(handleEditEditingFields, 500);
const handleRemoveEditingFilter = (uniKey: string | undefined) => {
updater.editingFilters(filter(editingFilters, (item) => item.uniKey !== uniKey));
};
const handleRemoveEditingField = (uniKey: string | undefined) => {
updater.editingFields(filter(editingFields, (item) => item.uniKey !== uniKey));
};
const extraKeys = ['uniKey', 'aggregations', 'aggregatorType'];
const openModal = (id?: number) => {
id && getCustomAlarmDetail(id);
updater.modalVisible(true);
};
const closeModal = () => {
updater.editingFields([]);
updater.editingFilters([]);
updater.activedFormData({});
updater.modalVisible(false);
updater.previewerKey(undefined);
clearCustomAlarmDetail();
};
const someValueEmpty = (data: any[], key: string) => {
return some(data, (item) => isEmpty(toString(item[key])));
};
const beforeSubmit = (data: any) => {
return new Promise((resolve, reject) => {
if (isEmpty(editingFields)) {
message.warning(i18n.t('cmp:field rules are required'));
return reject();
}
if (someValueEmpty(editingFilters, 'value')) {
message.warning(i18n.t('cmp:The expected value of filter rule is required.'));
return reject();
}
if (someValueEmpty(editingFields, 'alias')) {
message.warning(i18n.t('cmp:field rule alias is required'));
return reject();
}
if (uniqBy(editingFields, 'alias').length !== editingFields.length) {
message.warning(i18n.t('cmp:field rule alias cannot be repeated'));
return reject();
}
if (someValueEmpty(editingFields, 'value')) {
message.warning(i18n.t('cmp:field rule threshold is required'));
return reject();
}
resolve(data);
});
};
const handleUpdateCustomAlarm = (value: { name: string; rule: any; notify: any }) => {
const _notify = merge({}, value.notify, { targets: [...(value.notify.targets || []), 'ticket'] });
const payload = {
name: value.name,
rules: [
{
...value.rule,
metric: selectedMetric,
functions: map(editingFields, (item) => omit(item, extraKeys)),
filters: map(editingFilters, (item) => omit(item, extraKeys)),
},
],
notifies: [_notify],
};
if (isEmpty(activedFormData)) {
createCustomAlarm(payload);
} else {
editCustomAlarm({ id: activedFormData.id, ...payload });
}
closeModal();
};
const BasicForm = ({ form }: { form: FormInstance }) => {
const fieldsList = [
{
label: i18n.t('Name'),
name: 'name',
itemProps: {
maxLength: 50,
},
},
];
return <RenderPureForm list={fieldsList} form={form} formItemLayout={formItemLayout} />;
};
const RuleForm = ({ form }: { form: FormInstance }) => {
let fieldsList = [
{
label: `${i18n.t('Period')} (${i18n.t('common:minutes')})`,
name: ['rule', 'window'],
type: 'inputNumber',
itemProps: {
min: 0,
precision: 0,
className: 'w-full',
},
},
{
label: i18n.t('Indicator'),
name: ['rule', 'activedMetricGroups'],
type: 'cascader',
options: metaGroups,
itemProps: {
className: 'w-full',
showSearch: true,
placeholder: i18n.t('cmp:Please select the index group'),
onChange: (v: any) => {
getMetaData({ groupId: v[v.length - 1] }).then(() => {
form.setFieldsValue({
rule: {
group: undefined,
},
});
update({
editingFilters: [],
editingFields: [],
previewerKey: undefined,
});
});
},
},
},
];
if (selectedMetric) {
fieldsList = concat(
fieldsList,
{
label: i18n.t('cmp:Filter rule'),
name: ['rule', 'filters'],
required: false,
getComp: () => (
<>
<Button
ghost
className="mb-2"
type="primary"
disabled={someValueEmpty(editingFilters, 'value')}
onClick={handleAddEditingFilters}
>
{i18n.t('cmp:Add-filter-rules')}
</Button>
<ErdaTable
hideHeader
className="filter-rule-table"
rowKey="uniKey"
dataSource={editingFilters}
columns={filterColumns}
actions={filteredTableActions}
scroll={undefined}
/>
</>
),
},
{
label: i18n.t('cmp:Grouping rule'),
name: ['rule', 'group'],
required: true,
type: 'select',
options: map(tags, ({ key, name }) => ({ value: key, name })),
itemProps: {
mode: 'multiple',
allowClear: true,
className: 'w-full',
},
},
{
label: i18n.t('cmp:Field rule'),
name: ['rule', 'functions'],
required: false,
getComp: () => (
<>
<Button
className="mb-2"
type="primary"
ghost
disabled={someValueEmpty(editingFields, 'value')}
onClick={handleAddEditingFields}
>
{i18n.t('cmp:Add-field-rules')}
</Button>
<ErdaTable
hideHeader
className="field-rule-table"
rowKey="uniKey"
dataSource={editingFields}
actions={fieldsTableActions}
columns={getFieldColumns(form)}
scroll={undefined}
/>
</>
),
},
);
}
return <RenderPureForm list={fieldsList} form={form} formItemLayout={formItemLayout} />;
};
const NotifyForm = ({ form }: { form: FormInstance }) => {
const Comp = () => (
<>
<Button
className="mb-2"
type="primary"
ghost
disabled={isEmpty(customMetricMap.notifySample)}
onClick={handleShowNotifySample}
>
{i18n.t('cmp:Template Sample')}
</Button>
<MarkdownEditor
value={form.getFieldValue(['notify', 'content'])}
onBlur={(value) => {
form.setFieldsValue({
notify: {
...(form.getFieldValue('notify') || {}),
content: value,
},
});
}}
placeholder={i18n.t('cmp:Refer to the sample to input content')}
maxLength={512}
/>
</>
);
const fieldsList = [
{
label: i18n.t('cmp:Notification method'),
name: ['notify', 'targets'],
type: 'select',
required: false,
options: map(
filter(customAlarmTargets, ({ key }) => key !== 'ticket'),
({ key, display }) => ({ value: key, name: display }),
),
itemProps: {
mode: 'multiple',
allowClear: true,
className: 'w-full',
},
},
{
label: i18n.t('cmp:Message title'),
name: ['notify', 'title'],
itemProps: {
maxLength: 128,
placeholder: i18n.t('cmp:message title rules template', { interpolation: { suffix: '>', prefix: '<' } }),
},
},
{
label: i18n.t('cmp:Message content'),
name: ['notify', 'content'],
getComp: () => <Comp />,
},
];
return <RenderPureForm list={fieldsList} form={form} formItemLayout={formItemLayout} />;
};
const CustomAlarmForm = ({ form }: any) => {
if (isEmpty(customMetricMap) || isEmpty(customAlarmTargets)) return null;
return (
<div className="custom-alarm-form">
<BasicForm form={form} />
<div className="title font-bold text-base">{i18n.t('cmp:Trigger rule')}</div>
<RuleForm form={form} />
<div className="title font-bold text-base">{i18n.t('cmp:Message template')}</div>
<NotifyForm form={form} />
</div>
);
};
const customRender = (content: JSX.Element) => (
<div className="flex justify-between items-center">
<div className="flex-1">{content}</div>
<IF check={!!previewerKey}>
<div className="custom-alarm-previewer px-4">
<Spin spinning={getPreviewMetaDataLoading}>
<BoardGrid.Pure layout={layout} />
</Spin>
</div>
</IF>
</div>
);
const actions: IActions<COMMON_CUSTOM_ALARM.CustomAlarms> = {
render: (record: COMMON_CUSTOM_ALARM.CustomAlarms) => renderMenu(record),
};
const renderMenu = (record: COMMON_CUSTOM_ALARM.CustomAlarms) => {
const { editAlarmRule, deleteAlarmRule } = {
editAlarmRule: {
title: i18n.t('Edit'),
onClick: () => openModal(record.id),
},
deleteAlarmRule: {
title: i18n.t('Delete'),
onClick: () => handleDeleteAlarm(record.id),
},
};
return [editAlarmRule, deleteAlarmRule];
};
const handleChange = React.useCallback(
debounce((value) => {
updater.searchValue(value);
}, 1000),
[],
);
return (
<div className="custom-alarm">
<TopButtonGroup>
<Button type="primary" onClick={() => openModal()}>
{i18n.t('cmp:Add Custom Rule')}
</Button>
</TopButtonGroup>
<ErdaTable
slot={
<Input
size="small"
className="w-[200px] bg-black-06 border-none ml-0.5"
allowClear
prefix={<ErdaIcon size="16" fill={'default-3'} type="search" />}
onChange={(e) => {
handleChange(e.target.value);
}}
placeholder={i18n.t('search by {name}', { name: i18n.t('Name').toLowerCase() })}
/>
}
loading={getCustomAlarmsLoading || switchCustomAlarmLoading}
dataSource={customAlarms}
columns={columns}
rowKey="id"
onChange={handlePageChange}
pagination={{ current: pageNo, pageSize, total }}
actions={actions}
/>
<FormModal
name={i18n.t('cmp:custom rule')}
loading={getCustomAlarmDetailLoading || extraLoading}
visible={modalVisible}
width={1200}
modalProps={{ bodyStyle: { height: '550px', overflow: 'auto' } }}
PureForm={CustomAlarmForm}
formData={activedFormData}
customRender={customRender}
onOk={handleUpdateCustomAlarm}
beforeSubmit={beforeSubmit}
onCancel={closeModal}
/>
</div>
);
}
Example #17
Source File: index.tsx From gant-design with MIT License | 4 votes |
withSelector = compose( defaultProps(defaultprop), withState('label', 'setLabel', null), // 读模式下的显示文本 withState('cacheLabel', 'setCacheLabel', ({ optionLabel }) => optionLabel), // 当前选项的文本, 在点确认的时候才更新 withState('loading', 'setLoading', false), withState('filter', 'setFilter', ''), withState('selectRef', 'setSelectRef', null), // select组件 withState('dataList', 'setDataList', ({ dataSource }) => dataSource), // 监听搜索 withPropsOnChange(['filter'], ({ filter, selectorId }) => ({ taskId: `${selectorId}:${escape(filter).replace(/\%u/g, '')}`, })), withHandlers({ //将最近选择的项的key转化为真实的key storageToReal: ({ selectorId, reg }) => value => { // 最近选择 if (value?.startsWith(selectorId)) return value.replace(reg, '$1'); return value; }, }), withHandlers({ getValue: ({ valueProp }) => data => String(valueProp && isPlainObject(data) ? data[valueProp] : data), // 获取选项的value getLabel: ({ storageToReal, valueProp, labelProp }) => data => { if (labelProp && isPlainObject(data)) return valueProp == labelProp ? storageToReal(data[labelProp]) : data[labelProp]; return data; }, // 获取选项的label setLabel: ({ setLabel: originSetLabel, splitStr = '、' }) => labels => originSetLabel(Array.isArray(labels) ? labels.filter(Boolean).join(splitStr) : labels), // 重置setlabel方法,增加格式化的功能 }), withHandlers({ // 从dataList或者storageList中找到数据 getItemLabel: ({ dataList, storageList, selectorId, getValue, storageToReal, getLabel, optionLabel, useStorage, }) => (value, index = 0) => { let list = concat(dataList, storageList); // 启用缓存的情况下执行判断 // fix: 解决当storageId恰好是value的前缀的情况 if (useStorage && value?.startsWith(selectorId)) { list = storageList; } const valueItem = list.find(item => storageToReal(getValue(item)) === value); if (valueItem) return getLabel(valueItem); const optionLabelArray = Array.isArray(optionLabel) ? optionLabel : [optionLabel]; return optionLabelArray[index]; }, }), withPropsOnChange(['multiple', 'mode'], ({ multiple, mode }) => ({ isMultiple: multiple || mode === 'multiple' || mode === 'tags', })), withPropsOnChange( ['value'], ({ dataList, storageList, value, getValue, selectorId, isMultiple }) => { if (isNil(value)) { return { value: undefined, }; } const isArray = Array.isArray(value); let cValue = isArray ? value : [value]; const transormedValue = cValue.map(cv => { const v = String(cv); const isInList = dataList.find(item => getValue(item) === v); const isInStorage = storageList.find(item => getValue(item) === v); // 选择的缓存中的数据,需要做一层转化 if (!isInList && isInStorage) { return `${selectorId}-${v}`; } return v; }); return { value: isMultiple ? transormedValue : transormedValue[0], }; }, ), withHandlers({ // 依赖转化后的value transformDataToList: ({ getLabel, getValue, renderItem, optionLabelProp, hideSelected, isMultiple, value: comValue, }) => list => list.map(item => { const transformItemInfo = item => { const value = getValue(item); const key = value || item.key; const label = getLabel(item); return { value, key, label }; }; if (renderItem) { return renderItem( isPlainObject(item) ? { ...item, ...transformItemInfo(item) } : item, Option, ); } if (isPlainObject(item)) { const { disabled, title, className } = item; const { value, key, label } = transformItemInfo(item); let show = true, style; if (hideSelected) { if (isMultiple) { show = comValue.every(v => v !== value); } else { show = comValue !== value; } } if (!show) style = { display: 'none' }; //支持 antd提供的回填到选择框的 Option 的属性值参数功能 const optionLabelPropObj = optionLabelProp && item[optionLabelProp] ? { [optionLabelProp]: item[optionLabelProp] } : {}; return ( <Option key={key} value={value} disabled={disabled} title={title} style={style} className={className} {...optionLabelPropObj} > {label} </Option> ); } return ( <Option key={item} value={item}> {item} </Option> ); }), setLabelWithValue: ({ value, setLabel, setCacheLabel, getItemLabel }) => () => { if (isNil(value)) { setLabel(null); return; } let label = null; // 从dataList找到value对应的项 // 如果没有找到就从storagelist里面找 // 如果还是没有找到,那么就要使用optionLabel参数 if (Array.isArray(value)) { // 多选 label = value.map((itemValue, index) => itemValue ? getItemLabel(itemValue, index) : null, ); } else { label = getItemLabel(value); } setLabel(label); // 设置读模式下的显示文本 setCacheLabel(label); // 设置选项的label }, }), withHandlers({ updateStorage: ({ selectorId, selectorStorageId, storageList, getValue, valueProp, setStorageList, useStorage, historyLength, }) => (data, update) => { if (!useStorage) return; // 不启用缓存 let copyList = cloneDeep(storageList); data.map(item => { const id = `${selectorId}-${getValue(item)}`; let isUpdate = update; // 为true表示从最近选择的项里面选择,只更新 if (!isUpdate) { // const existed = copyList.some(pItem => getValue(pItem) === id); isUpdate = existed; // 如果最近选择种已存在,将直接更新数据 if (!existed) { // 新增最近被选择的数据 if (valueProp && isPlainObject(item)) { copyList.unshift({ ...item, [valueProp]: id }); } else { copyList.unshift(id); } copyList = copyList.slice(0, historyLength); // 保留最近?条 } } if (isUpdate) { copyList.map(item => { if (getValue(item) === id) { // 找到被选择的那一条,更新数据 return valueProp && isPlainObject(item) ? { ...item, [valueProp]: id } : id; } return item; }); } }); setStorageList(copyList); // 更新list localStorage.setItem(selectorStorageId, JSON.stringify(copyList)); // 更新缓存 }, cleanStorage: ({ selectorId, selectorStorageId, storageList, getValue, valueProp, setStorageList, useStorage, }) => (data, update) => { setStorageList([]); // 更新list localStorage.setItem(selectorStorageId, JSON.stringify([])); // 更新缓存 }, getData: ({ taskId, useCache, loading, setLoading, query, afterQuery, filter, setDataList, }) => () => { if (!query) return; let task = null; if (!useCache) { // 不使用选择器缓存,由业务自己决定缓存 setLoading(true); task = query(filter); } else { task = selectorCache.get(taskId); if (!task) { if (loading) return; setLoading(true); task = query(filter); selectorCache.set(taskId, task); } } if (!(task.then && typeof task.then === 'function')) task = Promise.resolve(task); task.then(data => { let list = Array.isArray(data) ? data : []; setLoading(false); setDataList(list); afterQuery && afterQuery(list, setDataList); // else { // throw new Error('选择器选项列表只能是数组格式') // } }); }, }), // 更新选项列表 //#region withPropsOnChange( ['dataList', 'filter', 'storageList', 'loading'], ({ dataList, filter, storageList, cleanStorage, transformDataToList, loading, useStorage, query, labelProp, getLabel, isFilter, customNotDataContent }) => { let result = dataList; if (!query && filter && isFilter) { /** * 筛选算法 * axbxcx ---> abc true * abcabc ---> abc true * bacbcc ---> abc true * bbabdd ---> abc false 没有c */ try { result = dataList.filter(item => { const label = getLabel(item); if (!label) { throw new Error( `应用选择器的过滤功能,请确保列表数据中${labelProp}属性存在,或修改'labelProp'对应的属性名称,作为过滤的依据`, ); } return label.toLowerCase().indexOf(filter.toLowerCase()) > -1; // const LastIndex = filter.split('').reduce( // (index, char) => { // if (index === -1) return -1; // let label = getLabel(item) // if (!label) { // throw new Error(`应用选择器的过滤功能,请确保列表数据中${labelProp}属性存在,或修改'labelProp'对应的属性名称,作为过滤的依据`) // } // label = label.toUpperCase() // char = char.toUpperCase() // return label.slice(index).indexOf(char) // }, // 0 // ) // return ~LastIndex }); } catch (e) { console.error(e); } } let list = [ //'加载中...' : '没有查询到数据' <Select.Option key="none" disabled> <Receiver>{locale => <>{loading ? locale.loading : customNotDataContent||locale.noData}</>}</Receiver> </Select.Option>, ]; if (result.length) { const hasGroup = result.some(item => item.group); if (!hasGroup) { list = transformDataToList(result); } else { const everyGroup = result.every(item => item.group); const group = groupBy(result, 'group'); // 某些项没有写group if (!everyGroup) { group['其他选项'] = group['undefined']; } list = Object.entries(group).reduce((result, [key, data]) => { if (key !== 'undefined') { result.push( <Select.OptGroup key={key} label={key}> {transformDataToList(data)} </Select.OptGroup>, ); } return result; }, [] as React.ReactElement[]); } } if (useStorage && !filter) { // 搜索结果 const newItems = ( <Select.OptGroup key="result" label={<Receiver>{locale => <>{locale.searchResult}</>}</Receiver>} > {list} </Select.OptGroup> ); const selectedItems = ( <Select.OptGroup key="recent" label={ <Receiver> {locale => ( <div style={{ width: '100%', display: 'flex' }}> <span style={{ flex: 1 }}>{locale.recentSelect}</span> <Tooltip title={locale.clearRecent}> <Icon type="delete" style={{ fontSize: '12px', lineHeight: '32px', }} onClick={() => { cleanStorage(); }} /> </Tooltip> </div> )} </Receiver> } > {storageList.length ? ( transformDataToList(storageList) ) : ( <Select.Option key="empty" disabled> <Receiver>{locale => <>{locale.noRecent}</>}</Receiver> </Select.Option> )} </Select.OptGroup> ); return { renderList: [selectedItems].concat(newItems), }; } else { return { renderList: list, }; } }, ), //#endregion withPropsOnChange(['query'], ({ getData }) => getData()), // 下列属性变化的时候重新根据value值设置label withPropsOnChange(['value', 'optionLabel', 'dataList'], ({ setLabelWithValue }) => setLabelWithValue(), ), // 监听label // withPropsOnChange(['optionLabel'], ({ optionLabel }) => { // return { // cacheLabel: optionLabel // } // }), // 去支持只传递dataSource,并且希望更新dataSource的情况 withPropsOnChange(['dataSource'], ({ dataSource, setDataList }) => setDataList(dataSource)), mapProps(({ dataSource, transformDataToList, ...props }) => props), )
Example #18
Source File: loader.ts From qiankun with MIT License | 4 votes |
/**
* 完成了以下几件事:
* 1、通过 HTML Entry 的方式远程加载微应用,得到微应用的 html 模版(首屏内容)、JS 脚本执行器、静态经资源路径
* 2、样式隔离,shadow DOM 或者 scoped css 两种方式
* 3、渲染微应用
* 4、运行时沙箱,JS 沙箱、样式沙箱
* 5、合并沙箱传递出来的 生命周期方法、用户传递的生命周期方法、框架内置的生命周期方法,将这些生命周期方法统一整理,导出一个生命周期对象,
* 供 single-spa 的 registerApplication 方法使用,这个对象就相当于使用 single-spa 时你的微应用导出的那些生命周期方法,只不过 qiankun
* 额外填了一些生命周期方法,做了一些事情
* 6、给微应用注册通信方法并返回通信方法,然后会将通信方法通过 props 注入到微应用
* @param app 微应用配置对象
* @param configuration start 方法执行时设置的配置对象
* @param lifeCycles 注册微应用时提供的全局生命周期对象
*/
export async function loadApp<T extends object>(
app: LoadableApp<T>,
configuration: FrameworkConfiguration = {},
lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObject> {
// 微应用的入口和名称
const { entry, name: appName } = app;
// 实例 id
const appInstanceId = `${appName}_${+new Date()}_${Math.floor(Math.random() * 1000)}`;
// 下面这个不用管,就是生成一个标记名称,然后使用该名称在浏览器性能缓冲器中设置一个时间戳,可以用来度量程序的执行时间,performance.mark、performance.measure
const markName = `[qiankun] App ${appInstanceId} Loading`;
if (process.env.NODE_ENV === 'development') {
performanceMark(markName);
}
// 配置信息
const { singular = false, sandbox = true, excludeAssetFilter, ...importEntryOpts } = configuration;
/**
* 获取微应用的入口 html 内容和脚本执行器
* template 是 link 替换为 style 后的 template
* execScript 是 让 JS 代码(scripts)在指定 上下文 中运行
* assetPublicPath 是静态资源地址
*/
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
// single-spa 的限制,加载、初始化和卸载不能同时进行,必须等卸载完成以后才可以进行加载,这个 promise 会在微应用卸载完成后被 resolve,在后面可以看到
if (await validateSingularMode(singular, app)) {
await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise);
}
// --------------- 样式隔离 ---------------
// 是否严格样式隔离
const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation;
// 实验性的样式隔离,后面就叫 scoped css,和严格样式隔离不能同时开启,如果开启了严格样式隔离,则 scoped css 就为 false,强制关闭
const enableScopedCSS = isEnableScopedCSS(configuration);
// 用一个容器元素包裹微应用入口 html 模版, appContent = `<div id="__qiankun_microapp_wrapper_for_${appInstanceId}__" data-name="${appName}">${template}</div>`
const appContent = getDefaultTplWrapper(appInstanceId, appName)(template);
// 将 appContent 有字符串模版转换为 html dom 元素,如果需要开启样式严格隔离,则将 appContent 的子元素即微应用入口模版用 shadow dom 包裹起来,以达到样式严格隔离的目的
let element: HTMLElement | null = createElement(appContent, strictStyleIsolation);
// 通过 scoped css 的方式隔离样式,从这里也就能看出官方为什么说:
// 在目前的阶段,该功能还不支持动态的、使用 <link />标签来插入外联的样式,但考虑在未来支持这部分场景
// 在现阶段只处理 style 这种内联标签的情况
if (element && isEnableScopedCSS(configuration)) {
const styleNodes = element.querySelectorAll('style') || [];
forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
css.process(element!, stylesheetElement, appName);
});
}
// --------------- 渲染微应用 ---------------
// 主应用装载微应用的容器节点
const container = 'container' in app ? app.container : undefined;
// 这个是 1.x 版本遗留下来的实现,如果提供了 render 函数,当微应用需要被激活时就执行 render 函数渲染微应用,新版本用的 container,弃了 render
// 而且 legacyRender 和 strictStyleIsolation、scoped css 不兼容
const legacyRender = 'render' in app ? app.render : undefined;
// 返回一个 render 函数,这个 render 函数要不使用用户传递的 render 函数,要不将 element 插入到 container
const render = getRender(appName, appContent, container, legacyRender);
// 渲染微应用到容器节点,并显示 loading 状态
render({ element, loading: true }, 'loading');
// 得到一个 getter 函数,通过该函数可以获取 <div id="__qiankun_microapp_wrapper_for_${appInstanceId}__" data-name="${appName}">${template}</div>
const containerGetter = getAppWrapperGetter(
appName,
appInstanceId,
!!legacyRender,
strictStyleIsolation,
enableScopedCSS,
() => element,
);
// --------------- 运行时沙箱 ---------------
// 保证每一个微应用运行在一个干净的环境中(JS 执行上下文独立、应用间不会发生样式污染)
let global = window;
let mountSandbox = () => Promise.resolve();
let unmountSandbox = () => Promise.resolve();
if (sandbox) {
/**
* 生成运行时沙箱,这个沙箱其实由两部分组成 => JS 沙箱(执行上下文)、样式沙箱
*
* 沙箱返回 window 的代理对象 proxy 和 mount、unmount 两个方法
* unmount 方法会让微应用失活,恢复被增强的原生方法,并记录一堆 rebuild 函数,这个函数是微应用卸载时希望自己被重新挂载时要做的一些事情,比如动态样式表重建(卸载时会缓存)
* mount 方法会执行一些一些 patch 动作,恢复原生方法的增强功能,并执行 rebuild 函数,将微应用恢复到卸载时的状态,当然从初始化状态进入挂载状态就没有恢复一说了
*/
const sandboxInstance = createSandbox(
appName,
containerGetter,
Boolean(singular),
enableScopedCSS,
excludeAssetFilter,
);
// 用沙箱的代理对象作为接下来使用的全局对象
global = sandboxInstance.proxy as typeof window;
mountSandbox = sandboxInstance.mount;
unmountSandbox = sandboxInstance.unmount;
}
// 合并用户传递的生命周期对象和 qiankun 框架内置的生命周期对象
const { beforeUnmount = [], afterUnmount = [], afterMount = [], beforeMount = [], beforeLoad = [] } = mergeWith(
{},
// 返回内置生命周期对象,global.__POWERED_BY_QIANKUN__ 和 global.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ 的设置就是在内置的生命周期对象中设置的
getAddOns(global, assetPublicPath),
lifeCycles,
(v1, v2) => concat(v1 ?? [], v2 ?? []),
);
await execHooksChain(toArray(beforeLoad), app, global);
// get the lifecycle hooks from module exports,获取微应用暴露出来的生命周期函数
const scriptExports: any = await execScripts(global, !singular);
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(scriptExports, appName, global);
// 给微应用注册通信方法并返回通信方法,然后会将通信方法通过 props 注入到微应用
const {
onGlobalStateChange,
setGlobalState,
offGlobalStateChange,
}: Record<string, Function> = getMicroAppStateActions(appInstanceId);
const parcelConfig: ParcelConfigObject = {
name: appInstanceId,
bootstrap,
// 挂载阶段需要执行的一系列方法
mount: [
// 性能度量,不用管
async () => {
if (process.env.NODE_ENV === 'development') {
const marks = performance.getEntriesByName(markName, 'mark');
// mark length is zero means the app is remounting
if (!marks.length) {
performanceMark(markName);
}
}
},
// 单例模式需要等微应用卸载完成以后才能执行挂载任务,promise 会在微应用卸载完以后 resolve
async () => {
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
return prevAppUnmountedDeferred.promise;
}
return undefined;
},
// 添加 mount hook, 确保每次应用加载前容器 dom 结构已经设置完毕
async () => {
// element would be destroyed after unmounted, we need to recreate it if it not exist
// unmount 阶段会置空,这里重新生成
element = element || createElement(appContent, strictStyleIsolation);
// 渲染微应用到容器节点,并显示 loading 状态
render({ element, loading: true }, 'mounting');
},
// 运行时沙箱导出的 mount
mountSandbox,
// exec the chain after rendering to keep the behavior with beforeLoad
async () => execHooksChain(toArray(beforeMount), app, global),
// 向微应用的 mount 生命周期函数传递参数,比如微应用中使用的 props.onGlobalStateChange 方法
async props => mount({ ...props, container: containerGetter(), setGlobalState, onGlobalStateChange }),
// 应用 mount 完成后结束 loading
async () => render({ element, loading: false }, 'mounted'),
async () => execHooksChain(toArray(afterMount), app, global),
// initialize the unmount defer after app mounted and resolve the defer after it unmounted
// 微应用挂载完成以后初始化这个 promise,并且在微应用卸载以后 resolve 这个 promise
async () => {
if (await validateSingularMode(singular, app)) {
prevAppUnmountedDeferred = new Deferred<void>();
}
},
// 性能度量,不用管
async () => {
if (process.env.NODE_ENV === 'development') {
const measureName = `[qiankun] App ${appInstanceId} Loading Consuming`;
performanceMeasure(measureName, markName);
}
},
],
// 卸载微应用
unmount: [
async () => execHooksChain(toArray(beforeUnmount), app, global),
// 执行微应用的 unmount 生命周期函数
async props => unmount({ ...props, container: containerGetter() }),
// 沙箱导出的 unmount 方法
unmountSandbox,
async () => execHooksChain(toArray(afterUnmount), app, global),
// 显示 loading 状态、移除微应用的状态监听、置空 element
async () => {
render({ element: null, loading: false }, 'unmounted');
offGlobalStateChange(appInstanceId);
// for gc
element = null;
},
// 微应用卸载以后 resolve 这个 promise,框架就可以进行后续的工作,比如加载或者挂载其它微应用
async () => {
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
prevAppUnmountedDeferred.resolve();
}
},
],
};
// 微应用有可能定义 update 方法
if (typeof update === 'function') {
parcelConfig.update = update;
}
return parcelConfig;
}