lodash#some TypeScript Examples
The following examples show how to use
lodash#some.
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: utils.ts From redux-with-domain with MIT License | 7 votes |
export function hasDuplicatedKeys(obj, ...others) {
return some(obj, (val, key) =>
some(others, compareItem => {
if (isString(compareItem)) {
return compareItem === key
}
return key in compareItem
})
)
}
Example #2
Source File: site.utils.ts From aqualink-app with MIT License | 7 votes |
filterMetricDataByDate = (
exclusionDates: ExclusionDates[],
metricData?: ValueWithTimestamp[],
) =>
metricData?.filter(
({ timestamp }) =>
// Filter data that do not belong at any `[startDate, endDate]` exclusion date interval
!some(exclusionDates, ({ startDate, endDate }) => {
const dataDate = new Date(timestamp);
return dataDate <= endDate && (!startDate || startDate <= dataDate);
}),
)
Example #3
Source File: utils.tsx From erda-ui with GNU Affero General Public License v3.0 | 7 votes |
getTableList = (data: IPerm, scope: string, filterKey: string) => {
let list = [] as any[];
const countData = (curData: any, key = scope, depth = 0, prevData = {}) => {
if (!curData) return;
if (curData.role) {
list.push({ ...prevData, action: { ...curData, key } });
} else {
const { name, ...rest } = curData;
map(rest, (item, cKey) => {
const curPrevData = { ...prevData, [`depth${depth}`]: { key, name } };
countData(item, cKey, depth + 1, curPrevData);
});
}
};
countData(data);
if (filterKey) {
list = filter(list, (l) => some(l, (item) => item.key.includes(filterKey) || item.name.includes(filterKey)));
}
const maxDeepthObj = maxBy(list, (item) => Object.keys(item).length);
const tableList = [] as any[];
map(list, (item) => {
const itemData = { ...item };
map(maxDeepthObj, (val, key) => {
if (!itemData[key]) {
itemData[key] = {};
}
});
const sortKeys = sortBy(Object.keys(itemData), (k) => (k.startsWith('depth') ? Number(k.slice(5)) : 1000));
itemData.actionKey = map(sortKeys, (k) => itemData[k].key || '__').join('');
tableList.push(itemData);
});
return map(
sortBy(tableList, (item) => item.actionKey),
({ actionKey, ...rest }) => rest,
);
}
Example #4
Source File: actions.ts From github-deploy-center with MIT License | 6 votes |
saveApplication = (
{ state }: Context,
{
repo,
name,
releaseFilter,
}: {
repo: RepoModel
name: string
releaseFilter: string
}
) => {
if (!state.editApplicationDialog) return
const id = state.selectedApplicationId
if (
some(
state.applicationsById,
(app) => app.id !== id && app.repo.id === repo.id && app.name === name
)
) {
state.editApplicationDialog.warning =
'App with same name and repo already exists!'
return
}
state.applicationsById[id].repo = clone(repo)
state.applicationsById[id].name = name
state.applicationsById[id].releaseFilter = releaseFilter
delete state.editApplicationDialog
}
Example #5
Source File: non-stable-id.ts From ui5-language-assistant with Apache License 2.0 | 6 votes |
function hasNonAdaptableTreeMetaData(xmlElement: XMLElement): boolean {
let currElement = xmlElement;
while (currElement.parent.type !== "XMLDocument") {
const hasNonAdaptableTreeMetaData = some(
currElement.attributes,
(attribute) =>
//TODO - inspect if we need to properly resolve the attribute "NS" / use plain string matcher
attribute.key === "sap.ui.dt:designtime" &&
attribute.value === "not-adaptable-tree"
);
if (hasNonAdaptableTreeMetaData) {
return true;
}
currElement = currElement.parent;
}
return false;
}
Example #6
Source File: non-stable-id.ts From ui5-language-assistant with Apache License 2.0 | 6 votes |
function isElementWithStableID(xmlElement: XMLElement): boolean {
return some(
xmlElement.attributes,
(attribute) =>
attribute.key === "id" &&
attribute.value !== null &&
// Contains a single non ws character
/\S/.test(attribute.value)
);
}
Example #7
Source File: actions.ts From github-deploy-center with MIT License | 6 votes |
createNewApplication = (
{ state, actions }: Context,
{
repo,
name,
releaseFilter,
}: {
repo: RepoModel
name: string
releaseFilter: string
}
) => {
if (!state.newApplicationDialog) return
if (
Object.values(state.applicationsById).some(
(app) => app.repo.id === repo.id && app.name === name
)
) {
state.newApplicationDialog.warning =
'App with same name and repo already exists!'
return
}
const appConfig = createApplicationConfig(repo, name, releaseFilter)
state.applicationsById[appConfig.id] = appConfig
state.selectedApplicationId = appConfig.id
delete state.newApplicationDialog
actions.editDeployment()
}
Example #8
Source File: Group.tsx From hub with Apache License 2.0 | 6 votes |
hasBadges = (packages: PackageSummary[] | null): boolean => {
if (packages) {
return some(
packages,
(pkg: PackageSummary) => pkg.official || pkg.repository.official || pkg.repository.verifiedPublisher
);
}
return false;
}
Example #9
Source File: index.ts From erda-ui with GNU Affero General Public License v3.0 | 6 votes |
orgCenterStore = createStore({
name: 'orgCenter',
state: {},
subscriptions: async ({ listenRoute }: IStoreSubs) => {
listenRoute(({ isEntering, isMatch }) => {
if (isEntering('orgCenter')) {
const subSiderInfoMap = layoutStore.getState((s) => s.subSiderInfoMap);
const orgMenus = subSiderInfoMap.orgCenter.menu;
const hasAuth = some(orgMenus, (menu) => location.pathname.includes(menu.href));
if (!hasAuth && orgMenus.length) {
goTo(orgMenus[0].href);
}
}
});
},
})
Example #10
Source File: custom-label.tsx From erda-ui with GNU Affero General Public License v3.0 | 6 votes |
checkTagLabels = (_rule: any, value: string[], callback: Function) => {
const valueArr = isEmpty(value) ? [] : value;
const reg = /^[A-Za-z]([-A-Za-z0-9_.]*)[A-Za-z]$/;
const notPass = valueArr.length
? some(valueArr, (val: string) => {
return val.trim() ? !reg.test(val.trim()) : true;
})
: false;
return notPass
? callback(
i18n.t(
'cmp:each label can only contain letters, numbers, hyphens, underscores and dots, and should start and end with letters',
),
)
: callback();
}
Example #11
Source File: custom-label.tsx From erda-ui with GNU Affero General Public License v3.0 | 6 votes |
checkCustomLabels = (_rule: any, value: string[], callback: Function) => {
const valueArr = isEmpty(value) ? [] : value;
const reg = /^[a-zA-Z0-9-]+$/;
const notPass = valueArr.length
? some(valueArr, (val: string) => {
return val.trim() ? !reg.test(val.trim()) : true;
})
: false;
return notPass ? callback(i18n.t('cmp:each label can only contain letters, numbers and hyphens')) : callback();
}
Example #12
Source File: quick-fix-stable-id.ts From ui5-language-assistant with Apache License 2.0 | 5 votes |
export function computeQuickFixStableIdInfo(
xmlDoc: XMLDocument,
errorOffset: OffsetRange[]
): QuickFixStableIdInfo[] {
const biggestIdsByElementNameCollector = new BiggestIdsByElementNameCollector();
accept(xmlDoc, biggestIdsByElementNameCollector);
const biggestIdsByElementName =
biggestIdsByElementNameCollector.biggestIdsByElementName;
const quickFixStableIdInfo = compact(
map(errorOffset, (_) => {
const astNode = astPositionAtOffset(xmlDoc, _.start);
if (astNode?.kind !== "XMLElementOpenName") {
return undefined;
}
const xmlElement = astNode.astNode;
/* istanbul ignore if - ast node of kind "XMLElementOpenName" will always have name */
if (
xmlElement.name === null ||
xmlElement.syntax.openName === undefined
) {
return undefined;
}
const hasIdAttribute = some(
xmlElement.attributes,
(attrib) => attrib.key === "id"
);
const newText = computeQuickFixIdSuggestion(
biggestIdsByElementName,
xmlElement.name,
hasIdAttribute
);
// @ts-expect-error - TSC does not understand: `xmlElement.syntax.openName !== undefined` is a type guard
const replaceRange = computeQuickFixIdReplaceRange(xmlElement);
return { newText, replaceRange };
})
);
return quickFixStableIdInfo;
}
Example #13
Source File: axios-config.ts From erda-ui with GNU Affero General Public License v3.0 | 5 votes |
isExcludeOrgHeaderApi = (url: string) => {
const excludeApis = ['/api/files', '/api/uc', '/api/-/orgs'];
return some(excludeApis, (api) => url.startsWith(api));
}
Example #14
Source File: index.tsx From erda-ui with GNU Affero General Public License v3.0 | 5 votes |
isSelectorData = (option: IOption[]) => {
return !some(option, (op) => has(op, 'isLeaf'));
}
Example #15
Source File: fields.tsx From erda-ui with GNU Affero General Public License v3.0 | 5 votes |
Fields: React.MemoExoticComponent<
({ fields, isMultiColumn, columnNum, readonly, fid }: IProps) => JSX.Element
> = React.memo(({ fields = [], isMultiColumn, columnNum, readonly, fid }: IProps) => {
const getColumn = (contextProps: IContextType) => {
if (isMultiColumn || (isMultiColumn === undefined && contextProps.parentIsMultiColumn)) {
if (columnNum) return columnNum;
if (contextProps.parentColumnNum) return contextProps.parentColumnNum;
return contextProps.realColumnNum;
}
return 1;
};
return (
<FormContext.Consumer>
{(contextProps) => {
if (!contextProps) return null;
fid && contextProps.setFieldsInfo(fid, fields);
const fieldRealColumnNum = getColumn(contextProps);
if (!fieldRealColumnNum) return null;
return (
<Row gutter={[20, 0]}>
{map(fields, (item, idx) => {
const {
type: Comp,
customProps = {},
required = true,
rules = [],
readonly: itemReadonly,
className,
wrapperClassName,
label,
isHoldLabel = true,
colSpan,
...rest
} = item;
const afterAddRequiredRules =
required && !some(rules, (rule) => has(rule, 'required'))
? [{ required: true, message: i18n.t('{label} can not be empty', { label }) }, ...rules]
: rules;
const isRealReadOnly =
(itemReadonly !== undefined
? itemReadonly
: readonly !== undefined
? readonly
: contextProps?.parentReadonly) || false;
const realReadData = isBoolean(isRealReadOnly) ? null : isRealReadOnly;
return (
<Col span={colSpan || 24 / fieldRealColumnNum} key={idx} className={wrapperClassName}>
<Item
label={label || (isHoldLabel ? <div /> : null)}
colon={!!label}
required={required}
rules={afterAddRequiredRules}
className={`${label ? '' : 'no-label'} ${className || ''}`}
style={{ marginBottom: 6 }}
initialValue={customProps.defaultValue}
{...rest}
>
{isRealReadOnly ? (
<ReadonlyField
{...customProps}
{...realReadData}
renderData={realReadData && realReadData.renderItem}
/>
) : (
Comp && <Comp {...customProps} />
)}
</Item>
</Col>
);
})}
</Row>
);
}}
</FormContext.Consumer>
);
})
Example #16
Source File: non-stable-id.ts From ui5-language-assistant with Apache License 2.0 | 5 votes |
function hasNonAdaptableMetaData(xmlElement: XMLElement): boolean {
return some(
xmlElement.attributes,
(attribute) =>
attribute.key === "sap.ui.dt:designtime" &&
attribute.value === "not-adaptable"
);
}
Example #17
Source File: GameLayout.tsx From fishbowl with MIT License | 5 votes |
function GameLayout(props: { children: React.ReactNode; joinCode: string }) {
const currentPlayer = React.useContext(CurrentPlayerContext)
const location = useLocation()
const history = useHistory()
const inSettings = matchPath(location.pathname, {
path: routes.game.settings,
exact: true,
})
const showFabOnThisRoute = !some(
[routes.game.pending, routes.game.lobby, routes.game.ended],
(route) => {
return matchPath(location.pathname, {
path: route,
exact: true,
})
}
)
return (
<Box>
<Box>{props.children}</Box>
{showFabOnThisRoute && currentPlayer.role === PlayerRole.Host && (
<Box display="flex" flexDirection="row-reverse" pb={2} pt={6}>
<Fab
color="default"
size="small"
onClick={() => {
if (inSettings) {
history.goBack()
} else {
history.push(
generatePath(routes.game.settings, {
joinCode: props.joinCode.toLocaleUpperCase(),
})
)
}
}}
>
{inSettings ? <CloseIcon /> : <SettingsIcon />}
</Fab>
</Box>
)}
</Box>
)
}
Example #18
Source File: CodeEditorFormItem.tsx From next-basics with GNU General Public License v3.0 | 5 votes |
export function CodeEditorFormItem(
props: CodeEditorItemProps
): React.ReactElement {
const { name, label, ...rest } = props;
const [hasError, setHasError] = useState(false);
const handleValidate = (err: Error["err"]): void => {
const error = some(err, ["type", "error"]);
setHasError(error);
};
const validatorFn = async () => {
if (!hasError) {
return Promise.resolve();
} else {
return Promise.reject("请填写正确的 yaml 语法");
}
};
return (
<Form.Item
key={props.name}
name={props.name}
label={props.label}
rules={[
{ required: props.required, message: `请输入${props.name}` },
{ validator: validatorFn },
]}
>
<CodeEditorItem
tabSize={2}
minLines={5}
maxLines={12}
printMargin={false}
showLineNumbers={false}
theme="tomorrow"
enableLiveAutocompletion={true}
onValidate={handleValidate}
{...rest}
></CodeEditorItem>
</Form.Item>
);
}
Example #19
Source File: CodeEditorFormItem.tsx From next-basics with GNU General Public License v3.0 | 5 votes |
export function CodeEditorFormItem(
props: CodeEditorItemProps
): React.ReactElement {
const { name, label, ...rest } = props;
const [hasError, setHasError] = useState(false);
const handleValidate = (err: Error["err"]): void => {
const error = some(err, ["type", "error"]);
setHasError(error);
};
const validatorFn = async () => {
if (!hasError) {
return Promise.resolve();
} else {
return Promise.reject("请填写正确的 yaml 语法");
}
};
return (
<Form.Item
key={props.name}
name={props.name}
label={props.label}
rules={[
{ required: props.required, message: `请输入${props.name}` },
{ validator: validatorFn },
]}
>
<CodeEditorItem
tabSize={2}
minLines={5}
maxLines={12}
printMargin={false}
showLineNumbers={false}
theme="tomorrow"
enableLiveAutocompletion={true}
onValidate={handleValidate}
{...rest}
/>
</Form.Item>
);
}
Example #20
Source File: basic-params-config.tsx From erda-ui with GNU Affero General Public License v3.0 | 4 votes |
PropertyItemForm = React.memo((props: IPropertyItemForm) => {
const { formData, onChange, formType = 'Query', isEditMode = true, index } = props;
const [
{
detailVisible,
curPropertyType,
innerParamList,
dataTempStorage,
paramListTempStorage,
paramsModalVisible,
arrayItemDataStorage,
errorMap,
formErrorMap,
},
updater,
update,
] = useUpdate({
detailVisible: props.detailVisible || false,
curPropertyType: formData.type || initialTypeMap[formType] || ('string' as API_SETTING.PropertyType),
innerParamList: [],
dataTempStorage: {},
paramListTempStorage: [],
paramsModalVisible: false,
arrayItemDataStorage: null,
errorMap: {
name: false,
maxLength: false,
minLength: false,
maximum: false,
minimum: false,
},
formErrorMap: {},
});
const formRef = React.useRef<IFormExtendType>({} as any);
const paramListTempStorageRef = React.useRef<any[]>([]);
const dataTempStorageRef = React.useRef<Obj>({});
React.useImperativeHandle(paramListTempStorageRef, () => paramListTempStorage);
React.useImperativeHandle(dataTempStorageRef, () => dataTempStorage);
const isCurTypeOf = React.useCallback(
(type: BASE_DATA_TYPE) => {
return curPropertyType === type || get(props, ['extraDataTypes', curPropertyType, 'type']) === type;
},
[curPropertyType, props],
);
const isBasicDataType = React.useMemo(() => {
return some(BASE_DATA_TYPE, (item) => item === curPropertyType);
}, [curPropertyType]);
const getRefTypePath = React.useCallback((data: Obj): string => {
return get(data, [QUOTE_PREFIX, 0, '$ref']) || get(data, [QUOTE_PREFIX_NO_EXTENDED]) || '';
}, []);
const getExampleData = React.useCallback(
(data: Obj, extraTypes?: Obj) => {
if (!data) return '';
const _extraTypes = extraTypes || props?.extraDataTypes;
const refTypePath = getRefTypePath(data);
const customType = refTypePath.split('/').slice(-1)[0];
const customTypeData = get(_extraTypes, [customType]) || customType || {};
if (typeof customTypeData === 'string') {
const _type =
get(props, ['allExtraDataTypes', customType, 'type']) || get(props, ['extraDataTypes', customType, 'type']);
return _type === 'array' ? [] : {};
}
const curType = data.type || customTypeData.type;
if (curType === 'object') {
const newExtraTypes = produce(_extraTypes, (draft) => {
draft && (draft[customType] = null);
});
const newExample: Obj = refTypePath ? getExampleData(customTypeData, newExtraTypes) : {};
const customProperties = data.properties || {};
forEach(keys(customProperties), (pName) => {
const propertyItem = customProperties[pName];
newExample[pName] = getExampleData(propertyItem, newExtraTypes);
});
return newExample;
} else if (curType === 'array') {
if (refTypePath) {
const newItemExtraTypes = produce(_extraTypes, (draft) => {
draft && (draft[customType] = null);
});
return getExampleData(customTypeData, newItemExtraTypes);
} else {
return [getExampleData(data.items, _extraTypes)];
}
} else if (refTypePath && customTypeData.example !== undefined) {
return customTypeData.example;
} else if (data.example !== undefined) {
return data.example;
} else {
return DATATYPE_EXAMPLE_MAP[curType] || '';
}
},
[getRefTypePath, props],
);
// 表单初始化加载
React.useEffect(() => {
formRef.current.resetFields();
update({
errorMap: {
name: false,
maxLength: false,
minLength: false,
maximum: false,
minimum: false,
},
formErrorMap: {},
});
const innerProperties = formData?.properties;
const requiredNames = formData?.required || [];
updater.dataTempStorage(formData);
const tempList = map(keys(innerProperties), (pKey: string) => {
const _temp = { ...innerProperties[pKey] };
if (formData?.type === 'object' && Array.isArray(formData?.required)) {
_temp[API_PROPERTY_REQUIRED] = requiredNames.includes(pKey);
}
_temp[API_FORM_KEY] = pKey;
return _temp;
});
updater.innerParamList(tempList);
updater.paramListTempStorage(tempList);
let _curPropertyType = formData?.type || formData?.schema?.type || 'object';
const tempFormData = { ...formData };
const refTypePath = getRefTypePath(formData);
if (refTypePath) {
const customType = refTypePath.split('/').slice(-1)[0];
tempFormData.type = customType;
_curPropertyType = customType;
}
updater.curPropertyType(_curPropertyType);
setTimeout(() => {
formRef.current!.setFieldsValue(tempFormData);
if (isEmpty(formData)) {
const _formData = formRef.current!.getFieldsValue();
updater.dataTempStorage(_formData);
} else {
updater.dataTempStorage(formData);
}
});
}, [updater, formData, getRefTypePath, update]);
const AllDataTypes = React.useMemo(() => {
return filter(props?.allDataTypes, (item) => item !== dataTempStorage[API_FORM_KEY]) || [];
}, [dataTempStorage, props]);
const onToggleDetail = React.useCallback(
(visible) => {
if (visible) {
const omitList = getRefTypePath(dataTempStorage) ? ['type', API_FORM_KEY] : [API_FORM_KEY];
const tempFormData = omit(dataTempStorage, omitList);
const example = getExampleData(tempFormData);
setTimeout(() => formRef.current!.setFieldsValue({ ...tempFormData, example }));
}
updater.dataTempStorage(dataTempStorageRef.current);
if (curPropertyType === 'array' && arrayItemDataStorage) {
updater.dataTempStorage(arrayItemDataStorage);
} else {
updater.dataTempStorage(dataTempStorageRef.current);
}
updater.innerParamList(paramListTempStorageRef.current);
updater.detailVisible(visible);
},
[arrayItemDataStorage, curPropertyType, dataTempStorage, getExampleData, getRefTypePath, updater],
);
const propertyNameMap = React.useMemo(() => {
const list = props?.siblingProperties || [];
return map(list, (item) => item[API_FORM_KEY]);
}, [props]);
const setFields = React.useCallback(
(fieldProps: ISetFieldProps) => {
const { propertyKey = '', propertyData } = fieldProps;
if (propertyKey === API_MEDIA && props.onSetMediaType) {
props.onSetMediaType(fieldProps as { propertyKey: string; propertyData: string });
return;
}
if (propertyKey === 'operation') {
updater.detailVisible(propertyData);
return;
}
if (formRef?.current) {
const newFormData = produce(dataTempStorageRef.current, (draft: any) => {
if (
curPropertyType === 'array' &&
!['description', 'type', API_FORM_KEY, API_PROPERTY_REQUIRED].includes(propertyKey)
) {
set(draft, `items.${propertyKey}`, propertyData);
} else {
set(draft, propertyKey, propertyData);
}
if (propertyKey === 'type') {
const curType = propertyData;
updater.curPropertyType(curType);
unset(draft, QUOTE_PREFIX);
unset(draft, QUOTE_PREFIX_NO_EXTENDED);
unset(draft, 'default');
unset(draft, 'enum');
if (curType === 'object' || curType === 'array') {
unset(draft, 'pattern');
unset(draft, 'maxLength');
unset(draft, 'minLength');
unset(draft, 'format');
unset(draft, 'maximum');
unset(draft, 'minimum');
if (curType === 'object') {
set(draft, 'properties', {});
set(draft, 'required', []);
unset(draft, 'items');
}
if (curType === 'array') {
const tempItemData = {
type: 'string',
example: 'Example',
};
tempItemData[API_FORM_KEY] = 'items';
set(draft, 'items', tempItemData);
unset(draft, 'properties');
updater.innerParamList([]);
updater.paramListTempStorage([]);
}
} else if (['boolean', 'string', 'number', 'integer'].includes(curType)) {
unset(draft, 'items');
unset(draft, 'properties');
unset(draft, 'required');
if (curType !== 'number') {
unset(draft, 'format');
unset(draft, 'maximum');
unset(draft, 'minimum');
}
if (curType !== 'string') {
unset(draft, 'pattern');
unset(draft, 'maxLength');
unset(draft, 'minLength');
}
updater.innerParamList([]);
updater.paramListTempStorage([]);
}
set(draft, 'example', DATATYPE_EXAMPLE_MAP[curType]);
}
});
if (propertyKey === 'type') {
if (!DATATYPE_EXAMPLE_MAP[propertyData]) {
const customTypeData = get(props, ['extraDataTypes', propertyData]) || {};
const _newTypeData = {
...omit(dataTempStorage, [QUOTE_PREFIX, QUOTE_PREFIX_NO_EXTENDED]),
example: customTypeData.example || getExampleData(customTypeData),
properties: customTypeData.type === 'object' ? {} : undefined,
required: dataTempStorage.required,
type: customTypeData.type,
};
// object类型的引用类型支持可拓展编辑
if (customTypeData.type === 'object') {
_newTypeData[QUOTE_PREFIX] = [{ $ref: `#/components/schemas/${propertyData}` }];
} else {
_newTypeData[QUOTE_PREFIX_NO_EXTENDED] = `#/components/schemas/${propertyData}`;
}
const typeQuotePath = _newTypeData[API_FORM_KEY];
update({
dataTempStorage: _newTypeData,
innerParamList: [],
paramListTempStorage: [],
});
formRef.current.setFieldsValue({ ..._newTypeData, type: propertyData });
onChange(dataTempStorage[API_FORM_KEY], _newTypeData, { typeQuotePath, quoteTypeName: propertyData });
return;
}
}
updater.dataTempStorage(newFormData);
onChange(dataTempStorage[API_FORM_KEY], newFormData);
}
},
[curPropertyType, dataTempStorage, onChange, props, update, updater],
);
const dataTypeOptions = React.useMemo(() => {
if (!props?.extraDataTypes) {
return map(BASE_DATA_TYPE, (item) => (
<Option key={item} value={item}>
{item.slice(0, 1).toUpperCase() + item.slice(1)}
</Option>
));
} else {
const basicDataTypeOptions = map(BASE_DATA_TYPE, (item) => (
<Option key={item} value={item}>
{item.slice(0, 1).toUpperCase() + item.slice(1)}
</Option>
));
const extraOptions =
map(keys(props.extraDataTypes), (typeName) => (
<Option key={typeName} value={typeName}>
{typeName}
</Option>
)) || [];
return [...basicDataTypeOptions, ...extraOptions];
}
}, [props]);
const updateErrorNum = React.useCallback(
(num, name) => {
const _formErrorMap: IFormErrorMap = {};
_formErrorMap[name] = num;
if (curPropertyType === 'object') {
forEach(paramListTempStorage, (param) => {
const pName = param[API_FORM_KEY];
if (pName === name) {
_formErrorMap[pName] = num;
} else {
_formErrorMap[pName] = formErrorMap[pName] || 0;
}
});
}
updater.formErrorMap(_formErrorMap);
const totalError = reduce(values(_formErrorMap), (total, cur) => total + cur, 0);
props.updateErrorNum && props.updateErrorNum(totalError, dataTempStorage[API_FORM_KEY]);
props.onFormErrorNumChange && props.onFormErrorNumChange(totalError, dataTempStorage[API_FORM_KEY]);
},
[curPropertyType, dataTempStorage, formErrorMap, paramListTempStorage, props, updater],
);
const onChangePropertyName = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const newName = e.target.value;
const { pattern } = regRules.specialLetter;
const nameMap = formType === 'DataType' ? AllDataTypes : propertyNameMap;
let isErrorValue = true;
const isSameWithBaseType = formType === 'DataType' && newName.toLocaleLowerCase() in BASE_DATA_TYPE;
if (newName !== '' && !pattern.test(newName) && !nameMap.includes(newName) && !isSameWithBaseType) {
const temp = produce(dataTempStorageRef.current, (draft) => {
set(draft, API_FORM_KEY, newName);
});
isErrorValue = false;
updater.dataTempStorage(temp);
onChange(dataTempStorageRef.current[API_FORM_KEY], temp, {
typeQuotePath: newName,
quoteTypeName: curPropertyType,
});
}
// 统计name类型错误数量
const newErrorMap = { ...errorMap };
if (isErrorValue && !errorMap.name) {
newErrorMap.name = true;
updater.errorMap(newErrorMap);
const errorNum = reduce(values(newErrorMap), (total, cur) => (cur ? total + 1 : total), 0);
updateErrorNum(errorNum, dataTempStorage[API_FORM_KEY]);
} else if (!isErrorValue && errorMap.name) {
newErrorMap.name = false;
updater.errorMap(newErrorMap);
const errorNum = reduce(values(newErrorMap), (total, cur) => (cur ? total + 1 : total), 0);
updateErrorNum(errorNum, dataTempStorage[API_FORM_KEY]);
}
},
[
AllDataTypes,
curPropertyType,
dataTempStorage,
errorMap,
formType,
onChange,
propertyNameMap,
updateErrorNum,
updater,
],
);
const onChangeNumberValue = React.useCallback(
({ propertyKey, propertyData }: { propertyKey: string; propertyData: number }) => {
let isErrorValue = true;
if (propertyKey === 'minLength') {
const maxLength = dataTempStorageRef.current?.maxLength;
if (maxLength === undefined || maxLength >= propertyData) {
setFields({ propertyKey, propertyData });
isErrorValue = false;
}
} else if (propertyKey === 'maxLength') {
const minLength = dataTempStorageRef.current?.minLength;
if (minLength === undefined || minLength <= propertyData) {
setFields({ propertyKey, propertyData });
isErrorValue = false;
}
} else if (propertyKey === 'minimum') {
const maximum = dataTempStorageRef.current?.maximum;
if (maximum === undefined || maximum >= propertyData) {
setFields({ propertyKey, propertyData });
isErrorValue = false;
}
} else {
const minimum = dataTempStorageRef.current?.minimum;
if (minimum === undefined || minimum <= propertyData) {
setFields({ propertyKey, propertyData });
isErrorValue = false;
}
}
// 统计number类型错误数量
const newErrorMap = { ...errorMap };
if (isErrorValue && !errorMap[propertyKey]) {
newErrorMap[propertyKey] = true;
updater.errorMap(newErrorMap);
const errorNum = reduce(values(newErrorMap), (total, cur) => (cur ? total + 1 : total), 0);
updateErrorNum(errorNum, dataTempStorage[API_FORM_KEY]);
} else if (!isErrorValue && errorMap[propertyKey]) {
newErrorMap[propertyKey] = false;
updater.errorMap(newErrorMap);
const errorNum = reduce(values(newErrorMap), (total, cur) => (cur ? total + 1 : total), 0);
updateErrorNum(errorNum, dataTempStorage[API_FORM_KEY]);
}
},
[errorMap, dataTempStorage, setFields, updater, updateErrorNum],
);
// 参数详情选项
const propertyFields = React.useMemo(() => {
const fields = formType !== 'Parameters' ? [descriptionField] : [];
if (!getRefTypePath(dataTempStorage)) {
const tempFields = getPropertyDetailFields({ type: curPropertyType, curPropertyType, formData: dataTempStorage });
fields.push(...tempFields);
} else if (get(props, `extraDataTypes.${curPropertyType}.type`)) {
const tempFields = getPropertyDetailFields({
type: curPropertyType,
curPropertyType: get(props, ['extraDataTypes', curPropertyType, 'type']),
formData: dataTempStorage,
});
fields.push(...tempFields);
}
return map(fields, (fieldItem) => {
const tempFieldItem = produce(fieldItem, (draft) => {
if (['minLength', 'maxLength', 'minimum', 'maximum'].includes(draft.name)) {
set(draft, 'customProps.onChange', (e: React.ChangeEvent<HTMLInputElement> | any) => {
const newNum = !(Object.prototype.toString.call(e) === '[object Object]') ? e : +e.target.value;
onChangeNumberValue({ propertyKey: fieldItem?.name, propertyData: newNum });
});
} else {
set(draft, 'customProps.onChange', (e: React.ChangeEvent<HTMLInputElement> | any) => {
const newVal = !(Object.prototype.toString.call(e) === '[object Object]') ? e : e.target.value;
setFields({ propertyKey: fieldItem?.name, propertyData: newVal });
});
}
set(draft, 'customProps.disabled', !isEditMode);
});
return tempFieldItem;
});
}, [formType, getRefTypePath, dataTempStorage, props, curPropertyType, isEditMode, onChangeNumberValue, setFields]);
const onArrayItemChange = (_formKey: string, _formData: any, extraProps?: Obj) => {
const newExample = [getExampleData(_formData)];
const tempData = produce(dataTempStorageRef.current, (draft) => {
draft.items = _formData;
draft.example = newExample;
});
const _extraProps = {
quoteTypeName: extraProps?.quoteTypeName,
typeQuotePath: extraProps?.typeQuotePath
? `${dataTempStorageRef.current[API_FORM_KEY]}.${extraProps.typeQuotePath}`
: '',
};
props.onChange(dataTempStorageRef.current[API_FORM_KEY], tempData, _extraProps);
updater.arrayItemDataStorage(tempData);
};
const updateInnerParamList = (formKey: string, _formData: any, extraProps?: Obj) => {
const tempList = produce(paramListTempStorageRef.current, (draft) => {
forEach(draft, (item, index) => {
if (item[API_FORM_KEY] === formKey) {
draft[index] = _formData;
}
});
});
const requiredNames: string[] = [];
const refTypePath = getRefTypePath(dataTempStorage);
const customDataType = refTypePath ? refTypePath.split('/').slice(-1)[0] : '';
const objectExample: Obj = { ...getExampleData(get(props, ['extraDataTypes', customDataType])) };
forEach(tempList, (item) => {
const _example = item?.example || DATATYPE_EXAMPLE_MAP[item?.type];
if (item[API_PROPERTY_REQUIRED]) {
requiredNames.push(item[API_FORM_KEY]);
}
objectExample[item[API_FORM_KEY]] = _example;
});
updater.paramListTempStorage(tempList);
if (props.onChange && tempList?.length) {
const newProperties = {};
forEach(tempList, (item) => {
newProperties[item[API_FORM_KEY]] = item;
});
const tempData = produce(dataTempStorageRef.current, (draft) => {
draft.properties = newProperties;
draft.type = 'object';
draft.required = requiredNames;
draft.example = objectExample;
});
updater.dataTempStorage(tempData);
const typeQuotePath = extraProps?.typeQuotePath
? `${tempData[API_FORM_KEY] || 'schema'}.properties.${extraProps?.typeQuotePath}`
: '';
props.onChange(dataTempStorageRef.current[API_FORM_KEY], tempData, {
typeQuotePath,
quoteTypeName: extraProps?.quoteTypeName,
});
}
};
const deleteParamByFormKey = (_data: Obj, index: number) => {
const tempList = paramListTempStorage.filter((_record, i) => index !== i);
updater.innerParamList(tempList);
updater.paramListTempStorage(tempList);
const tempProperties = {};
const newExample = {};
const requiredNames: string[] = [];
forEach(tempList, (item) => {
tempProperties[item[API_FORM_KEY]] = item;
newExample[item[API_FORM_KEY]] = item?.example;
item[API_PROPERTY_REQUIRED] && requiredNames.push(item[API_FORM_KEY]);
});
const newFormData = produce(dataTempStorage, (draft) => {
set(draft, 'properties', tempProperties);
set(draft, 'example', newExample);
set(draft, 'required', requiredNames);
});
updater.dataTempStorage(newFormData);
props?.onChange && props.onChange(newFormData[API_FORM_KEY], newFormData);
};
const getExistNames = React.useCallback(() => {
const existNames = map(paramListTempStorage, (item) => item[API_FORM_KEY]);
const refTypePath = getRefTypePath(dataTempStorage);
const customDataType = refTypePath ? refTypePath.split('/').slice(-1)[0] : '';
if (refTypePath && get(props, `extraDataTypes.${curPropertyType}.type`) === 'object') {
const _extraProperties = get(props, ['extraDataTypes', customDataType, 'properties']);
existNames.push(...keys(_extraProperties));
}
return existNames;
}, [curPropertyType, dataTempStorage, getRefTypePath, paramListTempStorage, props]);
// object类型的批量添加参数
const addParamList = React.useCallback(
(newList: Obj[]) => {
const refTypePath = getRefTypePath(dataTempStorage);
const customDataType = refTypePath ? refTypePath.split('/').slice(-1)[0] : '';
const tempList = [...paramListTempStorage, ...newList];
updater.innerParamList(tempList);
updater.paramListTempStorage(tempList);
const tempProperties = {};
forEach(tempList, (item) => {
tempProperties[item[API_FORM_KEY]] = item;
});
const newExample = refTypePath ? { ...get(props, `extraDataTypes.${customDataType}.example`) } : {};
const requiredNames: string[] = [];
forEach(tempList, (property) => {
property[API_PROPERTY_REQUIRED] && requiredNames.push(property[API_FORM_KEY]);
newExample[property[API_FORM_KEY]] = property?.example;
});
const newFormData = produce(dataTempStorage, (draft) => {
set(draft, 'properties', tempProperties);
set(draft, 'type', 'object');
set(draft, 'example', newExample);
set(draft, 'required', requiredNames);
});
updater.dataTempStorage(newFormData);
props?.onChange && props.onChange(dataTempStorage[API_FORM_KEY], newFormData);
},
[dataTempStorage, getRefTypePath, paramListTempStorage, props, updater],
);
// object添加单个参数
const addParam = React.useCallback(() => {
let newPropertyName = `propertyName${innerParamList?.length + 1}`;
const existNames = getExistNames();
while (existNames.includes(newPropertyName)) {
newPropertyName += '1';
}
const tempObj = {
type: 'string',
example: 'Example',
};
tempObj[API_PROPERTY_REQUIRED] = true;
tempObj[API_FORM_KEY] = newPropertyName;
addParamList([tempObj]);
}, [addParamList, getExistNames, innerParamList]);
// 更新设置example示例
React.useEffect(() => {
const tempData = isCurTypeOf(BASE_DATA_TYPE.array) && arrayItemDataStorage ? arrayItemDataStorage : dataTempStorage;
if (tempData.example && typeof tempData.example === 'object') {
const newExample = getExampleData(tempData);
formRef.current.resetFields(['example']);
formRef.current.setFieldsValue({ example: newExample });
}
}, [arrayItemDataStorage, curPropertyType, dataTempStorage, getExampleData, isCurTypeOf]);
const onCloseParamsModal = () => updater.paramsModalVisible(false);
const onImport = (importedParams: Obj[]) => {
onCloseParamsModal();
addParamList(importedParams);
};
const formFieldsSelector = React.useMemo(() => {
const tempFields = getPropertyFormSelector({
formType,
dataTypeOptions,
propertyNameMap,
AllDataTypes,
detailVisible,
index,
});
return map(tempFields, (fieldItem: any) => {
const tempFieldItem = produce(fieldItem, (draft: { name: string }) => {
if (draft.name === API_FORM_KEY && ['DataType', 'Query'].includes(formType)) {
set(draft, 'customProps.onChange', onChangePropertyName);
} else if (draft.name === 'operation') {
set(draft, 'customProps.onChange', onToggleDetail);
} else {
set(draft, 'customProps.onChange', (e: React.ChangeEvent<HTMLInputElement> | string | boolean) => {
const newVal = typeof e === 'string' || typeof e === 'boolean' ? e : e.target.value;
setFields({ propertyKey: fieldItem?.name, propertyData: newVal });
});
}
set(draft, 'customProps.disabled', !isEditMode);
});
return tempFieldItem;
});
}, [
formType,
dataTypeOptions,
propertyNameMap,
AllDataTypes,
detailVisible,
isEditMode,
onChangePropertyName,
onToggleDetail,
setFields,
index,
]);
const detailType = React.useMemo(() => {
if (detailVisible) {
if (isCurTypeOf(BASE_DATA_TYPE.object)) {
return 'object';
} else if (isCurTypeOf(BASE_DATA_TYPE.array)) {
return 'array';
} else if (!isBasicDataType) {
return 'example';
}
}
return '';
}, [detailVisible, isBasicDataType, isCurTypeOf]);
return (
<FormBuilder isMultiColumn ref={formRef}>
{props?.formType !== 'Parameters' && <Fields fields={formFieldsSelector} />}
{detailVisible && isBasicDataType && <Fields fields={propertyFields} />}
{detailType === 'object' && (
<div>
{map(innerParamList, (record, index) => {
return (
<div className="param-form" key={record[API_FORM_KEY]}>
{isEditMode && (
<div className="param-form-operation">
<Popconfirm
title={`${i18n.t('common:confirm to delete')}?`}
onConfirm={() => deleteParamByFormKey(record, index)}
>
<CustomIcon type="shanchu" className="param-form-operation-btn cursor-pointer" />
</Popconfirm>
</div>
)}
<div className="param-form-content">
<FormBuilder isMultiColumn>
<PropertyItemForm
key={record[API_FORM_KEY]}
updateErrorNum={updateErrorNum}
formData={record}
isEditMode={isEditMode}
onChange={updateInnerParamList}
extraDataTypes={props?.extraDataTypes}
allExtraDataTypes={props?.allExtraDataTypes}
siblingProperties={filter(
paramListTempStorage,
(item) => item[API_FORM_KEY] !== record[API_FORM_KEY],
)}
index={index}
/>
</FormBuilder>
</div>
</div>
);
})}
{isEditMode && (
<>
<Button className="operation-btn mb-4" onClick={addParam}>
{i18n.t('common:add parameter')}
</Button>
<Button className="operation-btn mb-4 ml-2" onClick={() => updater.paramsModalVisible(true)}>
{i18n.t('dop:import parameters')}
</Button>
</>
)}
{props?.formType !== 'Parameters' && <Fields fields={[objectExampleField]} />}
</div>
)}
{detailType === 'array' && (
<>
{isBasicDataType && (
<div className="array-form">
<PropertyItemForm
formType="Array"
updateErrorNum={updateErrorNum}
formData={dataTempStorage.items || {}}
detailVisible
onChange={onArrayItemChange}
isEditMode={isEditMode}
extraDataTypes={props?.extraDataTypes}
allExtraDataTypes={props?.allExtraDataTypes}
siblingProperties={filter(
paramListTempStorage,
(item) => item[API_FORM_KEY] !== dataTempStorage.items[API_FORM_KEY],
)}
/>
</div>
)}
<Fields fields={[objectExampleField]} />
</>
)}
{detailType === 'example' && <Fields fields={[objectExampleField]} />}
<ApiParamsModal
visible={paramsModalVisible}
onImport={onImport}
onClose={onCloseParamsModal}
paramList={paramListTempStorage}
/>
</FormBuilder>
);
})
Example #21
Source File: index.tsx From aqualink-app with MIT License | 4 votes |
SiteMap = ({
siteId,
spotterPosition,
polygon,
surveyPoints,
selectedPointId,
surveyPointEditModeEnabled,
editPointLatitude,
editPointLongitude,
onEditPointCoordinatesChange,
classes,
}: SiteMapProps) => {
const dispatch = useDispatch();
const mapRef = useRef<Map>(null);
const markerRef = useRef<Marker>(null);
const editPointMarkerRer = useRef<Marker>(null);
const draftSite = useSelector(siteDraftSelector);
const user = useSelector(userInfoSelector);
const [focusedPoint, setFocusedPoint] = useState<SurveyPoints>();
const reverseCoords = (coordArray: Position[]): [Position[]] => {
return [coordArray.map((coords) => [coords[1], coords[0]])];
};
const selectedSurveyPoint = surveyPoints.find(
(item) => item.id === selectedPointId
);
const setCenter = (
inputMap: L.Map,
latLng: [number, number],
zoom: number
) => {
const maxZoom = Math.max(inputMap.getZoom() || 15, zoom);
const pointBounds = L.latLngBounds(latLng, latLng);
inputMap.flyToBounds(pointBounds, {
maxZoom,
duration: 2,
paddingTopLeft: L.point(0, 200),
});
};
// Fit the polygon constructed by the site's center and its survey points
const fitSurveyPointsPolygon = useCallback(
(inputMap: L.Map, siteCenter: Point) => {
inputMap.fitBounds(
L.polygon([
[siteCenter.coordinates[1], siteCenter.coordinates[0]],
...surveyPoints
.filter((item) => item.polygon?.type === "Point")
.map((item) => {
const coords = item.polygon?.coordinates as Position;
// Reverse coordinates since they come as [lng, lat]
return [coords[1], coords[0]] as LatLngTuple;
}),
]).getBounds()
);
},
[surveyPoints]
);
useEffect(() => {
if (
mapRef?.current?.leafletElement &&
focusedPoint?.polygon?.type === "Point"
) {
const [lng, lat] = focusedPoint.polygon.coordinates;
setCenter(mapRef.current.leafletElement, [lat, lng], 15);
}
}, [focusedPoint]);
useEffect(() => {
const { current } = mapRef;
if (current?.leafletElement) {
const map = current.leafletElement;
// Initialize map's position to fit the given polygon
if (polygon.type === "Polygon") {
map.fitBounds(L.polygon(polygon.coordinates).getBounds());
} else if (draftSite?.coordinates) {
map.panTo(
new L.LatLng(
draftSite.coordinates.latitude || polygon.coordinates[1],
draftSite.coordinates.longitude || polygon.coordinates[0]
)
);
} else if (some(surveyPoints, (item) => item.polygon?.type === "Point")) {
fitSurveyPointsPolygon(map, polygon);
} else {
map.panTo(new L.LatLng(polygon.coordinates[1], polygon.coordinates[0]));
}
}
}, [draftSite, fitSurveyPointsPolygon, polygon, surveyPoints]);
const handleDragChange = () => {
const { current } = markerRef;
if (current?.leafletElement) {
const mapMarker = current.leafletElement;
const { lat, lng } = mapMarker.getLatLng().wrap();
dispatch(
setSiteDraft({
coordinates: {
latitude: lat,
longitude: lng,
},
})
);
}
};
const handleEditPointDragChange = () => {
const { current } = editPointMarkerRer;
if (current && current.leafletElement && onEditPointCoordinatesChange) {
const mapMarker = current.leafletElement;
const { lat, lng } = mapMarker.getLatLng().wrap();
onEditPointCoordinatesChange(lat.toString(), lng.toString());
}
};
return (
<Map
ref={mapRef}
minZoom={1}
maxZoom={17}
zoom={13}
dragging
scrollWheelZoom={false}
className={classes.map}
tap={false}
maxBoundsViscosity={1.0}
maxBounds={mapConstants.MAX_BOUNDS}
>
<TileLayer url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}" />
{polygon.type === "Polygon" ? (
<Polygon positions={reverseCoords(...polygon.coordinates)} />
) : (
<>
{/* Marker to listen to survey point drag changes on edit mode */}
{surveyPointEditModeEnabled && (
<Marker
ref={editPointMarkerRer}
draggable={surveyPointEditModeEnabled}
ondragend={handleEditPointDragChange}
icon={surveyPointIcon(true)}
zIndexOffset={100}
position={[
editPointLatitude ||
(selectedSurveyPoint?.polygon?.type === "Point" &&
selectedSurveyPoint.polygon.coordinates[1]) ||
polygon.coordinates[1],
editPointLongitude ||
(selectedSurveyPoint?.polygon?.type === "Point" &&
selectedSurveyPoint.polygon.coordinates[0]) ||
polygon.coordinates[0],
]}
/>
)}
<Marker
ref={markerRef}
draggable={Boolean(draftSite)}
ondragend={handleDragChange}
icon={pinIcon}
position={[
draftSite?.coordinates?.latitude || polygon.coordinates[1],
draftSite?.coordinates?.longitude || polygon.coordinates[0],
]}
/>
{surveyPoints.map(
(point) =>
point?.polygon?.type === "Point" &&
!samePosition(polygon, point.polygon) &&
(point.id !== selectedPointId || !surveyPointEditModeEnabled) && ( // Hide selected survey point marker if it is in edit mode
<Marker
key={point.id}
icon={surveyPointIcon(point.id === selectedPointId)}
position={[
point.polygon.coordinates[1],
point.polygon.coordinates[0],
]}
onclick={() => setFocusedPoint(point)}
>
<SurveyPointPopup siteId={siteId} point={point} />
</Marker>
)
)}
</>
)}
{!draftSite && spotterPosition && isManager(user) && (
<Marker
icon={buoyIcon}
position={[
spotterPosition.latitude.value,
spotterPosition.longitude.value,
]}
/>
)}
</Map>
);
}
Example #22
Source File: ObservationBox.tsx From aqualink-app with MIT License | 4 votes |
ObservationBox = ({
depth,
dailyData,
date,
classes,
}: ObservationBoxProps) => {
const { bottomTemperature, topTemperature } =
useSelector(siteTimeSeriesDataSelector) || {};
const loading = useSelector(siteTimeSeriesDataLoadingSelector);
const {
satelliteTemperature,
hoboBottom,
hoboSurface,
spotterBottom,
spotterTop,
} = getCardTemperatureValues(
dailyData,
bottomTemperature,
topTemperature,
date
);
return (
<div className={classes.outerDiv}>
{loading ? (
<Box
height="204px"
width="100%"
display="flex"
alignItems="center"
justifyContent="center"
>
<CircularProgress
className={classes.loading}
thickness={1}
size="102px"
/>
</Box>
) : (
<Grid container direction="column">
<Grid container item direction="column" spacing={4}>
<Grid container item direction="column" spacing={1}>
<Grid item>
<Typography color="textPrimary" variant="subtitle1">
SATELLITE OBSERVATION
</Typography>
</Grid>
<Grid container item direction="column">
<Typography color="textPrimary" variant="overline">
SURFACE TEMP
</Typography>
<Typography color="textPrimary" variant="h4">
{`${formatNumber(satelliteTemperature, 1)} °C`}
</Typography>
</Grid>
</Grid>
{some([hoboBottom, hoboSurface, spotterBottom, spotterTop]) && (
<Grid container item direction="column" spacing={1}>
<Grid item>
<Typography color="textPrimary" variant="subtitle1">
SENSOR OBSERVATION
</Typography>
</Grid>
<Grid container item spacing={2}>
<Grid container item direction="column" xs={6}>
<Typography color="textPrimary" variant="overline">
TEMP AT 1m
</Typography>
<Typography color="textPrimary" variant="h4">
{`${formatNumber(spotterTop || hoboSurface, 1)} °C`}
</Typography>
</Grid>
<Grid container item direction="column" xs={6}>
<Typography color="textPrimary" variant="overline">
TEMP AT {depth ? `${depth}m` : "DEPTH"}
</Typography>
<Typography color="textPrimary" variant="h4">
{`${formatNumber(spotterBottom || hoboBottom, 1)} °C`}
</Typography>
</Grid>
</Grid>
</Grid>
)}
</Grid>
</Grid>
)}
</div>
);
}
Example #23
Source File: connectChart.tsx From erda-ui with GNU Affero General Public License v3.0 | 4 votes |
ConnectChart = (props) => {
const {
data,
seriseName,
legendFormatter,
decimal = 2,
isBarChangeColor,
tooltipFormatter,
yAxisNames = [],
isLabel,
noAreaColor,
timeSpan,
opt,
} = props;
const groups = keys(data.results);
const [selectedGroup, setSelectedGroup] = React.useState();
const getOption = () => {
const moreThanOneDay = timeSpan ? timeSpan.seconds > 24 * 3600 : false;
const { results: originData, xAxis, time, lines } = data;
const results = sortBy(originData[selectedGroup] || values(originData)[0], 'axisIndex');
const legendData = [];
let yAxis = [];
const series = [];
const maxArr = [];
// 处理markLine
const markLines = lines || [];
let markLine = {};
if (markLines.length) {
markLine = {
silent: true,
label: {
normal: {
show: true,
position: 'middle',
formatter: (params) => {
const uType = results[0].unitType;
const { unit } = results[0];
const y = getFormatter(uType, unit).format(params.data.yAxis, decimal || 2);
return `${params.name}: ${y}`;
},
},
},
data: markLines.map(({ name, value }) => [
{ x: '7%', yAxis: value, name },
{ x: '93%', yAxis: value },
]),
};
}
map(results, (value, i) => {
const { axisIndex, name, tag, unit } = value;
(tag || name) && legendData.push({ name: tag || name });
const yAxisIndex = axisIndex || 0;
const areaColor = areaColors[i];
series.push({
type: value.chartType || 'line',
name: value.tag || seriseName || value.name || value.key,
yAxisIndex,
data: !isBarChangeColor
? value.data
: map(value.data, (item, j) => {
const sect = Math.ceil(value.data.length / CHANGE_COLORS.length);
return { ...item, itemStyle: { normal: { color: CHANGE_COLORS[Number.parseInt(j / sect, 10)] } } };
}),
label: {
normal: {
show: isLabel,
position: 'top',
formatter: (label) => label.data.label,
},
},
markLine: i === 0 ? markLine : {},
connectNulls: true,
symbol: 'emptyCircle',
symbolSize: 1,
barMaxWidth: 50,
areaStyle: {
normal: {
color: noAreaColor ? 'transparent' : areaColor,
},
},
});
const curMax = value.data ? calMax([value.data]) : [];
maxArr[yAxisIndex] = maxArr[yAxisIndex] && maxArr[yAxisIndex] > curMax ? maxArr[yAxisIndex] : curMax;
const curUnitType = value.unitType || ''; // y轴单位
const curUnit = value.unit || ''; // y轴单位
yAxis[yAxisIndex] = {
name: name || yAxisNames[yAxisIndex] || '',
nameTextStyle: {
padding: [0, 0, 0, 5],
},
position: yAxisIndex === 0 ? 'left' : 'right',
offset: 10,
min: 0,
splitLine: {
show: true,
},
axisTick: {
show: false,
},
axisLine: {
show: false,
},
unitType: curUnitType,
unit: curUnit,
axisLabel: {
margin: 0,
formatter: (val) => getFormatter(curUnitType, unit).format(val, decimal),
},
};
});
const formatTime = (timeStr) => moment(Number(timeStr)).format(moreThanOneDay ? 'D/M HH:mm' : 'HH:mm');
const getTTUnitType = (i) => {
const curYAxis = yAxis[i] || yAxis[yAxis.length - 1];
return [curYAxis.unitType, curYAxis.unit];
};
const genTTArray = (param) =>
param.map((unit, i) => {
return `<span style='color: ${unit.color}'>${cutStr(unit.seriesName, 20)} : ${getFormatter(
...getTTUnitType(i),
).format(unit.value, 2)}</span><br/>`;
});
let defaultTTFormatter = (param) => `${param[0].name}<br/>${genTTArray(param).join('')}`;
if (time) {
defaultTTFormatter = (param) => {
const endTime = time[param[0].dataIndex + 1];
if (!endTime) {
return `${formatTime(param[0].name)}<br />${genTTArray(param).join('')}`;
}
return `${formatTime(param[0].name)} - ${formatTime(endTime)}<br/>${genTTArray(param).join('')}`;
};
}
const lgFormatter = (name) => {
const defaultName = legendFormatter ? legendFormatter(name) : name;
return cutStr(defaultName, 20);
};
const haveTwoYAxis = yAxis.length > 1;
if (haveTwoYAxis) {
yAxis = yAxis.map((item, i) => {
// 有数据和无数据的显示有差异
const hasData = some(results[i].data || [], (_data) => Number(_data) !== 0);
let { name } = item;
if (!hasData) {
name =
i === 0
? `${' '.repeat(item.name.length + 1)}${item.name}`
: `${item.name}${' '.repeat(item.name.length)}`;
}
if (i > 1) {
// 右侧有超过两个Y轴
yAxis[i].offset = 80 * (i - 1);
}
const maxValue = item.max || maxArr[i];
return { ...item, name, max: maxValue, interval: maxValue / 5 };
// 如果有双y轴,刻度保持一致
});
} else {
yAxis[0].name = yAxisNames[0] || '';
}
const defaultOption = {
tooltip: {
trigger: 'axis',
transitionDuration: 0,
confine: true,
axisPointer: {
type: 'none',
},
formatter: tooltipFormatter || defaultTTFormatter,
},
legend: {
bottom: 10,
padding: [15, 5, 0, 5],
orient: 'horizontal',
align: 'left',
data: legendData,
formatter: lgFormatter,
type: 'scroll',
tooltip: {
show: true,
formatter: (t) => cutStr(t.name, 100),
},
},
grid: {
top: haveTwoYAxis ? 30 : 25,
left: 15,
right: haveTwoYAxis ? 30 : 5,
bottom: 40,
containLabel: true,
},
xAxis: [
{
type: 'category',
data: xAxis || time || [] /* X轴数据 */,
axisTick: {
show: false /* 坐标刻度 */,
},
axisLine: {
show: false,
},
axisLabel: {
formatter: xAxis
? (value) => value
: (value) => moment(Number(value)).format(moreThanOneDay ? 'D/M HH:mm' : 'HH:mm'),
},
splitLine: {
show: false,
},
},
],
yAxis,
textStyle: {
fontFamily: 'arial',
},
series,
};
return merge(defaultOption, opt);
};
const { xAxis, time, results } = data;
let hasData = size(results) > 0 && !isEmpty(xAxis || time);
if (time === undefined && xAxis === undefined) {
hasData = size(results) > 0;
}
const handleChange = (value) => {
setSelectedGroup(value);
};
return (
<>
<IF check={hasData}>
<div className="chart-selecter">
{i18n.t('msp:select instance')}:
<Select className="my-3" value={selectedGroup || groups[0]} style={{ width: 200 }} onChange={handleChange}>
{map(groups, (item) => (
<Option value={item} key={item}>
{item}
</Option>
))}
</Select>
</div>
</IF>
<ChartRender {...props} hasData={hasData} getOption={getOption} />
</>
);
}
Example #24
Source File: dashboard.ts From erda-ui with GNU Affero General Public License v3.0 | 4 votes |
dashboard = createStore({
name: 'clusterDashboard',
state: initState,
effects: {
async getFilterTypes({ call, update, select }) {
const { id: orgId, name: orgName } = orgStore.getState((s) => s.currentOrg);
const clusterList = await call(getClusterList, { orgId });
if (isEmpty(clusterList)) return;
const clusterNameString = map(clusterList, (item) => item.name).join();
const groupInfos = select((s) => s.groupInfos);
const newGroupInfos = appendDisplayNameToGroupInfo(groupInfos, clusterList);
update({ groupInfos: newGroupInfos });
const filterGroup = await call(getFilterTypes, { clusterName: clusterNameString, orgName });
update({
filterGroup,
clusterList,
selectedGroups: some(filterGroup, { key: 'cluster' }) ? ['cluster'] : [],
});
},
async getGroupInfos({ call, update, select }, payload: Omit<ORG_DASHBOARD.IGroupInfoQuery, 'orgName'>) {
const { name: orgName } = orgStore.getState((s) => s.currentOrg);
const data = await call(getGroupInfos, { orgName, ...payload });
const { groups: groupInfos, ...unGroupInfo } = data || {};
const clusterList = select((s) => s.clusterList);
const newGroupInfos = appendDisplayNameToGroupInfo(groupInfos || [], clusterList);
update({
unGroupInfo: unGroupInfo || {},
groupInfos: newGroupInfos || [],
});
},
async getNodeLabels({ call, update }) {
let nodeLabels = dashboard.getState((s) => s.nodeLabels);
if (!nodeLabels.length) {
nodeLabels = await call(getNodeLabels);
update({ nodeLabels });
}
},
async getInstanceList(
{ call, update },
{
clusters,
filters,
instanceType,
isWithoutOrg,
}: Merge<ORG_DASHBOARD.IInstanceListQuery, { isWithoutOrg?: boolean }>,
) {
const { name: orgName } = orgStore.getState((s) => s.currentOrg);
const list = await call(getInstanceList, {
instanceType,
orgName: isWithoutOrg ? undefined : orgName,
clusters,
filters,
});
const instanceMap = {
service: 'serviceList',
job: 'jobList',
all: 'instanceList',
};
update({
[`${instanceMap[instanceType]}`]: list,
});
},
async getChartData({ call }, payload) {
const { name: orgName } = orgStore.getState((s) => s.currentOrg);
const { type, ...rest } = payload;
const timeSpan = monitorCommonStore.getState((s) => s.timeSpan);
const { startTimeMs, endTimeMs } = timeSpan;
const query = { start: startTimeMs, end: endTimeMs, orgName };
const data = await call(getChartData, { url: RESOURCE_TYPE_MAP[type].url, ...rest, query });
dashboard.reducers.getChartDataSuccess({ data, type, orgName });
},
async getAlarmList(
{ call, update },
payload: Pick<IMachineAlarmQuery, 'endTime' | 'metricID' | 'pageNo' | 'pageSize' | 'startTime'>,
) {
const { id: orgID } = orgStore.getState((s) => s.currentOrg);
const { list: alarmList, total } = await call(
getAlarmList,
{
...payload,
orgID,
targetID: orgID,
type: 'machine',
targetType: 'org',
},
{ paging: { key: 'alarmPaging', listKey: 'tickets' } },
);
update({ alarmList });
return {
total,
list: alarmList,
};
},
},
reducers: {
setSelectedGroups(state, selectedGroups) {
state.selectedGroups = selectedGroups;
},
clearClusterList(state) {
state.clusterList = [];
},
getChartDataSuccess(state, { type, data }) {
const INDEX_MAP = { cpu: 0, mem: 1, count: 2 };
const { title } = RESOURCE_TYPE_MAP[type];
const KEYS_MAP = {
cpu: [
'group_reduce.{group=tags.addon_id&avg=fields.cpu_allocation&reduce=sum}',
'group_reduce.{group=tags.service_id&avg=fields.cpu_allocation&reduce=sum}',
'group_reduce.{group=tags.job_id&avg=fields.cpu_allocation&reduce=sum}',
],
mem: [
'group_reduce.{group=tags.addon_id&avg=fields.mem_allocation&reduce=sum}',
'group_reduce.{group=tags.service_id&avg=fields.mem_allocation&reduce=sum}',
'group_reduce.{group=tags.job_id&avg=fields.mem_allocation&reduce=sum}',
],
count: ['cardinality.tags.addon_id', 'cardinality.tags.job_id', 'cardinality.tags.service_id'],
};
const dataHandler = multipleGroupDataHandler(KEYS_MAP[type]);
state.chartList[INDEX_MAP[type]] = {
loading: false,
...dataHandler(data),
title,
};
},
},
})
Example #25
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 #26
Source File: line-option.ts From erda-ui with GNU Affero General Public License v3.0 | 4 votes |
getLineOption = (data: IStaticData, optionExtra = {}) => {
const { xData, metricData, yAxisLength, xAxisIsTime } = data;
const {
lines,
yAxisNames = [],
timeSpan,
tooltipFormatter,
legendFormatter,
decimal = 2,
isBarChangeColor,
isLabel,
noAreaColor,
} = optionExtra;
if (!metricData.length) {
return {};
}
const moreThanOneDay = timeSpan ? timeSpan.seconds > 24 * 3600 : false;
const results = sortBy(metricData, 'axisIndex');
let yAxis = [] as object[];
// 处理markLine
const markLines = lines || [];
let markLine = {};
if (markLines.length) {
markLine = {
silent: true,
label: {
normal: {
show: true,
position: 'middle',
formatter: (params) => {
const uType = results[0].unitType;
const { unit } = results[0];
const y = getFormatter(uType, unit).format(params.data.yAxis, decimal || 2);
return `${params.name}: ${y}`;
},
},
},
data: markLines.map(({ name, value }) => [
{ x: '7%', yAxis: value, name },
{ x: '93%', yAxis: value },
]),
};
}
const series = [] as any[];
const maxArr = [];
map(metricData, (metric, i) => {
const yAxisIndex = metric.axisIndex || 0;
const areaColor = areaColors[i];
series.push({
type: metric.chartType || 'line',
name: metric.tag || metric.name || metric.key,
yAxisIndex,
data: !isBarChangeColor
? metric.data
: map(metric.data, (item, j) => {
const sect = Math.ceil(metric.data.length / changeColors.length);
return Object.assign({}, item, {
itemStyle: { normal: { color: changeColors[Number.parseInt(j / sect, 10)] } },
});
}),
label: {
normal: {
show: isLabel,
position: 'top',
formatter: (label) => {
return label.data.label;
},
},
},
markLine: i === 0 ? markLine : {},
connectNulls: true,
symbol: 'emptyCircle',
symbolSize: 1,
barMaxWidth: 50,
areaStyle: {
normal: {
color: noAreaColor ? 'transparent' : areaColor,
},
},
});
const curMax = metric.data ? calMax([metric.data]) : [];
maxArr[yAxisIndex] = maxArr[yAxisIndex] && maxArr[yAxisIndex] > curMax ? maxArr[yAxisIndex] : curMax;
const curUnitType = metric.unitType || ''; // y轴单位类型
const curUnit = metric.unit || ''; // y轴单位
yAxis[yAxisIndex] = {
name: metric.name || yAxisNames[yAxisIndex] || '',
nameTextStyle: {
padding: [0, 0, 0, 5],
},
position: yAxisIndex === 0 ? 'left' : 'right',
offset: 10,
min: 0,
splitLine: {
show: true,
},
axisTick: {
show: false,
},
axisLine: {
show: false,
},
unitType: curUnitType,
unit: curUnit,
axisLabel: {
margin: 0,
formatter: (val) => getFormatter(curUnitType, metric.unit).format(val, decimal),
},
};
});
if (markLines.length && lines[0]) {
const yMax = Math.min(lines[0].value * 1.05, Number.MAX_SAFE_INTEGER);
if (!Number.isNaN(yMax)) {
yAxis[0].max = yMax;
}
}
const formatTime = (timeStr: string) => moment(Number(timeStr)).format(moreThanOneDay ? 'M月D日 HH:mm' : 'HH:mm');
const getTTUnitType = (i: number) => {
const curYAxis = yAxis[i] || yAxis[yAxis.length - 1];
return [curYAxis.unitType, curYAxis.unit];
};
const genTTArray = (param) =>
param.map((unit, i) => {
return `<span style='color: ${unit.color}'>${cutStr(unit.seriesName, 20)} : ${getFormatter(
...getTTUnitType(i),
).format(unit.value, 2)}</span><br/>`;
});
let defaultTTFormatter = (param) => `${param[0].name}<br/>${genTTArray(param).join('')}`;
if (xAxisIsTime) {
defaultTTFormatter = (param) => {
const endTime = xData[param[0].dataIndex + 1];
if (!endTime) {
return `${formatTime(param[0].name)}<br />${genTTArray(param).join('')}`;
}
return `${formatTime(param[0].name)} 到 ${formatTime(endTime)}<br/>${genTTArray(param).join('')}`;
};
}
const lgFormatter = (name: string) => {
const defaultName = legendFormatter ? legendFormatter(name) : name;
return cutStr(defaultName, 20);
};
const haveTwoYAxis = yAxisLength > 1;
if (haveTwoYAxis) {
yAxis = yAxis.map((item, i) => {
// 有数据和无数据的显示有差异
const hasData = some(results[i].data || [], (_data) => Number(_data) !== 0);
let { name } = item;
if (!hasData) {
name =
i === 0 ? `${' '.repeat(item.name.length + 1)}${item.name}` : `${item.name}${' '.repeat(item.name.length)}`;
}
// if (i > 1) { // 右侧有超过两个Y轴
// yAxis[i].offset = 80 * (i - 1);
// }
const maxValue = item.max || maxArr[i];
return { ...item, name, max: maxValue, interval: maxValue / 5 };
// 如果有双y轴,刻度保持一致
});
} else {
yAxis[0].name = yAxisNames[0] || '';
}
const defaultOption = {
tooltip: {
trigger: 'axis',
transitionDuration: 0,
confine: true,
axisPointer: {
type: 'none',
},
formatter: tooltipFormatter || defaultTTFormatter,
},
legend: {
bottom: 10,
padding: [15, 5, 0, 5],
orient: 'horizontal',
align: 'left',
// data: legendData,
formatter: lgFormatter,
type: 'scroll',
tooltip: {
show: true,
formatter: (t) => cutStr(t.name, 100),
},
},
grid: {
top: haveTwoYAxis ? 30 : 25,
left: 15,
right: haveTwoYAxis ? 30 : 5,
bottom: 40,
containLabel: true,
},
xAxis: [
{
type: 'category',
// data: xAxis || time || [], /* X轴数据 */
axisTick: {
show: false /* 坐标刻度 */,
},
axisLine: {
show: false,
},
axisLabel: {
formatter: (value: string | number) => moment(Number(value)).format(moreThanOneDay ? 'M/D HH:mm' : 'HH:mm'),
},
splitLine: {
show: false,
},
},
],
yAxis,
textStyle: {
fontFamily: 'arial',
},
series,
};
return defaultOption;
}
Example #27
Source File: api-doc-tree.tsx From erda-ui with GNU Affero General Public License v3.0 | 4 votes |
ApiDocTree = React.memo((props: IApiDocTree) => {
const [{ treeList }, updater] = useUpdate({
treeList: [] as API_SETTING.IFileTree[],
});
const { getQuoteMap, onSelectDoc, newTreeNode, treeNodeData, popVisible, onVisibleChange } = props;
const { appId, projectId, orgName } = routeInfoStore.useStore((s) => s.params);
const { inode: inodeRouter, pinode: pinodeRouter } = routeInfoStore.useStore((s) => s.query);
let inodeQuery = inodeRouter;
let pinodeQuery = pinodeRouter;
const [branchList, apiWs, isDocChanged, isSaved, wsQuery] = apiDesignStore.useStore((s) => [
s.branchList,
s.apiWs,
s.isDocChanged,
s.isSaved,
s.wsQuery,
]);
const { getTreeList, getApiDetail, deleteTreeNode, renameTreeNode, setSavedState, commitSaveApi } = apiDesignStore;
useMount(() => {
if (!inodeRouter && !pinodeRouter) {
inodeQuery = localStorage.getItem(`apim-${orgName}-${projectId}-${appId}-inode`);
pinodeQuery = localStorage.getItem(`apim-${orgName}-${projectId}-${appId}-pinode`);
}
initBranchTree();
});
useUpdateEffect(() => {
if (!inodeRouter && !pinodeRouter) {
inodeQuery = localStorage.getItem(`apim-${orgName}-${projectId}-${appId}-inode`);
pinodeQuery = localStorage.getItem(`apim-${orgName}-${projectId}-${appId}-pinode`);
jumpToNewDoc({ inode: inodeQuery, pinode: pinodeQuery, branches: branchList });
}
}, [inodeRouter, pinodeRouter]);
const onSelectTreeNode = (_selectedKeys: string[], { node }: AntTreeNodeSelectedEvent) => {
const { eventKey, isLeaf, pinode, readOnly, name } = node.props;
onVisibleChange(false);
const _branch = find(branchList, { inode: pinode });
const tempNodeData = { inode: eventKey, pinode, branchName: _branch?.name, apiDocName: name, readOnly };
const onSelectHandle = () => {
apiWs && apiWs.close();
onSelectDoc(tempNodeData, true);
jumpToNewDoc({ inode: eventKey as string, pinode });
};
if (isLeaf) {
if (isDocChanged) {
Modal.confirm({
title: `${i18n.t('dop:not saved yet, confirm to leave')}?`,
onOk: onSelectHandle,
});
} else {
onSelectHandle();
}
}
};
const treeListRef = React.useRef({});
React.useEffect(() => {
if (!isEmpty(treeList)) {
treeListRef.current = treeList;
}
}, [treeList]);
const jumpToNewDoc = React.useCallback(
({ inode, branches, pinode }: { inode: string; pinode: string; branches?: API_SETTING.IFileTree[] }) => {
if (!inode || !pinode) return;
apiWs && apiWs.close();
const _branchList = branches || branchList;
getApiDetail(inode).then((data) => {
getQuoteMap(data.openApiDoc);
updateSearch({ inode, pinode });
localStorage.setItem(`apim-${orgName}-${projectId}-${appId}-inode`, inode);
localStorage.setItem(`apim-${orgName}-${projectId}-${appId}-pinode`, pinode);
const _branch = find(_branchList, { inode: pinode });
const _curNodeData = { inode, pinode, branchName: _branch?.name, asset: data?.asset, apiDocName: data?.name };
onSelectDoc(_curNodeData);
});
},
[apiWs, branchList, getApiDetail, getQuoteMap, onSelectDoc, orgName, projectId, appId],
);
React.useEffect(() => {
if (isSaved) {
setSavedState(false);
const { inode, pinode } = newTreeNode;
if (inode && pinode) {
jumpToNewDoc({ inode, pinode });
}
}
}, [isSaved, jumpToNewDoc, newTreeNode, setSavedState]);
const getTitleProps = (titleData: any) => {
const { name, pinode, inode, meta } = titleData;
return {
name,
inode,
pinode,
readOnly: meta?.readOnly,
execOperation: (operationKey: string, extraParam?: Obj) => {
if (operationKey === API_TREE_OPERATION.delete) {
deleteTreeNode(titleData).then(() => {
const tempData = produce(treeListRef.current, (draft) => {
forEach(draft, (d: any) => {
if (d.key === pinode) {
d.children = filter(d.children, (item) => item.name !== name);
}
});
});
updater.treeList(tempData as API_SETTING.IFileTree[]);
});
} else {
renameTreeNode({ ...titleData, name: extraParam?.name }).then((res) => {
const tempData = produce(treeListRef.current, (draft) => {
forEach(draft, (d: any) => {
if (d.key === pinode) {
forEach(d.children, (c: any) => {
if (c.key === inode) {
c.key = res.inode;
c.name = res.name;
c.title = <TreeTitle {...getTitleProps(res)} key={res.inode} popToggle={onVisibleChange} />;
}
});
}
});
});
updater.treeList(tempData as API_SETTING.IFileTree[]);
});
}
},
};
};
React.useEffect(() => {
if (!isEmpty(newTreeNode)) {
const { name, pinode, inode } = newTreeNode;
const titleProps = getTitleProps({ ...newTreeNode });
const newNode = {
title: <TreeTitle {...titleProps} key={inode} popToggle={onVisibleChange} />,
key: inode,
isLeaf: true,
name,
pinode,
};
const oldBranch = find(treeList, { key: pinode });
const tempTreeData = produce(treeList, (draft) => {
if (!oldBranch) {
const newBranch: API_SETTING.IFileTree = find(branchList, { inode: pinode });
if (newBranch) {
const newBranchNode = {
title: <BranchTitle name={newBranch.name} />,
key: newBranch.inode,
isLeaf: false,
selectable: false,
showIcon: true,
name,
children: [newNode],
};
draft.push(newBranchNode);
}
} else {
some(draft, (item) => {
if (item.key === pinode && item.children) {
item.children.push(newNode);
return true;
} else {
return false;
}
});
}
});
updater.treeList(tempTreeData);
const isConnectedWs = wsQuery && wsQuery.sessionID;
if (isEmpty(treeNodeData)) {
jumpToNewDoc({ inode, pinode });
} else {
const confirmTitle = isConnectedWs
? `${i18n.t('dop:whether to save and jump to the newly created document')}?`
: `${i18n.t('dop:Go to the newly created document?')}?`;
Modal.confirm({
title: confirmTitle,
onOk: () => {
if (isConnectedWs) {
commitSaveApi();
} else {
jumpToNewDoc({ inode, pinode });
}
},
});
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [newTreeNode]);
const onLoadData = (treeNode: any) => {
return new Promise((resolve) => {
if (treeNode.props.children) {
resolve(true);
return;
}
const curPinode = treeNode?.props?.eventKey;
getTreeList({
pinode: curPinode,
}).then((res) => {
const tempList = map(res, (item) => {
const { name, inode, meta } = item;
return {
title: <TreeTitle {...getTitleProps(item)} key={inode} popToggle={onVisibleChange} />,
key: inode,
isLeaf: true,
name,
pinode: curPinode,
readOnly: meta?.readOnly || false,
};
});
treeNode.props.dataRef.children = tempList;
updater.treeList(treeList);
resolve(true);
});
});
};
const renderTreeNodes = (data: any[]) => {
return map(data, (item) => {
if (item.children) {
return (
<TreeNode title={item.title} key={item.key} dataRef={item}>
{renderTreeNodes(item.children)}
</TreeNode>
);
}
return <TreeNode {...item} dataRef={item} />;
});
};
const treeRef = React.useRef(null);
const content = (
<React.Fragment>
<div className="list-wrap">
{isEmpty(treeList) ? (
<EmptyHolder relative />
) : (
<Tree
className="api-tree"
blockNode
defaultExpandedKeys={[pinodeQuery]}
selectedKeys={[inodeQuery]}
loadData={onLoadData}
onSelect={onSelectTreeNode}
ref={treeRef}
>
{renderTreeNodes(treeList)}
</Tree>
)}
</div>
</React.Fragment>
);
const initBranchTree = () => {
if (!isEmpty(treeList)) return;
getTreeList({
pinode: '0',
scope: 'application',
scopeID: +appId,
}).then((res: API_SETTING.IFileTree[]) => {
const validBranches: API_SETTING.IFileTree[] = res || [];
const tempList = map(validBranches, ({ name, inode }) => {
return {
title: <BranchTitle name={name} />,
key: inode,
isLeaf: false,
selectable: false,
showIcon: true,
name,
};
});
updater.treeList(tempList);
if (pinodeQuery && inodeQuery) {
jumpToNewDoc({ inode: inodeQuery, pinode: pinodeQuery, branches: validBranches });
}
});
};
React.useEffect(() => {
const popoverHide = (e: any) => {
// 点击外部,隐藏选项
// eslint-disable-next-line react/no-find-dom-node
const el2 = ReactDOM.findDOMNode(treeRef.current) as HTMLElement;
if (!(el2 && el2.contains(e.target))) {
onVisibleChange(false);
document.body.removeEventListener('click', popoverHide);
}
};
if (popVisible) {
document.body.addEventListener('click', popoverHide);
}
}, [onVisibleChange, popVisible]);
return (
<Popover
title={i18n.t('dop:Please select a document under the branch')}
overlayClassName="branch-doc-select-popover"
trigger="hover"
placement="bottomLeft"
autoAdjustOverflow={false}
content={content}
visible={popVisible}
>
<button
onClick={() => onVisibleChange(!popVisible)}
className={`api-file-select ${!treeNodeData?.apiDocName ? 'text-desc' : ''}`}
>
<span>{firstCharToUpper(i18n.t('document'))}: </span>
<span className="name nowrap">
{treeNodeData?.branchName
? `${treeNodeData?.branchName}/${treeNodeData?.apiDocName}`
: i18n.t('common:Expand the branch directory to select document')}
</span>
<ErdaIcon type="caret-down" size="20" color="black-4" />
</button>
</Popover>
);
})
Example #28
Source File: resource.tsx From erda-ui with GNU Affero General Public License v3.0 | 4 votes |
ApiResource = (props: Merge<CP_API_RESOURCE.Props, API_SETTING.IResourceProps>) => {
const { quotePathMap, onQuoteChange, apiName, apiDetail } = props;
const [
{ currentMethod, apiMethodDetail, curTabKey, apiDisplayName, tempApiData, pathParams, open },
updater,
update,
] = useUpdate({
currentMethod: API_METHODS.get as API_SETTING.ApiMethod,
apiMethodDetail: {} as Obj,
curTabKey: API_RESOURCE_TAB.Summary,
apiDisplayName: '',
tempApiData: {},
pathParams: null,
open: false,
});
const { apiData, execOperation, operations } = props?.data || {};
const formRef = React.useRef<IFormExtendType>({} as any);
const [openApiDoc, apiLockState, formErrorNum] = apiDesignStore.useStore((s) => [
s.openApiDoc,
s.apiLockState,
s.formErrorNum,
]);
const { updateOpenApiDoc, updateFormErrorNum } = apiDesignStore;
const dataPath = React.useMemo(() => [apiName, currentMethod], [apiName, currentMethod]);
React.useEffect(() => {
if (apiData?.apiMethod) {
// 适配组件化协议的内容
updater.apiMethodDetail(apiData);
updater.tempApiData(!isEmpty(tempApiData) ? tempApiData : apiData);
} else {
// 点击左侧api列表导致的内容变化,更新第一个不为空的method,更新resource内容,resource内容切换到summary
let initialMethod = API_METHODS.get;
some(API_METHODS, (method) => {
if (!isEmpty(apiDetail[method])) {
initialMethod = method;
return true;
} else {
return false;
}
});
updater.currentMethod(initialMethod);
const _apiMethodDetail = apiDetail[initialMethod] || {};
updater.apiMethodDetail(_apiMethodDetail);
}
updater.pathParams(null);
updater.curTabKey(API_RESOURCE_TAB.Summary); // API切换后重置tab
}, [apiDetail, apiData, updater, tempApiData]);
React.useEffect(() => {
let _name = '';
if (apiData) {
_name = tempApiData.apiName || apiData?.apiName;
} else if (apiName) {
_name = apiName;
}
if (_name) {
updater.apiDisplayName(_name);
setTimeout(() => {
formRef.current.setFieldsValue({ apiName: _name });
});
}
}, [apiData, apiName, tempApiData.apiName, updater]);
React.useEffect(() => {
if (!pathParams) return;
const prefixPath = !apiData ? ['paths', apiName] : [];
const tempDetail = produce(openApiDoc, (draft) => {
if (!isEmpty(pathParams)) {
const _pathParams = map(pathParams, (name) => {
return {
...DEFAULT_PATH_PARAM,
name,
};
});
set(draft, [...prefixPath, 'parameters'], _pathParams);
} else {
set(draft, [...prefixPath, 'parameters'], []);
updater.pathParams(null);
}
});
updateOpenApiDoc(tempDetail);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [apiData, apiName, pathParams, updateOpenApiDoc]);
const setFieldHandle = React.useCallback(
(key: string, fieldData: Obj, extraProps?: Obj) => {
const prefixPath = !apiData?.apiMethod ? ['paths', ...dataPath] : [];
const baseFormData = !apiData?.apiMethod ? openApiDoc : tempApiData;
if (onQuoteChange && extraProps?.typeQuotePath) {
const newTypeQuotePath = extraProps?.typeQuotePath ? ['paths', ...dataPath, ...extraProps.typeQuotePath] : [];
const tempQuotePathMap = produce(quotePathMap, (draft) => {
if (extraProps?.quoteTypeName) {
const { quoteTypeName } = extraProps;
draft[quoteTypeName] = draft[quoteTypeName] || [];
draft[quoteTypeName].push(newTypeQuotePath);
}
});
onQuoteChange(tempQuotePathMap);
}
const tempDetail = produce(baseFormData, (draft) => {
if (key !== 'responses') {
const responseData = get(draft, [...prefixPath, 'responses']);
// 设置默认的response
if (!responseData) {
set(draft, [...prefixPath, 'responses'], DEFAULT_RESPONSE);
}
if (key === 'summary') {
set(draft, [...prefixPath, fieldData?.propertyName], fieldData?.propertyData);
if (fieldData?.propertyName === 'operationId') {
set(draft, [...prefixPath, 'summary'], fieldData?.propertyData);
}
if (fieldData?.newTags) {
set(draft, 'tags', fieldData?.newTags);
message.success(i18n.t('dop:category created successfully'));
}
} else if (key === 'query' || key === 'header') {
set(draft, [...prefixPath, 'parameters'], fieldData?.parameters);
}
}
if (key === 'responses' || key === 'requestBody') {
set(draft, [...prefixPath, key], fieldData[key]);
}
// 设置默认的operationId
if (!get(draft, [...prefixPath, 'operationId'])) {
const _operationIdList: string[] = [];
const { paths } = openApiDoc;
forEach(keys(paths), (pathName: string) => {
const methodData = paths[pathName];
forEach(keys(methodData), (item) => {
methodData[item]?.operationId && _operationIdList.push(methodData[item]?.operationId);
});
});
let _operationId = 'operationId';
while (_operationIdList.includes(_operationId)) {
_operationId += '1';
}
set(draft, [...prefixPath, 'operationId'], _operationId);
}
// 设置默认的tags
if (!get(draft, [...prefixPath, 'tags'])) {
set(draft, [...prefixPath, 'tags'], ['other']);
}
if (!draft.tags) {
set(draft, 'tags', [{ name: 'other' }]);
}
});
if (!apiData?.apiMethod) {
updateOpenApiDoc(tempDetail);
} else {
updater.tempApiData(tempDetail);
}
},
[apiData, dataPath, onQuoteChange, openApiDoc, quotePathMap, tempApiData, updateOpenApiDoc, updater],
);
const iconClassMap = React.useMemo(() => {
const classMap = {};
const emptyIcon = {};
forEach(API_METHODS, (method) => {
const tempMethodDetail = get(openApiDoc, ['paths', apiName, method]);
const emptyMethodClass = !tempMethodDetail || isEmpty(tempMethodDetail) ? 'btn-icon-empty' : '';
classMap[method] = `btn-icon btn-icon-${method} ${emptyMethodClass}`;
emptyIcon[method] = `${emptyMethodClass}`;
});
return { classMap, emptyIcon };
}, [apiName, openApiDoc]);
const deleteMethod = React.useCallback(
(methodKey: API_METHODS) => {
updater.open(false);
const tempDetail = produce(openApiDoc, (draft) => {
unset(draft, ['paths', apiName, methodKey]);
});
updateOpenApiDoc(tempDetail);
if (currentMethod === methodKey) {
updater.apiMethodDetail({});
}
},
[apiName, currentMethod, openApiDoc, updateOpenApiDoc, updater],
);
const onApiNameChange = React.useCallback(
(name: string) => {
updater.apiDisplayName(name);
props.onApiNameChange(name);
// 获取api中的path parameters
const _pathParams = map(name.match(pathParamReg), (item) => {
return item.slice(1, item.length - 1);
});
updater.pathParams(_pathParams);
if (onQuoteChange && !isEmpty(quotePathMap)) {
const tempQuotePathMap = produce(quotePathMap, (draft) => {
forEach(keys(draft), (k) => {
forEach(draft[k], (path, i) => {
if (path.includes(apiDisplayName)) {
const oldPathArray = path.slice(0, path.length - 1);
draft[k][i] = [...oldPathArray, name];
}
});
});
});
onQuoteChange(tempQuotePathMap);
}
if (!apiData?.apiMethod) {
const tempDetail = produce(openApiDoc, (draft) => {
const apiTempData = get(draft, ['paths', apiName]);
set(draft, ['paths', name], apiTempData);
unset(draft, ['paths', apiName]);
});
updateOpenApiDoc(tempDetail);
} else {
const tempDetail = produce(tempApiData, (draft) => {
set(draft, 'apiName', name);
});
updater.tempApiData(tempDetail);
}
},
[
apiData,
apiDisplayName,
apiName,
onQuoteChange,
openApiDoc,
props,
quotePathMap,
tempApiData,
updateOpenApiDoc,
updater,
],
);
const hasBody = !['get', 'head'].includes(currentMethod);
const onSaveApiData = React.useCallback(() => {
execOperation &&
execOperation(operations.submit, {
apiData: { ...tempApiData, apiMethod: apiData?.apiMethod, apiName: tempApiData?.name || apiData?.apiName },
});
}, [apiData, execOperation, operations, tempApiData]);
const fieldList = React.useMemo(() => {
const existApiPathNames = keys(openApiDoc?.paths).filter((n) => n !== apiDisplayName);
return [
{
type: Input,
name: 'apiName',
colSpan: 24,
required: false,
isHoldLabel: false,
wrapperClassName: 'pl-0',
customProps: {
className: 'name-input',
maxLength: INPUT_MAX_LENGTH,
disabled: apiLockState,
addonBefore: apiData?.apiMethod,
placeholder: i18n.t('Please enter the {name}', { name: i18n.t('API path') }),
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
const newApiName = e.target.value;
if (newApiName && !existApiPathNames.includes(newApiName) && newApiName.startsWith('/')) {
onApiNameChange(newApiName);
updateFormErrorNum(0);
} else {
updateFormErrorNum(1);
}
},
},
rules: [
{
validator: (_rule: any, value: string, callback: (msg?: string) => void) => {
if (existApiPathNames.includes(value)) {
callback(i18n.t('the same {key} exists', { key: i18n.t('Name') }));
} else if (!value) {
callback(i18n.t('can not be empty'));
} else if (!value.startsWith('/')) {
callback(i18n.t('dop:path must start with /'));
} else {
callback();
}
},
},
],
},
];
}, [apiData, apiDisplayName, apiLockState, onApiNameChange, openApiDoc, updateFormErrorNum]);
const popconfirmRef = React.useRef(null as any);
const selectRef = React.useRef(null) as any;
useClickAway(selectRef, () => {
updater.open(false);
});
const maskClick = React.useCallback(
(e: any) => {
e.stopPropagation();
updater.open(true);
},
[updater],
);
const labelClick = React.useCallback(
(e: any, methodKey: string) => {
e.stopPropagation();
updater.open(false);
const nextHandle = () => {
const _apiMethodDetail = get(openApiDoc, ['paths', apiName, methodKey]) || {};
update({
currentMethod: methodKey as API_SETTING.ApiMethod,
curTabKey: API_RESOURCE_TAB.Summary,
apiMethodDetail: _apiMethodDetail,
});
updateFormErrorNum(0);
formRef.current.setFieldsValue({ apiName });
};
if (formErrorNum > 0) {
confirm({
title: i18n.t('dop:Are you sure to leave, with the error message not saved?'),
onOk() {
nextHandle();
},
});
} else {
nextHandle();
}
},
[apiName, formErrorNum, openApiDoc, update, updateFormErrorNum, updater],
);
const renderSelectMenu = () => {
return (
<div className="select-container" ref={selectRef}>
{!apiData?.apiMethod ? (
<Select
getPopupContainer={(triggerNode) => triggerNode.parentElement as HTMLElement}
style={{ marginRight: '8px', width: '141px' }}
defaultValue={currentMethod}
open={open}
value={currentMethod}
>
{map(API_METHODS, (methodKey) => {
let item = (
<div className="circle-container flex-all-center">
{iconClassMap.emptyIcon[methodKey] ? (
<div className={`${iconClassMap.classMap[methodKey]}`} />
) : (
<ErdaIcon type="check" className={iconClassMap.classMap[methodKey]} />
)}
</div>
);
if (get(openApiDoc, ['paths', apiName, methodKey])) {
item = (
<Popconfirm
title={`${i18n.t('common:confirm to delete')}?`}
onConfirm={() => deleteMethod(methodKey)}
placement="right"
// disabled={apiLockState}
overlayClassName="popconfirm-container"
getPopupContainer={() => popconfirmRef?.current}
onCancel={(e: any) => {
e.stopPropagation();
updater.open(false);
}}
>
{item}
</Popconfirm>
);
}
return (
<Option value={methodKey} key={methodKey}>
<div
className={`api-method-option ${currentMethod === methodKey ? 'api-method-option-active' : ''}`}
key={methodKey}
>
<div
onClick={(e) => {
e.stopPropagation();
labelClick(e, methodKey);
}}
>
{methodKey.toUpperCase()}
</div>
{item}
</div>
</Option>
);
})}
</Select>
) : undefined}
<div className="mask" onClick={maskClick} />
</div>
);
};
const onTabChange = (tabKey: string) => {
const nextHandle = () => {
updater.curTabKey(tabKey as API_RESOURCE_TAB);
const _apiMethodDetail = get(openApiDoc, ['paths', apiName, currentMethod]) || {};
updater.apiMethodDetail(_apiMethodDetail);
updateFormErrorNum(0);
formRef.current.setFieldsValue({ apiName });
};
if (formErrorNum > 0) {
confirm({
title: i18n.t('dop:Are you sure to leave, with the error message not saved?'),
onOk() {
nextHandle();
},
});
} else {
nextHandle();
}
};
return (
<div className="api-resource" ref={popconfirmRef}>
<div className="popover">
{renderSelectMenu()}
<FormBuilder ref={formRef} className="w-full">
<Fields fields={fieldList} />
</FormBuilder>
</div>
<div className="api-resource-tabs">
<Tabs activeKey={curTabKey} onChange={onTabChange}>
<TabPane tab={API_RESOURCE_TAB.Summary} key={API_RESOURCE_TAB.Summary}>
<ResourceSummary formData={apiMethodDetail} onChange={setFieldHandle} isEditMode={!apiLockState} />
</TabPane>
<TabPane tab={API_RESOURCE_TAB.Params} key={API_RESOURCE_TAB.Params}>
<QueryParamsConfig
formData={apiMethodDetail}
paramIn="query"
onChange={setFieldHandle}
isEditMode={!apiLockState}
resourceKey={curTabKey}
/>
</TabPane>
<TabPane tab={API_RESOURCE_TAB.Headers} key={API_RESOURCE_TAB.Headers}>
<QueryParamsConfig
formData={apiMethodDetail}
paramIn="header"
onChange={setFieldHandle}
isEditMode={!apiLockState}
resourceKey={curTabKey}
/>
</TabPane>
<TabPane tab={API_RESOURCE_TAB.Body} key={API_RESOURCE_TAB.Body} disabled={!hasBody}>
{hasBody && (
<ResponseConfig
formData={apiMethodDetail}
paramIn="requestBody"
onChange={setFieldHandle}
dataPath={dataPath}
isEditMode={!apiLockState}
resourceKey={curTabKey}
/>
)}
</TabPane>
<TabPane tab={API_RESOURCE_TAB.Response} key={API_RESOURCE_TAB.Response}>
<ResponseConfig
formData={apiMethodDetail}
paramIn="responses"
onChange={setFieldHandle}
dataPath={dataPath}
isEditMode={!apiLockState}
resourceKey={curTabKey}
/>
</TabPane>
{apiData?.apiMethod && <TabPane tab={API_RESOURCE_TAB.Test} key={API_RESOURCE_TAB.Test} />}
</Tabs>
{apiData?.apiMethod && (
<div className="flex items-center flex-wrap justify-end">
<Button type="primary" onClick={onSaveApiData}>
{i18n.t('Save')}
</Button>
</div>
)}
</div>
</div>
);
}
Example #29
Source File: UploadFilesV2.tsx From next-basics with GNU General Public License v3.0 | 4 votes |
export function RealUploadFile(
props: UploadFilesV2Props,
ref: any
): React.ReactElement {
const { uploadButtonProps } = props;
const theme = useCurrentTheme();
const { t } = useTranslation(NS_FORMS);
const [value, setValue] = React.useState([]);
const [fileList, setFileList] = useState([]);
const [disabled, setDisabled] = useState(false);
const buttonIcon: MenuIcon = {
lib: "easyops",
category: "colored-common",
icon: theme == "dark-v2" ? "upload-dark" : "upload-light",
};
React.useEffect(() => {
setValue(addUid(props.value));
const isDifferent = compareValues(props.value, fileList);
if (isDifferent) {
setFileList(addUid(props.value));
}
}, [props.value]);
const handleValueChange = (v: UploadFileValueItem[]): void => {
setValue(v);
props.onChange?.(v);
};
const handleBeforeUpload = (file: RcFile): Promise<RcFile> | boolean => {
if (FileUtils.sizeCompare(file, props.limitSize ?? 100)) {
// 如果上传文件大小大于限定大小
props.onError?.(i18n.t(`${NS_FORMS}:${K.VOLUME_TOO_BIG}`));
return new Promise((_resolve, reject) => {
// 返回reject阻止文件添加
reject(new Error(i18n.t(`${NS_FORMS}:${K.VOLUME_TOO_BIG}`)));
});
}
if (props.autoUpload) {
// 进行自动上传
return new Promise((resolve) => resolve(file));
} else {
// 返回false阻止默认上传行为
return false;
}
};
const handleFilesChange = async (
newFile: FileItem,
newFileList: FileItem[],
isDone: boolean
): Promise<void> => {
if (isDone) {
if (props.maxNumber === 1) {
setFileList([newFile]);
handleValueChange([
{
response: newFile.response,
name: newFile.name,
uid: newFile.uid,
},
]);
} else {
setFileList(newFileList);
handleValueChange([
...value,
{
response: newFile.response,
name: newFile.name,
uid: newFile.uid,
},
]);
}
} else {
if (props.maxNumber === 1) {
setFileList([newFile]);
if (!props.autoUpload) {
handleValueChange([
{
file: newFile,
name: newFile.name,
uid: newFile.uid,
},
]);
}
} else {
setFileList(newFileList);
if (!props.autoUpload) {
handleValueChange([
...value,
{
file: newFile,
name: newFile.name,
uid: newFile.uid,
},
]);
}
}
}
};
const handleChange = (data: any) => {
const _file = data.file;
if (
props.maxNumber &&
props.maxNumber !== 1 &&
value?.length >= props.maxNumber &&
_file.status !== "removed" &&
_file.status !== "error"
)
return;
const _fileList = data.fileList;
if (some(_fileList, ["status", "uploading"])) {
setDisabled(true);
} else {
setDisabled(false);
}
if (_file.status === "removed") {
const index = findIndex(value, ["uid", _file.uid]);
if (index !== -1) {
handleValueChange(update(value, { $splice: [[index, 1]] }));
}
setFileList(_fileList);
} else if (_file.status === "error") {
setDisabled(false);
const index = findIndex(fileList, ["uid", _file.uid]);
if (index !== -1) {
setFileList(update(fileList, { $splice: [[index, 1]] }));
}
props.onError?.(_file);
} else {
handleFilesChange(_file, _fileList, false);
if (_file.response && _file.status === "done") {
_file.response = _file.response.data;
handleFilesChange(_file, _fileList, true);
}
}
};
const uploadNode = () => {
if (props.hideUploadButton && !props.uploadDraggable) {
return null;
}
if (props.uploadDraggable) {
return (
<>
<p className="ant-upload-drag-icon">
<GeneralIcon icon={buttonIcon} />
</p>
<p className="ant-upload-text">
{props.draggableUploadText ??
i18n.t(`${NS_FORMS}:${K.CLICK_AND_DRAP_FIEL}`)}
</p>
<p className="ant-upload-hint">
{props.draggableUploadHint ?? t(K.DRAGGABLE_UPLOAD_HINT)}
</p>
</>
);
}
return (
<Button
disabled={
(props.maxNumber && value?.length >= props.maxNumber) ||
props.disabled
}
type={uploadButtonProps?.buttonType}
>
<GeneralIcon
icon={
uploadButtonProps?.buttonIcon ?? {
lib: "antd",
icon: "upload",
theme: "outlined",
}
}
/>
{uploadButtonProps?.buttonName ?? props.uploadButtonName ?? "Upload"}
</Button>
);
};
const handleRemove = (e: any) => {
props.onRemove?.(e);
};
const uploadProps = {
className: classNames({
[styles.uploadContainerDisplayNone]:
props.hideDragBtnWhenAchieveMax &&
props.uploadDraggable &&
props.maxNumber &&
value?.length >= props.maxNumber,
}),
method: props.method ?? "post",
disabled: props.disabled || disabled,
data: props.data,
name: props.uploadName,
action: props.url,
accept: props.accept,
listType: "text",
fileList,
maxCount: props.maxNumber,
beforeUpload: handleBeforeUpload,
onChange: handleChange,
onRemove: handleRemove,
supportServerRender: true,
progress: {
strokeColor: "var(--color-success)",
trailColor: "var(--color-fill-bg-base-1)",
strokeWidth: "1px",
showInfo: false,
},
showUploadList: {
// eslint-disable-next-line react/display-name
removeIcon: (file: UploadFile): ReactNode =>
file.status === "error" ? (
<GeneralIcon
icon={{
lib: "antd",
theme: "outlined",
icon: "close",
}}
/>
) : (
<GeneralIcon
icon={{
lib: "easyops",
category: "default",
icon: "delete",
}}
/>
),
},
// eslint-disable-next-line react/display-name
iconRender: (file: UploadFile): ReactNode =>
file.status === "uploading" ? (
<LoadingOutlined />
) : (
<GeneralIcon
icon={{
lib: "antd",
icon: "file-text",
theme: "outlined",
}}
/>
),
};
return (
<div ref={ref} className={styles.uploadContainer}>
{props.uploadDraggable ? (
<Upload.Dragger {...uploadProps}>{uploadNode()}</Upload.Dragger>
) : (
<Upload {...uploadProps}>{uploadNode()}</Upload>
)}
</div>
);
}