react-use#useDebounce TypeScript Examples

The following examples show how to use react-use#useDebounce. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: index.tsx    From erda-ui with GNU Affero General Public License v3.0 6 votes vote down vote up
DebounceSearch = (props: IProps) => {
  const { onChange, value: pValue, className = '', placeholder = i18n.t('Search by keyword-char'), ...rest } = props;
  const [value, setValue] = React.useState(undefined as string | undefined);

  React.useEffect(() => {
    setValue(pValue);
  }, [pValue]);

  useDebounce(
    () => {
      onChange && onChange(value);
    },
    600,
    [value],
  );

  return (
    <Search
      className={`search-input ${className}`}
      value={value}
      placeholder={placeholder}
      onChange={(e: any) => {
        setValue(e.target.value);
      }}
      {...rest}
    />
  );
}
Example #2
Source File: index.ts    From erda-ui with GNU Affero General Public License v3.0 6 votes vote down vote up
useWatchTableWidth = (selector: string) => {
  const client = layoutStore.useStore((s) => s.client);
  const [tableWidth, setTableWidth] = React.useState(0);
  React.useLayoutEffect(() => {
    setTableWidth(get(document.querySelector(selector), ['clientWidth'], 0));
  }, [selector]);
  useDebounce(
    () => {
      setTableWidth(get(document.querySelector(selector), ['clientWidth'], 0));
    },
    50,
    [client],
  );
  return [tableWidth];
}
Example #3
Source File: useDebounceEffect.ts    From ace with GNU Affero General Public License v3.0 6 votes vote down vote up
useChangeDebounce = (fn: Function, ms?: number, deps?: DependencyList) => {
    const lastDepValues = useRef(deps);

    useDebounce(() => {
        if (lastDepValues.current.some((it, index) => it !== deps[index])) {
            fn();
        }

        lastDepValues.current = deps;
    }, 500, [deps, fn]);
}
Example #4
Source File: HoverLinkPreview.tsx    From logseq-plugin-link-preview with MIT License 6 votes vote down vote up
function useDebounceValue<T>(v: T, timeout: number = 50) {
  const [state, setState] = React.useState(v);
  useDebounce(
    () => {
      setState(v);
    },
    timeout,
    [v]
  );
  return state;
}
Example #5
Source File: SearchInput.tsx    From phosphor-home with MIT License 5 votes vote down vote up
SearchInput: React.FC<SearchInputProps> = () => {
  const [value, setValue] = useState<string>("");
  const [query, setQuery] = useRecoilState(searchQueryAtom);
  const inputRef = useRef<HTMLInputElement>() as MutableRefObject<HTMLInputElement>;

  useHotkeys("ctrl+k,cmd+k", (e) => {
    e.preventDefault();
    if (!e.repeat) {
      inputRef.current?.focus();
      inputRef.current.select();
    }
  });

  /* eslint-disable react-hooks/exhaustive-deps */
  useEffect(() => {
    let isMounted = true;
    if (value !== query) {
      isMounted && setValue(query);
      ReactGA.event({ category: "Search", action: "Tag", label: query });
    }
    return () => void (isMounted = false);
  }, [query]);
  /* eslint-enable react-hooks/exhaustive-deps */

  const [isReady] = useDebounce(
    () => {
      if (value !== query) {
        setQuery(value);
        !!value &&
          ReactGA.event({ category: "Search", action: "Query", label: value });
      }
      !!value &&
        void document
          .getElementById("beacon")
          ?.scrollIntoView({ block: "start", behavior: "smooth" });
    },
    500,
    [value]
  );

  const handleCancelSearch = () => {
    setValue("");
    // Should cancel pending debounce timeouts and immediately clear query
    // without causing lag!
    // setQuery("");
  };

  return (
    <div className="search-bar">
      <MagnifyingGlass id="search-icon" size={24} />
      <input
        ref={inputRef}
        id="search-input"
        title="Search for icon names, categories, or keywords"
        aria-label="Search for an icon"
        type="text"
        autoCapitalize="off"
        autoComplete="off"
        value={value}
        placeholder="Search"
        onChange={({ currentTarget }) => setValue(currentTarget.value)}
        onKeyPress={({ currentTarget, key }) =>
          key === "Enter" && currentTarget.blur()
        }
      />
      {!value && !isMobile && <Keys>{isApple ? <Command /> : "Ctrl + "}K</Keys>}
      {value ? (
        isReady() ? (
          <X className="clear-icon" size={18} onClick={handleCancelSearch} />
        ) : (
          <HourglassHigh className="wait-icon" weight="fill" size={18} />
        )
      ) : null}
    </div>
  );
}
Example #6
Source File: index.tsx    From rocketredis with MIT License 4 votes vote down vote up
KeyList: React.FC = () => {
  const parentRef = useRef(null)

  const { width } = useWindowSize({ watch: false })
  const { t } = useTranslation('keyList')

  const [searchInputValue, setSearchInputValue] = useState('')
  const [filter, setFilter] = useState('')
  const [keys, setKeys] = useState<string[]>([])

  const [currentConnection] = useRecoilState(currentConnectionState)
  const [currentDatabase] = useRecoilState(currentDatabaseState)
  const [currentKey, setCurrentKey] = useRecoilState(currentKeyState)

  useDebounce(
    () => {
      setFilter(searchInputValue)
    },
    500,
    [searchInputValue]
  )

  const filteredKeys = useMemo(() => {
    if (!filter) {
      return keys
    }

    return keys.filter(key => key.includes(filter))
  }, [filter, keys])

  const rowVirtualizer = useVirtual({
    size: filteredKeys.length,
    parentRef,
    estimateSize: useCallback(() => 33, [])
  })

  const handleSearchInputChange = useCallback(
    (e: ChangeEvent<HTMLInputElement>) => {
      setSearchInputValue(e.target.value)
    },
    []
  )

  const handleSelectKey = useCallback(
    (key: string) => {
      setCurrentKey(key)
    },
    [setCurrentKey]
  )

  useEffect(() => {
    if (currentDatabase) {
      loadKeysFromDatabase().then(loadedKeys => {
        setKeys(loadedKeys)
      })
    }
  }, [currentDatabase])

  return (
    <Container
      width={(width - 300) / 2}
      height={Infinity}
      minConstraints={[500, Infinity]}
      maxConstraints={[width - 300 - 100, Infinity]}
    >
      {currentDatabase ? (
        <>
          <Header>
            <HeaderTextContainer>
              <HeaderTitle>{currentConnection?.name}</HeaderTitle>
              <HeaderDatabaseDetails>
                <span>{currentDatabase?.name}</span>
                <span>
                  {currentDatabase?.keys} {t('keys')}
                </span>
              </HeaderDatabaseDetails>
            </HeaderTextContainer>
            <SearchInput
              onChange={handleSearchInputChange}
              value={searchInputValue}
            />
          </Header>

          <KeyListWrapper ref={parentRef}>
            <KeyListContainer
              style={{
                height: `${rowVirtualizer.totalSize}px`
              }}
            >
              {rowVirtualizer.virtualItems.map(virtualRow => {
                const key = filteredKeys[virtualRow.index]

                return (
                  <Key
                    key={virtualRow.index}
                    style={{
                      position: 'absolute',
                      top: 0,
                      left: 0,
                      width: '100%',
                      height: `${virtualRow.size}px`,
                      transform: `translateY(${virtualRow.start}px)`
                    }}
                  >
                    <KeyTextContainer
                      selected={currentKey === key}
                      onClick={() => handleSelectKey(key)}
                    >
                      <KeyTitle>{key}</KeyTitle>
                    </KeyTextContainer>
                  </Key>
                )
              })}
            </KeyListContainer>
          </KeyListWrapper>
        </>
      ) : (
        <EmptyContent message={t('empty')} />
      )}
    </Container>
  )
}
Example #7
Source File: index.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
PureLoadMoreSelector = (props: IProps) => {
  const {
    className = '',
    dropdownClassName = '',
    dropdownStyle = '',
    type: initType = SelectType.Normal,
    mode,
    value,
    onChange = emptyFun,
    onClickItem = emptyFun,
    placeholder = i18n.t('Please Select'),
    list = emptyArray,
    showSearch = true,
    allowClear = false,
    disabled = false,
    valueItemRender = defaultValueItemRender,
    chosenItemConvert,
    changeQuery = emptyFun,
    quickSelect = null,
    onDropdownVisible,
    onVisibleChange,
    dropdownMatchSelectWidth = true,
    valueChangeTrigger = 'onChange',
    forwardedRef,
    resultsRender,
    size = '',
    bordered = true,
    q: propsQ,
  } = props;
  const isMultiple = mode === 'multiple';
  const [visible, setVisible] = React.useState(false);
  const [chosenItem, setChosenItem] = React.useState([] as IOption[]);
  const [q, setQ] = React.useState(undefined as string | undefined);
  const [type, setType] = React.useState(initType);
  const [displayValue, setDisplayValue] = React.useState([] as IOption[]);
  const [contentWidth, setContentWidth] = React.useState('');
  const [innerValue, setInnerValue] = React.useState([value] as any[]);
  const [valueChanged, setValueChanged] = React.useState(false);
  const [compId, setCompId] = React.useState(uuid());
  const reqRef = React.useRef(null as any);

  const searchRef = React.useRef(null);
  const menuRef = React.useRef(null);
  const valueRef = React.useRef(null);
  useEffectOnce(() => {
    document.body.addEventListener('click', dropdownHide);

    if (forwardedRef) {
      forwardedRef.current = {
        show: (vis: boolean) => setVisible(vis),
      };
    }

    return () => {
      document.body.removeEventListener('click', dropdownHide);
    };
  });

  React.useEffect(() => {
    if (propsQ === undefined && q !== undefined) setQ(propsQ);
  }, [propsQ]);

  const searchRefCur = searchRef && searchRef.current;
  React.useEffect(() => {
    onDropdownVisible && onDropdownVisible(visible);
    if (visible && searchRefCur) {
      searchRefCur.focus();
    }
  }, [visible, searchRefCur]);

  // 带上select的dropdownMatchSelectWidth特性
  const dropdownMinWidth = get(document.querySelector(`.load-more-selector-dropdown-${compId}`), 'style.minWidth');
  React.useEffect(() => {
    if (dropdownMatchSelectWidth && dropdownMinWidth) {
      setContentWidth(dropdownMinWidth);
    }
  }, [dropdownMinWidth, dropdownMatchSelectWidth]);

  useDebounce(
    () => {
      // 如果是category时,清除q时不需要changeQuery,因为在changeCategory里会自动清理,此处阻止避免触发两个修改引起请求两次
      if (!(initType === SelectType.Category && !q)) changeQuery(q);
    },
    600,
    [q],
  );

  React.useEffect(() => {
    if (initType === SelectType.Category) {
      // 模式为category,内部根据q是否有值来切换模式
      if (type === SelectType.Category && q) {
        setType(SelectType.Normal);
      }
      if (type === SelectType.Normal && !q) {
        setType(SelectType.Category);
      }
    }
  }, [q, initType, type]);

  React.useEffect(() => {
    setInnerValue([value]);
  }, [value]);

  React.useEffect(() => {
    !visible && valueChangeTrigger === 'onClose' && valueChanged && onChange(...innerValue);
    setValueChanged(false);
  }, [visible]);

  React.useEffect(() => {
    if (isNumber(innerValue[0])) return setChosenItem(changeValue(innerValue[0], list, chosenItem));
    !isEmpty(innerValue[0]) ? setChosenItem(changeValue(innerValue[0], list, chosenItem)) : setChosenItem([]);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [innerValue, list]);

  React.useEffect(() => {
    if (chosenItemConvert) {
      const val = map(chosenItem, (item) => {
        if (item.label) return item;
        const curVal = find(displayValue, (cItem) => `${cItem.value}` === `${item.value}`) || {};
        return !isEmpty(curVal) ? { ...item, ...curVal } : item;
      });

      const getNewValue = () => {
        reqRef.current = chosenItemConvert(isMultiple ? val : val[0], list);
        if (isPromise(reqRef.current)) {
          reqRef.current.then((res: IOption[]) => {
            setDisplayValue(Array.isArray(res) ? res : [res]);
          });
        } else {
          reqRef.current
            ? setDisplayValue(Array.isArray(reqRef.current) ? reqRef.current : [reqRef.current])
            : setDisplayValue([]);
        }
      };

      if (isPromise(reqRef.current)) {
        reqRef.current.then(() => {
          getNewValue();
        });
      } else {
        getNewValue();
      }
    } else {
      setDisplayValue(chosenItem);
    }
  }, [chosenItem]);

  const dropdownHide = (e: any) => {
    // 点击外部,隐藏选项
    const menuEl = menuRef && menuRef.current;
    const valueEl = valueRef && valueRef.current;
    // eslint-disable-next-line react/no-find-dom-node
    const el1 = ReactDOM.findDOMNode(menuEl) as HTMLElement;
    // eslint-disable-next-line react/no-find-dom-node
    const el2 = ReactDOM.findDOMNode(valueEl) as HTMLElement;
    if (!((el1 && el1.contains(e.target)) || (el2 && el2.contains(e.target)))) {
      setVisible(false);
    }
  };

  const setValue = (v: IOption[]) => {
    const newValue = isMultiple ? map(v, 'value') : get(v, '[0].value');
    if (isEqual(newValue, innerValue[0])) return;
    const [vals, opts] = isMultiple ? [map(v, 'value'), v] : [get(v, '[0].value'), v[0]];
    setInnerValue([vals, opts]);
    setValueChanged(true);
    (!visible || valueChangeTrigger === 'onChange') && onChange(vals, opts);
  };

  const clearValue = () => {
    setValue([]);
  };

  const deleteValue = (item: IOption) => {
    // 如果单选且不允许清除,则不能删数据
    if (!isMultiple && !allowClear) return;
    setValue(filter(chosenItem, (c) => `${c.value}` !== `${item.value}`));
  };
  const addValue = (item: IOption) => {
    if (isMultiple) {
      if (find(chosenItem, (cItem) => `${cItem.value}` === `${item.value}`)) {
        setValue(
          map(chosenItem, (cItem) => {
            return cItem.value === item.value ? { ...item } : { ...cItem };
          }),
        );
      } else {
        setValue([...chosenItem, item]);
      }
    } else {
      setValue([item]);
    }
  };

  const clickItem = (item: IOption, check: boolean) => {
    check ? addValue(item) : deleteValue(item);
    if (!isMultiple && check) setVisible(false); // 非多选模式,选中后隐藏
    onClickItem(item);
  };

  const getOverlay = () => {
    const Comp = CompMap[type];

    const curQuickSelect = isArray(quickSelect) ? quickSelect : quickSelect ? [quickSelect] : [];
    return (
      <Menu className="load-more-dropdown-menu" ref={menuRef} style={{ width: contentWidth }}>
        {showSearch
          ? [
              <MenuItem key="_search-item">
                <div>
                  <Input
                    ref={searchRef}
                    size="small"
                    className="search"
                    prefix={<ErdaIcon type="search" size={'16'} fill="default-3" />}
                    placeholder={i18n.t('Search by keyword-char')}
                    value={q}
                    onChange={(e) => setQ(e.target.value)}
                  />
                </div>
              </MenuItem>,
            ]
          : null}
        {isMultiple
          ? [
              <MenuItem className="chosen-info" key="_chosen-info-item">
                <div className="w-full flex my-1">
                  <div>
                    {i18n.t('common:Selected')}
                    <span className="mx-0.5">{chosenItem.length}</span>
                    {i18n.t('common:item')}
                  </div>
                  {chosenItem.length ? (
                    <span className="fake-link ml-4 text-purple-deep" onClick={clearValue}>
                      {i18n.t('common:Clear selected')}
                    </span>
                  ) : null}
                </div>
              </MenuItem>,
              <Menu.Divider key="_chosen-info-divider" />,
            ]
          : null}
        {curQuickSelect.map((quickSelectItem, idx) => {
          return [<MenuItem key={`quick-select-${idx}`}>{quickSelectItem}</MenuItem>, <Menu.Divider />];
        })}
        <MenuItem className="options" key="options">
          <Comp {...props} width={contentWidth} clickItem={clickItem} value={chosenItem} isMultiple={isMultiple} />
          {/* {
            isMultiple && (
              <div className={`chosen-info ${type === SelectType.Normal ? 'border-top' : ''}`}>
                {i18n.t('common:Selected')}
                <span>{chosenItem.length}</span>
                {i18n.t('common:item')}
              </div>
            )
          } */}
        </MenuItem>
      </Menu>
    );
  };
  return (
    <div className={`load-more-selector ${className}`} ref={valueRef} onClick={(e) => e.stopPropagation()}>
      <Dropdown
        overlay={getOverlay()}
        visible={visible}
        overlayClassName={`load-more-selector-dropdown load-more-selector-dropdown-${compId} ${dropdownClassName}`}
        overlayStyle={dropdownStyle}
        onVisibleChange={(visible) => onVisibleChange?.(visible, innerValue)}
      >
        <div
          className={`results cursor-pointer ${disabled ? 'not-allowed' : ''} ${size} ${bordered ? '' : 'border-none'}`}
          onClick={() => {
            !disabled && !visible && setVisible(true);
          }}
        >
          {resultsRender ? (
            resultsRender(displayValue, deleteValue, isMultiple, list)
          ) : (
            <div className="values">
              {map(displayValue, (item) => (
                <div key={item.value} className="value-item">
                  {valueItemRender(item, deleteValue, isMultiple, list)}
                </div>
              ))}
              {placeholder && isEmpty(chosenItem) ? <span className="placeholder">{placeholder}</span> : null}
            </div>
          )}
          {allowClear && !isEmpty(chosenItem) ? (
            <ErdaIcon
              type="close-one"
              className="close"
              size="14px"
              onClick={(e: any) => {
                e.stopPropagation();
                clearValue();
              }}
            />
          ) : null}
        </div>
      </Dropdown>
    </div>
  );
}
Example #8
Source File: index.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
ApiMarketList = () => {
  const [{ keyword, ...state }, updater, update] = useUpdate<IState>({
    keyword: '',
    visible: false,
    scope: 'asset',
    mode: 'add',
    assetDetail: {},
    showApplyModal: false,
    showExportModal: false,
  });
  const [assetList, assetListPaging] = apiMarketStore.useStore((s) => [s.assetList, s.assetListPaging]);
  const { scope } = routeInfoStore.useStore((s) => s.params) as { scope: API_MARKET.AssetScope };
  const { getAssetList } = apiMarketStore.effects;
  const { resetAssetList } = apiMarketStore.reducers;
  const [isFetchList] = useLoading(apiMarketStore, ['getAssetList']);
  useUnmount(() => {
    resetAssetList();
  });
  const getList = (params: Pick<API_MARKET.QueryAssets, 'keyword' | 'pageNo' | 'scope' | 'pageSize'>) => {
    getAssetList({
      ...commonQuery,
      ...params,
    });
  };
  useDebounce(
    () => {
      getList({ keyword, pageNo: 1, scope });
    },
    200,
    [keyword, scope],
  );

  const reload = () => {
    getList({ keyword, pageNo: 1, scope });
  };

  const filterConfig = React.useMemo(
    (): Field[] => [
      {
        label: '',
        type: 'input',
        outside: true,
        key: 'keyword',
        placeholder: i18n.t('default:Search by keyword'),
        customProps: {
          autoComplete: 'off',
        },
      },
    ],
    [],
  );

  const handleSearch = (query: Record<string, any>) => {
    updater.keyword(query.keyword);
  };

  const handleTableChange = ({ pageSize, current }: PaginationProps) => {
    getList({ keyword, pageNo: current, pageSize, scope });
  };

  const handleManage = ({ assetID }: API_MARKET.Asset) => {
    goTo(goTo.pages.apiManageAssetVersions, { scope, assetID });
  };

  const gotoVersion = ({ asset, latestVersion }: API_MARKET.AssetListItem) => {
    goTo(goTo.pages.apiManageAssetDetail, { assetID: asset.assetID, scope, versionID: latestVersion.id });
  };

  const handleApply = (record: API_MARKET.Asset) => {
    update({
      showApplyModal: true,
      assetDetail: record || {},
    });
  };

  const closeModal = () => {
    update({
      visible: false,
      showApplyModal: false,
      assetDetail: {},
    });
  };

  const showAssetModal = (assetScope: IScope, mode: IMode, record?: API_MARKET.Asset) => {
    update({
      scope: assetScope,
      mode,
      visible: true,
      assetDetail: record || {},
    });
  };

  const toggleExportModal = () => {
    updater.showExportModal((prev: boolean) => !prev);
  };

  const columns: Array<ColumnProps<API_MARKET.AssetListItem>> = [
    {
      title: i18n.t('API name'),
      dataIndex: ['asset', 'assetName'],
      width: 240,
    },
    {
      title: i18n.t('API description'),
      dataIndex: ['asset', 'desc'],
    },
    {
      title: 'API ID',
      dataIndex: ['asset', 'assetID'],
      width: 200,
    },
    {
      title: i18n.t('Update time'),
      dataIndex: ['asset', 'updatedAt'],
      width: 200,
      render: (date) => moment(date).format('YYYY-MM-DD HH:mm:ss'),
    },
    {
      title: i18n.t('Creator'),
      dataIndex: ['asset', 'creatorID'],
      width: 160,
      render: (text) => (
        <Tooltip title={<UserInfo id={text} />}>
          <UserInfo.RenderWithAvatar id={text} />
        </Tooltip>
      ),
    },
  ];

  const actions: IActions<API_MARKET.AssetListItem> = {
    render: (record) => {
      const { permission, asset } = record;
      const { manage, addVersion, hasAccess } = permission;
      return [
        {
          title: i18n.t('Export'),
          onClick: () => {
            exportApi
              .fetch({
                versionID: record.latestVersion.id,
                specProtocol,
              })
              .then(() => {
                toggleExportModal();
              });
          },
        },
        {
          title: i18n.t('manage'),
          onClick: () => {
            handleManage(asset);
          },
          show: manage,
        },
        {
          title: i18n.t('add {name}', { name: i18n.t('Version') }),
          onClick: () => {
            showAssetModal('version', 'add', asset);
          },
          show: !!addVersion,
        },
        {
          title: i18n.t('apply to call'),
          onClick: () => {
            handleApply(asset);
          },
          show: hasAccess,
        },
      ];
    },
  };

  return (
    <div className="api-market-list">
      <TopButtonGroup>
        <Button onClick={toggleExportModal}>{i18n.t('Export Records')}</Button>
        <Button
          type="primary"
          onClick={() => {
            showAssetModal('asset', 'add');
          }}
        >
          {i18n.t('default:Add Resource')}
        </Button>
      </TopButtonGroup>
      <ErdaTable
        rowKey="asset.assetID"
        columns={columns}
        dataSource={assetList}
        pagination={{
          ...assetListPaging,
          current: assetListPaging.pageNo,
        }}
        onRow={(record) => {
          return {
            onClick: () => {
              gotoVersion(record);
            },
          };
        }}
        onChange={handleTableChange}
        loading={isFetchList}
        actions={actions}
        slot={<ConfigurableFilter fieldsList={filterConfig} onFilter={handleSearch} />}
      />
      <AssetModal
        visible={state.visible}
        scope={state.scope}
        mode={state.mode}
        formData={state.assetDetail as API_MARKET.Asset}
        onCancel={closeModal}
        afterSubmit={reload}
      />
      <ApplyModal
        visible={state.showApplyModal}
        onCancel={closeModal}
        dataSource={state.assetDetail as API_MARKET.Asset}
      />
      <ExportRecord visible={state.showExportModal} onCancel={toggleExportModal} />
    </div>
  );
}
Example #9
Source File: index.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
MiddlewareDashboard = () => {
  const [projectList, middlewares, middelwaresPaging] = middlewareDashboardStore.useStore((s) => [
    s.projectList,
    s.middlewares,
    s.middelwaresPaging,
  ]);
  const { getProjects, getMiddlewares, getAddonUsage, getAddonDailyUsage } = middlewareDashboardStore.effects;
  const { clearMiddlewares } = middlewareDashboardStore.reducers;
  const [getProjectsLoading, getMiddlewaresLoading] = useLoading(middlewareDashboardStore, [
    'getProjects',
    'getMiddlewares',
  ]);

  const [workspace, setWorkspace] = useState('ALL');
  const [projectId, setProjectId] = useState();
  const [addonName, setAddonName] = useState(undefined as string | undefined);
  const [ip, setIp] = useState(undefined as string | undefined);

  useDebounce(
    () => {
      const searchQuery = { workspace: workspace === 'ALL' ? undefined : workspace, projectId, addonName, ip };
      getMiddlewares(searchQuery);
      getAddonUsage(searchQuery);
      getAddonDailyUsage(searchQuery);
      return clearMiddlewares;
    },
    600,
    [workspace, projectId, addonName, ip, getMiddlewares, clearMiddlewares, getAddonUsage, getAddonDailyUsage],
  );

  const handleEnvChange = (value: string) => {
    setWorkspace(value);
  };

  const handleProjectChange = (value: string) => {
    setProjectId(value);
  };

  const handleSearchProjects = (searchKey?: string) => {
    getProjects(searchKey);
  };

  const handleSearchAddon = (searchKey?: string) => {
    setAddonName(searchKey || undefined);
  };

  const handleSearchIp = (searchKey?: string) => {
    setIp(searchKey || undefined);
  };

  useMount(() => {
    handleSearchProjects();
    getAddonUsage();
    getAddonDailyUsage();
  });

  // const overviewItemNameMap = {
  //   cpu: 'CPU',
  //   mem: i18n.t('memory'),
  //   nodes: i18n.t('Node'),
  // };

  const handleTableChange = (pagination: any) => {
    const { current, pageSize: page_Size } = pagination;
    getMiddlewares({
      workspace: workspace === 'ALL' ? undefined : workspace,
      projectId,
      addonName,
      pageNo: current,
      pageSize: page_Size,
    });
  };

  const middlewareCols: Array<ColumnProps<MIDDLEWARE_DASHBOARD.IMiddlewareDetail>> = [
    {
      title: i18n.t('addon'),
      dataIndex: 'name',
      key: 'name',
      width: '35%',
      render: (value: string) => <span className="hover-text font-bold">{value}</span>,
    },
    {
      title: i18n.t('cluster'),
      dataIndex: 'clusterName',
      key: 'clusterName',
      width: '20%',
    },
    {
      title: i18n.t('project'),
      dataIndex: 'projectName',
      key: 'projectName',
      width: '20%',
    },
    {
      title: 'CPU',
      dataIndex: 'cpu',
      key: 'cpu',
      width: '10%',
      sorter: (a: MIDDLEWARE_DASHBOARD.IMiddlewareDetail, b: MIDDLEWARE_DASHBOARD.IMiddlewareDetail) => a.cpu - b.cpu,
    },
    {
      title: i18n.t('memory'),
      dataIndex: 'mem',
      key: 'mem',
      width: '10%',
      render: (value: number) => getFormatter('CAPACITY', 'MB').format(value),
      sorter: (a: MIDDLEWARE_DASHBOARD.IMiddlewareDetail, b: MIDDLEWARE_DASHBOARD.IMiddlewareDetail) => a.mem - b.mem,
    },
    {
      title: i18n.t('Node'),
      dataIndex: 'nodes',
      key: 'nodes',
      width: 80,
      sorter: (a: MIDDLEWARE_DASHBOARD.IMiddlewareDetail, b: MIDDLEWARE_DASHBOARD.IMiddlewareDetail) =>
        a.nodes - b.nodes,
    },
    {
      title: i18n.t('cmp:Number of references'),
      dataIndex: 'attachCount',
      key: 'attachCount',
      width: 200,
      sorter: (a: MIDDLEWARE_DASHBOARD.IMiddlewareDetail, b: MIDDLEWARE_DASHBOARD.IMiddlewareDetail) =>
        a.attachCount - b.attachCount,
    },
  ];
  const { pageNo, pageSize, total } = middelwaresPaging;

  return (
    <>
      <div className="middleware-dashboard-content">
        <div className="middleware-dashboard-top mb-4">
          <div className="filter-group-ct mb-4">
            <Row gutter={20}>
              <Col span={6} className="filter-item">
                <div className="filter-item-label">{firstCharToUpper(i18n.t('environment'))}</div>
                <Select className="filter-item-content" value={workspace} onChange={handleEnvChange}>
                  {envOptions}
                </Select>
              </Col>
              <Col span={6} className="filter-item">
                <div className="filter-item-label">{firstCharToUpper(i18n.t('project'))}</div>
                <Select
                  showSearch
                  className="filter-item-content"
                  allowClear
                  value={projectId}
                  placeholder={i18n.t('Search by keyword')}
                  notFoundContent={
                    <IF check={getProjectsLoading}>
                      <Spin size="small" />
                    </IF>
                  }
                  filterOption={false}
                  onSearch={handleSearchProjects}
                  onChange={handleProjectChange}
                >
                  {map(projectList, (d) => (
                    <Option key={d.id}>{d.name}</Option>
                  ))}
                </Select>
              </Col>
              <Col span={6} className="filter-item">
                <div className="filter-item-label">Addon</div>
                <Search
                  allowClear
                  className="filter-item-content"
                  placeholder={i18n.t('search by Addon name or ID')}
                  onChange={(e) => handleSearchAddon(e.target.value)}
                />
              </Col>
              <Col span={6} className="filter-item">
                <div className="filter-item-label">IP</div>
                <Search
                  allowClear
                  className="filter-item-content"
                  placeholder={i18n.t('cmp:search by container IP')}
                  onChange={(e) => handleSearchIp(e.target.value)}
                />
              </Col>
            </Row>
          </div>
          {/* <Row className="middleware-overview-ct mb-4" type="flex" justify="space-between" gutter={50}>
          {
            map(overview, (v, k) => (
              <Col span={8} key={k}>
                <div className="middleware-overview-item border-all">
                  <div className="title mb-5">{overviewItemNameMap[k]}</div>
                  <div className="num">{v}</div>
                </div>
              </Col>
            ))
          }
        </Row> */}
          <div className="bg-white px-2 py-3">
            <AddonUsageChart />
          </div>
        </div>
      </div>
      <ErdaTable
        className="cursor-pointer"
        rowKey="instanceId"
        columns={middlewareCols}
        dataSource={middlewares}
        loading={getMiddlewaresLoading}
        pagination={{
          current: pageNo,
          pageSize,
          total,
        }}
        onRow={({ instanceId }: MIDDLEWARE_DASHBOARD.IMiddlewareDetail) => ({
          onClick: () => {
            goTo(`./${instanceId}/monitor`);
          },
        })}
        onChange={handleTableChange}
        scroll={{ x: '100%' }}
      />
    </>
  );
}
Example #10
Source File: index.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
ServiceManager = () => {
  const [propsContainerList, propsServiceList, runtimeJson, runtimeStatus, serviceReqStatus, metrics] =
    dcosServiceStore.useStore((s) => [
      s.containerList,
      s.serviceList,
      s.runtimeJson,
      s.runtimeStatus,
      s.serviceReqStatus,
      s.metrics,
    ]);
  const list = clusterStore.useStore((s) => s.list);
  const { getClusterList } = clusterStore.effects;
  const { getContainerList, getServiceList, getRuntimeJson, getRuntimeStatus } = dcosServiceStore.effects;
  const { clearRuntimeJson, clearRuntimeStatus } = dcosServiceStore.reducers;
  const [isFetchingClusters] = useLoading(clusterStore, ['getClusterList']);
  const [isFetchingServices, isFetchingContainers] = useLoading(dcosServiceStore, [
    'getServiceList',
    'getContainerList',
  ]);

  const [{ path, cluster, environment, ip, serviceList, containerList }, updater, update] = useUpdate<IState>({
    path: [{ q: '', name: '' }],
    cluster: '',
    environment: 'dev',
    ip: undefined,
    serviceList: [],
    containerList: [],
  });

  React.useEffect(() => {
    update({
      serviceList: propsServiceList,
      containerList: propsContainerList,
    });
  }, [update, propsContainerList, propsServiceList]);

  useEffectOnce(() => {
    getClusterList().then((_list: ORG_CLUSTER.ICluster[]) => {
      !isEmpty(_list) &&
        update({
          cluster: _list[0].name,
          path: [{ q: _list[0].name, name: _list[0].name }],
        });
    });
    return () => {
      clearInterval(reqSt);
    };
  });

  const fetchServiceList = React.useCallback(
    (q: { paths: DCOS_SERVICES.path[]; environment: string; ip?: string }) => {
      const depth = q.paths.length;
      if (depth < 5 && depth > 0) {
        getServiceList(q);
      }
    },
    [getServiceList],
  );

  useDebounce(
    () => {
      fetchServiceList({ paths: path, environment, ip });
    },
    300,
    [ip, path, environment],
  );

  useUpdateEffect(() => {
    clearInterval(reqSt);
    if (['runtime', 'service'].includes(curLevel() as string)) {
      reqRuntimeStatus();
      reqSt = setInterval(() => reqRuntimeStatus(), 5000);
    }
  }, [serviceList]);

  useUpdateEffect(() => {
    combineStatuToService(runtimeStatus);
  }, [runtimeStatus]);

  useUpdateEffect(() => {
    const { cpu, mem } = metrics;
    cpu?.loading === false && mem?.loading === false && combineMetricsToList(formatMetricsToObj(metrics)); // 两部分数据都返回后才开始combine数据
  }, [metrics]);

  useUpdateEffect(() => {
    if (!serviceReqStatus) {
      clearInterval(reqSt);
    }
  }, [serviceReqStatus]);

  const formatMetricsToObj = (_metrics: {
    cpu: { data: Array<{ tag: string; data: number }> };
    mem: { data: Array<{ tag: string; data: number }> };
  }) => {
    const metricsObj = {};
    const { cpu, mem } = _metrics;
    (cpu.data || []).forEach((cItem) => {
      cItem.tag && (metricsObj[cItem.tag] = { cpuUsagePercent: cItem.data / 100 || 0, diskUsage: 0 });
    });
    (mem.data || []).forEach((mItem) => {
      if (mItem.tag) {
        !metricsObj[mItem.tag] && (metricsObj[mItem.tag] = {});
        metricsObj[mItem.tag].memUsage = mItem.data || 0;
      }
    });
    return metricsObj;
  };

  const combineMetricsToList = (metricsObj: Obj) => {
    let newContainerList = [...containerList];
    let newServiceList = [...serviceList];
    if (curLevel('container')) {
      // 当前层级在container上,
      newContainerList = newContainerList.map((item) => {
        let _metrics = null;
        try {
          _metrics = metricsObj[item.containerId] || null;
        } catch (e) {
          _metrics = null;
        }
        return { ...item, metrics: _metrics };
      });
      updater.containerList(newContainerList);
    } else {
      const combineKye = curLevel('service') ? 'name' : 'id';
      newServiceList = newServiceList.map((item) => {
        let _metrics = null;
        try {
          _metrics = metricsObj[item[combineKye]] || null;
        } catch (e) {
          _metrics = null;
        }
        return { ...item, metrics: _metrics };
      });
      updater.serviceList(newServiceList);
    }
  };

  const onJsonShow = (_visible: boolean) => {
    const runtimeId = getLevel('runtime').id;
    _visible && runtimeJson === null && runtimeId !== 'unknown' && getRuntimeJson({ runtimeId });
  };

  const combineStatuToService = (_runtimeStatus: Obj) => {
    let newServiceList = [...serviceList];
    if (curLevel('runtime')) {
      // runtime的status在runtimeStatus中runtimeId为key对象中
      newServiceList = newServiceList.map((item) => {
        let status = '';
        try {
          status = runtimeStatus[item.id].status || '';
        } catch (e) {
          status = '';
        }
        return { ...item, status };
      });
    } else if (curLevel('service')) {
      // service的status在runtimeStatus对应runtimeId为key的对象中的more字段中
      const runtimeId = getLevel('runtime').id;
      newServiceList = newServiceList.map((item) => {
        let status = '';
        try {
          status = runtimeStatus[runtimeId].more[item.name] || '';
        } catch (e) {
          status = '';
        }
        return { ...item, status };
      });
    }
    updater.serviceList(newServiceList);
  };

  const into = (p: { q: string; name: string }) => {
    if (curLevel('runtime')) clearRuntimeJson();
    const newPath = path.concat(p);
    update({
      path: newPath,
    });
    const depth = newPath.length;
    if (depth >= 5) {
      getContainerList(newPath);
    }
  };

  const backTo = (depth: number) => {
    if (curLevel('runtime')) clearRuntimeJson();
    update({
      path: path.slice(0, depth + 1),
    });
  };

  const curLevel = (lev = '') => {
    const levArr = ['project', 'application', 'runtime', 'service', 'container'];
    const curLev = levArr[path.length - 1];
    return lev ? lev === curLev : curLev;
  };

  const getLevel = (lev = '') => {
    const levs = {
      project: path[1] ? { id: path[1].q, name: path[1].name } : null,
      application: path[2] ? { id: path[2].q, name: path[2].name } : null,
      runtime: path[3] ? { id: path[3].q, name: path[3].name } : null,
      service: path[4] ? { id: path[4].q, name: path[4].name } : null,
    };
    return levs[lev] || null;
  };

  const handleEnvChange = (_environment: string) => {
    update({ environment: _environment });
  };

  const handleClusterChange = (_cluster: string) => {
    update({
      cluster: _cluster,
      path: [{ q: _cluster, name: cluster }],
    });
  };

  const reqRuntimeStatus = () => {
    let runtimeIds = '';
    if (curLevel('runtime')) {
      // runtime,批量查询runtime的状态
      runtimeIds = map(serviceList, 'id').join(',');
    } else if (curLevel('service')) {
      // service,查询单个runtime状态
      runtimeIds = getLevel('runtime').id;
    }
    if (runtimeIds && runtimeIds !== 'unknown') {
      getRuntimeStatus({ runtimeIds });
    } else {
      clearInterval(reqSt);
    }
  };
  const jsonString = runtimeJson === null ? '' : JSON.stringify(runtimeJson, null, 2);

  const slot = (
    <IF check={path.length === 1}>
      <div className="filter-group mb-4 ml-3-group">
        <Select
          value={cluster}
          className="w-[150px] bg-black-06 rounded"
          bordered={false}
          onChange={handleClusterChange}
        >
          {map(list, (v) => (
            <Option key={v.name} value={v.name}>
              {v.displayName || v.name}
            </Option>
          ))}
        </Select>
        <Input
          allowClear
          value={ip}
          className="w-[150px] bg-black-06 rounded"
          placeholder={i18n.t('cmp:Search by IP')}
          onChange={(e) => update({ ip: e.target.value })}
        />
        <Select
          value={environment}
          className="w-[150px] bg-black-06 rounded"
          bordered={false}
          onChange={handleEnvChange}
        >
          {map(ENV_MAP, (v, k) => (
            <Option key={k} value={k}>
              {v.cnName}
            </Option>
          ))}
        </Select>
      </div>
    </IF>
  );

  return (
    <Spin spinning={isFetchingClusters}>
      <Holder when={isEmpty(list)}>
        <Breadcrumb
          separator={<ErdaIcon className="text-xs align-middle" type="right" size="14px" />}
          className="path-breadcrumb"
        >
          {path.map((p, i) => {
            const isLast = i === path.length - 1;
            return (
              <Breadcrumb.Item
                key={i}
                className={isLast ? '' : 'hover-active'}
                onClick={() => {
                  if (!isLast) backTo(i);
                }}
              >
                {p.name}
              </Breadcrumb.Item>
            );
          })}
        </Breadcrumb>
        <div className="to-json">
          {path.length === 4 ? (
            <JsonChecker
              buttonText={i18n.t('runtime configs')}
              jsonString={jsonString}
              onToggle={onJsonShow}
              modalConfigs={{ title: i18n.t('runtime configs') }}
            />
          ) : null}
        </div>
      </Holder>
      <Spin spinning={isFetchingServices || isFetchingContainers}>
        <PureServiceList
          into={into}
          slot={slot}
          depth={path.length}
          serviceList={serviceList}
          onReload={
            path.length < 5 ? () => fetchServiceList({ paths: path, environment, ip }) : () => getContainerList(path)
          }
          containerList={containerList}
          haveMetrics={false}
          extraQuery={{ filter_cluster_name: cluster }}
        />
        {path.length === 2 ? (
          <AssociatedAddons projectId={path[1].q} environment={ENV_MAP[environment].enName} />
        ) : null}
      </Spin>
    </Spin>
  );
}
Example #11
Source File: publisher-list-v2.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
PurePublisherList = ({
  list = [],
  paging,
  getList,
  clearList,
  isFetching,
  onItemClick,
}: IPubliserListProps) => {
  const [{ q, formVisible }, updater] = useUpdate({
    q: undefined as string | undefined,
    formVisible: false,
  });
  const { mode } = routeInfoStore.useStore((s) => s.params);
  const publishOperationAuth = usePerm((s) => s.org.publisher.operation.pass);

  useUnmount(clearList);

  useUpdateEffect(() => {
    updater.q(undefined);
  }, [mode]);

  useDebounce(
    () => {
      getList({
        q,
        pageNo: 1,
        type: mode,
      });
    },
    300,
    [q, mode],
  );

  const onSubmit = ({ q: value }: { q: string }) => {
    updater.q(value);
  };

  const goToPublisher = (item: PUBLISHER.IPublisher) => {
    onItemClick(item);
  };

  const handlePageChange = (pageNo: number, pageSize: number) => {
    getList({
      q,
      pageNo,
      pageSize,
      type: mode,
    });
  };

  const openFormModal = () => {
    updater.formVisible(true);
  };
  const closeFormModal = () => {
    updater.formVisible(false);
  };

  const afterSubmitAction = (_isUpdate: boolean, data: PUBLISHER.IArtifacts) => {
    const { id, type } = data;
    goTo(`../${type}/${id}`);
  };

  const column: Array<ColumnProps<PUBLISHER.IPublisher>> = [
    {
      title: i18n.t('publisher:publisher content name'),
      dataIndex: 'name',
      width: 240,
    },
    {
      title: i18n.t('Description'),
      dataIndex: 'desc',
    },
    ...insertWhen<ColumnProps<PUBLISHER.IPublisher>>(mode === 'LIBRARY', [
      {
        title: i18n.t('version number'),
        width: 160,
        dataIndex: 'latestVersion',
        render: (text) => text || '-',
      },
      {
        title: i18n.t('publisher:subscriptions'),
        width: 120,
        dataIndex: 'refCount',
        render: (text) => text || 0,
      },
    ]),
    {
      title: i18n.t('default:status'),
      width: 120,
      dataIndex: 'public',
      render: (bool) => {
        return (
          <span className={`item-status ${bool ? 'on' : 'off'}`}>
            {bool ? i18n.t('publisher:published') : i18n.t('publisher:withdrawn')}
          </span>
        );
      },
    },
  ];
  const config = React.useMemo(
    () => [
      {
        type: Input,
        name: 'q',
        customProps: {
          placeholder: i18n.t('filter by {name}', { name: i18n.t('publisher:publisher content name') }),
          autoComplete: 'off',
        },
      },
    ],
    [],
  );
  return (
    <Spin spinning={isFetching}>
      <div className="publisher-list-section">
        <TopButtonGroup>
          <WithAuth pass={publishOperationAuth} tipProps={{ placement: 'bottom' }}>
            <Button type="primary" onClick={() => openFormModal()}>
              {i18n.t('publisher:Add')}
            </Button>
          </WithAuth>
        </TopButtonGroup>
        <ErdaTable
          rowKey="id"
          columns={column}
          dataSource={list}
          onRow={(record: PUBLISHER.IPublisher) => {
            return {
              onClick: () => {
                goToPublisher(record);
              },
            };
          }}
          pagination={{
            current: paging.pageNo,
            ...paging,
            onChange: handlePageChange,
          }}
          slot={
            <CustomFilter
              key={mode}
              config={config}
              onSubmit={onSubmit}
              onReset={() => {
                updater.q('');
              }}
            />
          }
        />
        <ArtifactsFormModal visible={formVisible} onCancel={closeFormModal} afterSubmit={afterSubmitAction} />
      </div>
    </Spin>
  );
}
Example #12
Source File: useEstimatedOutput.ts    From mStable-apps with GNU Lesser General Public License v3.0 4 votes vote down vote up
useEstimatedOutput = (
  inputValue?: BigDecimalInputValue,
  outputValue?: BigDecimalInputValue,
  lpPriceAdjustment?: LPPriceAdjustment,
  shouldSkip?: boolean,
): Output => {
  const inputValuePrev = usePrevious(inputValue)
  const outputValuePrev = usePrevious(outputValue)

  const [estimatedOutputRange, setEstimatedOutputRange] = useFetchState<[BigDecimal, BigDecimal]>()

  const [action, setAction] = useState<Action | undefined>()

  const signer = useSigner()
  const massetState = useSelectedMassetState() as MassetState
  const { address: massetAddress, fAssets, bAssets, feeRate: swapFeeRate, redemptionFeeRate } = massetState

  const poolAddress = Object.values(fAssets).find(
    f =>
      f.feederPoolAddress === inputValue?.address ||
      f.feederPoolAddress === outputValue?.address ||
      f.address === inputValue?.address ||
      f.address === outputValue?.address,
  )?.feederPoolAddress

  const inputRatios = useMassetInputRatios()
  const scaledInput = useScaledInput(inputValue, outputValue, inputRatios)

  const contract: Contract | undefined = useMemo(() => {
    if (!signer) return

    // use feeder pool to do swap
    if (poolAddress) {
      return FeederPool__factory.connect(poolAddress, signer)
    }
    return Masset__factory.connect(massetAddress, signer)
  }, [poolAddress, massetAddress, signer])

  const isFeederPool = contract?.address === poolAddress

  const exchangeRate = useMemo<FetchState<number>>(() => {
    if (shouldSkip) return {}

    if (estimatedOutputRange.fetching) return { fetching: true }
    if (!scaledInput?.high || !outputValue || !estimatedOutputRange.value) return {}

    const [, high] = estimatedOutputRange.value

    if (!high.exact.gt(0) || !scaledInput.high.exact.gt(0)) {
      return { error: 'Amount must be greater than zero' }
    }

    const value = high.simple / scaledInput.high.simple
    return { value }
  }, [estimatedOutputRange, scaledInput, outputValue, shouldSkip])

  const feeRate = useMemo<FetchState<BigDecimal>>(() => {
    if (shouldSkip || !withFee.has(action) || !estimatedOutputRange.value?.[1]) return {}

    if (estimatedOutputRange.fetching) return { fetching: true }

    const _feeRate = action === Action.SWAP ? swapFeeRate : redemptionFeeRate

    const swapFee = estimatedOutputRange.value[1]
      .scale()
      .divPrecisely(BigDecimal.ONE.sub(_feeRate))
      .sub(estimatedOutputRange.value[1].scale())

    return { value: swapFee }
  }, [action, estimatedOutputRange, swapFeeRate, redemptionFeeRate, shouldSkip])

  const priceImpact = useMemo<FetchState<PriceImpact>>(() => {
    if (estimatedOutputRange.fetching || !estimatedOutputRange.value) return { fetching: true }

    if (!scaledInput || !scaledInput.high.exact.gt(0)) return {}

    const value = getPriceImpact([scaledInput.scaledLow, scaledInput.scaledHigh], estimatedOutputRange.value, lpPriceAdjustment)

    return { value }
  }, [estimatedOutputRange.fetching, estimatedOutputRange.value, lpPriceAdjustment, scaledInput])

  /*
   * |------------------------------------------------------|
   * | ROUTES                                               |
   * | -----------------------------------------------------|
   * | Input  | Output | Function      | Tokens             |
   * | -----------------------------------------------------|
   * | basset | masset | masset mint   | 1 basset, 1 masset |
   * | masset | basset | masset redeem | 1 masset, 1 basset |
   * | basset | basset | masset swap   | 2 bassets          |
   * | fasset | basset | fpool swap    | 1 fasset           |
   * | fasset | masset | fpool swap    | 1 fasset           |
   * |------------------------------------------------------|
   */

  const inputEq = inputValuesAreEqual(inputValue, inputValuePrev)
  const outputEq = inputValuesAreEqual(outputValue, outputValuePrev)
  const eq = inputEq && outputEq

  const [update] = useDebounce(
    () => {
      if (!scaledInput || !outputValue || shouldSkip || !contract) return

      const { address: inputAddress, decimals: inputDecimals } = inputValue
      const { address: outputAddress, decimals: outputDecimals } = outputValue

      const isLPRedeem = contract.address === inputAddress
      const isLPMint = contract.address === outputAddress
      const isMassetMint = bAssets[inputAddress]?.address && outputAddress === massetAddress
      const isBassetSwap = [inputAddress, outputAddress].filter(address => bAssets[address]?.address).length === 2
      const isInvalid = inputAddress === outputAddress

      if (!scaledInput.high.exact.gt(0)) return

      // same -> same; fallback to input value 1:1
      if (isInvalid) {
        setEstimatedOutputRange.value([scaledInput.scaledLow, scaledInput.high])
        return
      }

      let outputLowPromise: Promise<BigNumber> | undefined
      let outputHighPromise: Promise<BigNumber> | undefined

      if (isMassetMint || isLPMint) {
        setAction(Action.MINT)
        outputLowPromise = contract.getMintOutput(inputAddress, scaledInput.low.scale(inputDecimals).exact)
        outputHighPromise = contract.getMintOutput(inputAddress, scaledInput.high.exact)
      } else if ((isFeederPool || isBassetSwap) && !isLPRedeem) {
        setAction(Action.SWAP)
        outputLowPromise = contract.getSwapOutput(inputAddress, outputAddress, scaledInput.low.scale(inputDecimals).exact)
        outputHighPromise = contract.getSwapOutput(inputAddress, outputAddress, scaledInput.high.exact)
      } else if (!isFeederPool || isLPRedeem) {
        setAction(Action.REDEEM)
        outputLowPromise = contract.getRedeemOutput(outputAddress, scaledInput.low.scale(inputDecimals).exact)
        outputHighPromise = contract.getRedeemOutput(outputAddress, scaledInput.high.exact)
      }

      if (outputLowPromise && outputHighPromise) {
        setEstimatedOutputRange.fetching()

        Promise.all([outputLowPromise, outputHighPromise])
          .then(data => {
            const [_low, _high] = data
            const low = new BigDecimal(_low, outputDecimals)
            const high = new BigDecimal(_high, outputDecimals)
            setEstimatedOutputRange.value([low, high])
          })
          .catch(_error => {
            setEstimatedOutputRange.error(sanitizeMassetError(_error))
          })
        return
      }

      setEstimatedOutputRange.value()
    },
    2500,
    [eq],
  )

  useEffect(() => {
    if (shouldSkip) return

    if (!eq && contract && scaledInput && outputValue) {
      if (scaledInput.high.exact.gt(0)) {
        setEstimatedOutputRange.fetching()
        update()
      } else {
        setEstimatedOutputRange.value()
      }
    }
  }, [eq, contract, setEstimatedOutputRange, update, scaledInput, outputValue, shouldSkip])

  return useMemo(
    () => ({
      estimatedOutputAmount: {
        fetching: estimatedOutputRange.fetching,
        error: estimatedOutputRange.error,
        value: estimatedOutputRange.value?.[1],
      },
      priceImpact,
      exchangeRate,
      feeRate,
    }),
    [estimatedOutputRange, priceImpact, exchangeRate, feeRate],
  )
}
Example #13
Source File: useEstimatedOutputMulti.ts    From mStable-apps with GNU Lesser General Public License v3.0 4 votes vote down vote up
useEstimatedOutputMulti = (
  route: Route,
  scaledInputs: ScaledInputs,
  lpPriceAdjustment?: LPPriceAdjustment,
  contract?: MintableContract,
): Output => {
  const [estimatedOutputRange, setEstimatedOutputRange] = useFetchState<[BigDecimal, BigDecimal]>()

  const priceImpact = useMemo<FetchState<PriceImpact>>(() => {
    if (estimatedOutputRange.fetching) return { fetching: true }

    if (scaledInputs.scaledHighTotal.exact.eq(0) || !estimatedOutputRange.value) return {}

    const value = getPriceImpact(
      [scaledInputs.scaledLowTotal, scaledInputs.scaledHighTotal],
      estimatedOutputRange.value,
      lpPriceAdjustment,
      route === Route.Redeem,
    )

    return { value }
  }, [estimatedOutputRange.fetching, estimatedOutputRange.value, scaledInputs, lpPriceAdjustment, route])

  const [update] = useDebounce(
    () => {
      if (!contract || Object.values(scaledInputs.values).length === 0) return {}

      setEstimatedOutputRange.fetching()

      const addresses = Object.keys(scaledInputs.values)
      const lowAmounts = addresses.map(address => scaledInputs.values[address].low.exact)
      const highAmounts = addresses.map(address => scaledInputs.values[address].high.exact)

      const paths = ((): Promise<BigNumber>[] => {
        switch (route) {
          case Route.Mint: {
            const outputLow = contract.getMintMultiOutput(addresses, lowAmounts)
            const outputHigh = contract.getMintMultiOutput(addresses, highAmounts)
            return [outputLow, outputHigh]
          }
          case Route.Redeem: {
            const outputLow = contract.getRedeemExactBassetsOutput(addresses, lowAmounts)
            const outputHigh = contract.getRedeemExactBassetsOutput(addresses, highAmounts)
            return [outputLow, outputHigh]
          }
          default:
            return []
        }
      })()

      Promise.all(paths)
        .then(data => {
          const [_low, _high] = data
          const low = new BigDecimal(_low)
          const high = new BigDecimal(_high)
          setEstimatedOutputRange.value([low, high])
        })
        .catch((_error: Error): void => {
          setEstimatedOutputRange.error(sanitizeMassetError(_error))
        })
    },
    2500,
    [contract, scaledInputs],
  )

  const amountIsSet = scaledInputs.highTotal.exact.gt(0)

  useEffect(() => {
    if (!contract) return

    if (amountIsSet) {
      setEstimatedOutputRange.fetching()
      update()
    } else {
      setEstimatedOutputRange.value()
    }
  }, [contract, setEstimatedOutputRange, amountIsSet, update])

  return useMemo(
    () => ({
      estimatedOutputAmount: {
        fetching: estimatedOutputRange.fetching,
        error: estimatedOutputRange.error,
        value: estimatedOutputRange.value?.[1],
      },
      priceImpact,
    }),
    [estimatedOutputRange, priceImpact],
  )
}
Example #14
Source File: useSaveOutput.ts    From mStable-apps with GNU Lesser General Public License v3.0 4 votes vote down vote up
useSaveOutput = (route?: SaveRoutes, inputAddress?: string, inputAmount?: BigDecimal): FetchState<SaveOutput> => {
  const [saveOutput, setSaveOutput] = useFetchState<SaveOutput>()
  const networkAddresses = useNetworkAddresses()
  const networkPrices = useNetworkPrices()
  const nativeTokenPriceSimple = networkPrices.value?.nativeToken
  const selectedMassetPrice = useSelectedMassetPrice()

  const signer = useSigner() as Signer

  const massetState = useSelectedMassetState() as MassetState
  const {
    address: massetAddress,
    bAssets,
    feederPools,
    savingsContracts: {
      v2: { address: saveAddress, latestExchangeRate: { rate: latestExchangeRate } = {} },
    },
  } = massetState

  const inputToken = useTokenSubscription(inputAddress)
  const outputToken = useTokenSubscription(saveAddress)

  const inputRatios = useMassetInputRatios()
  const scaledInput = useScaledInput(
    { ...inputToken, amount: inputAmount } as BigDecimalInputValue,
    { ...outputToken, amount: BigDecimal.ZERO },
    inputRatios,
  )

  const scaledInputJSON = toScaledInputJSON(scaledInput)
  const feederPoolAddress = inputAddress && Object.values(feederPools).find(fp => fp.fasset.address === inputAddress)?.address

  const [update] = useDebounce(
    () => {
      if (!scaledInputJSON || !inputAddress || !signer) return setSaveOutput.value()

      const _scaledInput = fromScaledInputJSON(scaledInputJSON)

      if (
        !latestExchangeRate ||
        !networkAddresses ||
        !nativeTokenPriceSimple ||
        !selectedMassetPrice.value ||
        ((route === SaveRoutes.SwapAndSave || route === SaveRoutes.SwapAndStake) && !feederPoolAddress)
      ) {
        return setSaveOutput.fetching()
      }

      let promise: Promise<SaveOutput>
      switch (route) {
        case SaveRoutes.Save:
        case SaveRoutes.Stake:
        case SaveRoutes.SaveAndStake:
          promise = Promise.resolve({
            amount: _scaledInput.high,
          })
          break

        case SaveRoutes.BuyAndSave:
        case SaveRoutes.BuyAndStake:
          promise = (async () => {
            const [{ amount: low }, { amount: high, path, amountOut }] = await Promise.all([
              getOptimalBasset(signer, networkAddresses, massetAddress, bAssets, _scaledInput.low.exact),
              getOptimalBasset(signer, networkAddresses, massetAddress, bAssets, _scaledInput.high.exact),
            ])

            const nativeTokenPrice = BigDecimal.fromSimple(nativeTokenPriceSimple).exact
            const massetPrice = BigDecimal.fromSimple(selectedMassetPrice.value)

            const buyLow = _scaledInput.scaledLow.mulTruncate(nativeTokenPrice).divPrecisely(massetPrice)
            const buyHigh = _scaledInput.scaledHigh.mulTruncate(nativeTokenPrice).divPrecisely(massetPrice)

            const priceImpact = getPriceImpact([buyLow, buyHigh], [low, high])

            return {
              amount: high,
              amountOut,
              path,
              priceImpact,
            }
          })()
          break

        case SaveRoutes.MintAndSave:
        case SaveRoutes.MintAndStake:
          promise = (async () => {
            const contract = Masset__factory.connect(massetAddress, signer)

            const [_low, _high] = await Promise.all([
              contract.getMintOutput(inputAddress, _scaledInput.low.scale(scaledInput.high.decimals).exact),
              contract.getMintOutput(inputAddress, _scaledInput.high.exact),
            ])

            const low = new BigDecimal(_low)
            const high = new BigDecimal(_high)

            const priceImpact = getPriceImpact([_scaledInput.scaledLow, _scaledInput.scaledHigh], [low, high])

            return {
              amount: high,
              priceImpact,
            }
          })()
          break

        case SaveRoutes.SwapAndSave:
        case SaveRoutes.SwapAndStake:
          promise = (async () => {
            const contract = FeederPool__factory.connect(feederPoolAddress as string, signer)

            const [_low, _high] = await Promise.all([
              contract.getSwapOutput(inputAddress, massetAddress, _scaledInput.low.scale(scaledInput.high.decimals).exact),
              contract.getSwapOutput(inputAddress, massetAddress, _scaledInput.high.exact),
            ])

            const low = new BigDecimal(_low)
            const high = new BigDecimal(_high)

            const priceImpact = getPriceImpact([_scaledInput.scaledLow, _scaledInput.scaledHigh], [low, high])

            return {
              amount: high,
              priceImpact,
            }
          })()
          break

        default:
          return setSaveOutput.value()
      }

      setSaveOutput.fetching()

      return promise
        .then((output): void => {
          setSaveOutput.value(output)
        })
        .catch((_error: Error): void => {
          setSaveOutput.error(sanitizeMassetError(_error))
        })
    },
    1000,
    [scaledInputJSON, inputAddress, massetAddress, feederPoolAddress],
  )

  useEffect(() => {
    if (inputAmount?.exact.gt(0) && inputAddress) {
      setSaveOutput.fetching()
      update()
    } else {
      setSaveOutput.value()
    }
  }, [inputAddress, inputAmount, setSaveOutput, update])

  return saveOutput
}