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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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>
  );
}