@ant-design/icons#PlusCircleOutlined TypeScript Examples

The following examples show how to use @ant-design/icons#PlusCircleOutlined. 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 XFlow with MIT License 6 votes vote down vote up
GraphToolbar = (props: Props) => {
  const { onAddNodeClick, onDeleteNodeClick, onConnectEdgeClick } = props
  const [selectedNodes, setSelectedNodes] = React.useState([])

  /** 监听画布中选中的节点 */
  const watchModelService = async () => {
    const appRef = useXFlowApp()
    const modelService = appRef && appRef?.modelService
    if (modelService) {
      const model = await MODELS.SELECTED_NODES.getModel(modelService)
      model.watch(async () => {
        const nodes = await MODELS.SELECTED_NODES.useValue(modelService)
        setSelectedNodes(nodes)
      })
    }
  }

  watchModelService()

  return (
    <div className="xflow-er-solution-toolbar">
      <div className="icon" onClick={() => onAddNodeClick()}>
        <span>添加节点</span>
        <PlusCircleOutlined />
      </div>
      <div className="icon" onClick={() => onConnectEdgeClick()}>
        <span>添加关系</span>
        <LinkOutlined />
      </div>
      <div
        className={`icon ${selectedNodes?.length > 0 ? '' : 'disabled'}`}
        onClick={() => onDeleteNodeClick()}
      >
        <span>删除节点</span>
        <DeleteOutlined />
      </div>
    </div>
  )
}
Example #2
Source File: index.tsx    From drip-table with MIT License 6 votes vote down vote up
public renderFormItem(item: unknown, index: number) {
    return (
      <div className={styles['array-component-form-container']} key={index}>
        <div className={styles['array-component-left-container']}>
          { (this.props.schema['ui:props']?.items as DTGComponentPropertySchema[])
            .map((schema, i) => this.renderAttributeItem(schema, i, index)) }
        </div>
        <div className={styles['array-component-right-container']}>
          <Button
            icon={<PlusCircleOutlined />}
            shape="circle"
            size="small"
            onClick={() => {
              const currentValue = this.props.value?.slice() || [];
              currentValue.splice(index + 1, 0, { paramName: '', prefix: '', suffix: '' });
              this.props.onChange?.(currentValue);
            }}
          />
          <Button
            icon={<MinusCircleOutlined />}
            shape="circle"
            size="small"
            onClick={() => {
              const currentValue = this.props.value?.slice() || [];
              currentValue.splice(index, 1);
              this.props.onChange?.(currentValue);
            }}
          />
        </div>
      </div>
    );
  }
Example #3
Source File: EmptyStates.tsx    From posthog-foss with MIT License 5 votes vote down vote up
export function SavedInsightsEmptyState(): JSX.Element {
    const { addGraph } = useActions(savedInsightsLogic)
    const {
        filters: { tab },
        insights,
        usingFilters,
    } = useValues(savedInsightsLogic)

    // show the search string that was used to make the results, not what it currently is
    const searchString = insights.filters?.search || null
    const { title, description } = SAVED_INSIGHTS_COPY[tab]

    return (
        <div className="saved-insight-empty-state">
            <div className="empty-state-inner">
                <div className="illustration-main">
                    <IconTrendUp />
                </div>
                <h2 className="empty-state__title">
                    {usingFilters
                        ? searchString
                            ? title.replace('$CONDITION', `matching "${searchString}"`)
                            : title.replace('$CONDITION', `matching these filters`)
                        : title.replace('$CONDITION', 'for this project')}
                </h2>
                <p className="empty-state__description">{description}</p>
                {tab !== SavedInsightsTabs.Favorites && (
                    <Button
                        size="large"
                        type="primary"
                        onClick={() => addGraph('Trends')} // Add trends graph by default
                        data-attr="add-insight-button-empty-state"
                        icon={<PlusCircleOutlined />}
                        className="add-insight-button"
                    >
                        New Insight
                    </Button>
                )}
            </div>
        </div>
    )
}
Example #4
Source File: NumberValidatorInput.tsx    From next-basics with GNU General Public License v3.0 5 votes vote down vote up
export function NumberValidatorInput(
  props: NumberValidatorInputProps
): React.ReactElement {
  const { t } = useTranslation(NS_FLOW_BUILDER);

  const handleChange = (
    value: string | number,
    field: "method" | "value",
    index: number
  ): void => {
    const newValue = update(props.value, {
      $splice: [[index, 1, { ...props.value[index], [field]: value }]],
    });
    props.onChange?.(newValue);
  };

  return (
    <div>
      {props.value?.map((item, index) => (
        <div key={index} className={styles.wrapper}>
          <Input.Group compact style={{ flex: 1 }}>
            <Select
              value={item.method}
              style={{ width: 100 }}
              placeholder={t(K.COMPARE_METHOD_PLACEHOLDER)}
              onChange={(value) => handleChange(value, "method", index)}
            >
              {compareMethodList.map((name) => (
                <Select.Option key={name} value={name}>
                  {name}
                </Select.Option>
              ))}
            </Select>
            <InputNumber
              value={item.value}
              style={{ width: "calc(100% - 100px)" }}
              min={0}
              step={1}
              placeholder={t(K.COMPARE_VALUE_PLACEHOLDER)}
              onChange={(value) =>
                handleChange(value as number, "value", index)
              }
            />
          </Input.Group>
          <Button
            className={editorStyles.iconBtn}
            type="link"
            style={{ flexBasis: 30 }}
            onClick={() => props.onRemove(index)}
          >
            <MinusOutlined />
          </Button>
        </div>
      ))}
      <Button
        className={editorStyles.iconBtn}
        type="link"
        onClick={props.onAdd}
      >
        <PlusCircleOutlined />
      </Button>
    </div>
  );
}
Example #5
Source File: toolbar-config.tsx    From XFlow with MIT License 5 votes vote down vote up
IconStore.set('PlusCircleOutlined', PlusCircleOutlined)
Example #6
Source File: Col.tsx    From ant-extensions with MIT License 5 votes vote down vote up
Col: React.FC<IColConfig> = React.memo((item) => {
  const { t } = useTranslation(I18nKey);
  const { isEditing, updateConfig, addWidget } = useContext(Context);
  const { id, children, colSpan } = item;

  const refEl = useRef<HTMLDivElement>(null);
  const [span, setSpan] = useState(colSpan);

  useLayoutEffect(() => {
    setSpan(span);
  }, [span]);

  const onResize = (evt: MouseEvent) => {
    const newX = evt.clientX;
    if (refEl.current && refEl.current.parentElement) {
      const box = refEl.current.getBoundingClientRect();
      const minWidth = Math.round(refEl.current.parentElement.offsetWidth / 12);
      let newSpan = Math.floor((newX - (box.left - minWidth)) / minWidth) || 1;
      if (newSpan > 12) newSpan = 12;
      setSpan(newSpan as AnyObject);
    }
  };

  const onResizeEnd = () => {
    if (refEl.current) {
      updateConfig(id, "span", colSpan);
    }
    document.body.style.cursor = "unset";
    document.removeEventListener("mousemove", onResize);
    document.removeEventListener("mouseup", onResizeEnd);
  };

  const onResizeStart = (e: React.MouseEvent) => {
    e.preventDefault();
    document.body.style.cursor = "col-resize";
    document.addEventListener("mousemove", onResize);
    document.addEventListener("mouseup", onResizeEnd);
  };

  const isStretched = useMemo(() => {
    return children && children.length > 0 && children[0].type === EnumTypes.TILE;
  }, [children]);

  return (
    <Item
      item={item}
      itemRef={refEl}
      style={{ gridColumnEnd: `span ${span}`, gridAutoRows: isStretched ? "auto" : "max-content" }}
    >
      {Array.isArray(children) &&
        (children as AnyObject).map((item: AnyObject) => {
          switch (item.type) {
            case EnumTypes.HEADING:
              return <Heading key={item.id} {...item} />;
            case EnumTypes.DIVIDER:
              return <Divider key={item.id} {...item} />;
            case EnumTypes.ROW:
              return <Row key={item.id} {...item} />;
            case EnumTypes.TILE:
              return <Tile key={item.id} {...item} />;
            default:
              return null;
          }
        })}
      {isEditing && (!children || children.length === 0) && (
        <div className="ant-ext-pm__emptyCol">
          <div>{t("label.drag")}</div>
          <AntDivider>or</AntDivider>
          <Button
            icon={<PlusCircleOutlined />}
            onClick={(e) => [addWidget(item.id), e.stopPropagation()]}
          >
            Add New Widget
          </Button>
        </div>
      )}
      {isEditing && <div className="ant-ext-pm__resizer" onMouseDown={onResizeStart} />}
    </Item>
  );
})
Example #7
Source File: index.tsx    From fe-v5 with Apache License 2.0 5 votes vote down vote up
export default function index({ targets }) {
  const namePrefix = ['overrides'];

  return (
    <Form.List name={namePrefix}>
      {(fields, { add, remove }) => (
        <>
          {fields.map(({ key, name, ...restField }) => {
            return (
              <Panel
                isInner
                header='override'
                extra={
                  <Space>
                    <PlusCircleOutlined
                      onClick={() => {
                        add({
                          type: 'special',
                        });
                      }}
                    />
                    {fields.length > 1 && (
                      <MinusCircleOutlined
                        onClick={() => {
                          remove(name);
                        }}
                      />
                    )}
                  </Space>
                }
              >
                <Form.Item label='查询条件名称' {...restField} name={[name, 'matcher', 'value']}>
                  <Select suffixIcon={<CaretDownOutlined />} allowClear>
                    {_.map(targets, (target) => {
                      return (
                        <Select.Option key={target.refId} value={target.refId}>
                          {target.refId}
                        </Select.Option>
                      );
                    })}
                  </Select>
                </Form.Item>
                <ValueMappings preNamePrefix={namePrefix} namePrefix={[name, 'properties', 'valueMappings']} />
                <StandardOptions preNamePrefix={namePrefix} namePrefix={[name, 'properties', 'standardOptions']} />
              </Panel>
            );
          })}
        </>
      )}
    </Form.List>
  );
}
Example #8
Source File: index.tsx    From visual-layout with MIT License 5 votes vote down vote up
Home: React.FC<{}> = () => {
  const { appService } = useContext(AppContext);

  const projects: ProjectObject[] = [];
  appService.projects.forEach(project => projects.unshift(project));

  return (
    <Visible>
      {({ visible, setVisible }) => (
        <>
          <Drawer
            title="项目 Home"
            placement="top"
            height={'calc(100% - 40px)'}
            mask={false}
            onClose={() => setVisible(false)}
            visible={visible}
          >
            <Row gutter={[20, 20]}>
              <Col className={styles.newBuild} span={4}>
                <div
                  onClick={() => {
                    appService.new();
                    setVisible(false);
                  }}
                >
                  <span>
                    <PlusCircleOutlined />
                  </span>
                  <span>新建项目</span>
                </div>
              </Col>
              {projects.map(project => (
                <Col className={styles.projects} span={4} key={project.id}>
                  <Project
                    project={project}
                    appService={appService}
                    setVisible={setVisible}
                  />
                </Col>
              ))}
            </Row>
          </Drawer>
          <div
            onClick={() => setVisible(!visible)}
            style={{ height: '100%', width: '100%', padding: '5px 10px' }}
          >
            项目
            <FolderAddOutlined
              style={{ color: 'white', fontSize: 16, marginLeft: 5 }}
            />
          </div>
        </>
      )}
    </Visible>
  );
}
Example #9
Source File: PathCleanFilterInput.tsx    From posthog-foss with MIT License 5 votes vote down vote up
export function PathCleanFilterInput(): JSX.Element {
    const [open, setOpen] = useState(false)
    const { insightProps } = useValues(insightLogic)
    const { filter } = useValues(pathsLogic(insightProps))
    const { setFilter } = useActions(pathsLogic(insightProps))

    return (
        <>
            <PathCleanFilters
                style={{ paddingLeft: 10 }}
                pageKey="pathcleanfilters-local"
                pathCleaningFilters={filter.local_path_cleaning_filters || []}
                onChange={(newItem) => {
                    setFilter({
                        local_path_cleaning_filters: [...(filter.local_path_cleaning_filters || []), newItem],
                    })
                }}
                onRemove={(index) => {
                    const newState = (filter.local_path_cleaning_filters || []).filter((_, i) => i !== index)
                    setFilter({ local_path_cleaning_filters: newState })
                }}
            />
            <Row align="middle" justify="space-between" style={{ paddingLeft: 0 }}>
                <Popup
                    visible={open}
                    placement={'bottom-end'}
                    fallbackPlacements={['bottom-start']}
                    onClickOutside={() => setOpen(false)}
                    overlay={
                        <PathRegexPopup
                            item={{}}
                            onClose={() => setOpen(false)}
                            onComplete={(newItem) => {
                                setFilter({
                                    local_path_cleaning_filters: [
                                        ...(filter.local_path_cleaning_filters || []),
                                        newItem,
                                    ],
                                })
                                setOpen(false)
                            }}
                        />
                    }
                >
                    {({ setRef }) => {
                        return (
                            <>
                                <Button
                                    ref={setRef}
                                    onClick={() => setOpen(!open)}
                                    className="new-prop-filter"
                                    data-attr={'new-prop-filter-' + 'pathcleanfilters-local'}
                                    type="link"
                                    style={{ paddingLeft: 0 }}
                                    icon={<PlusCircleOutlined />}
                                >
                                    {'Add Rule'}
                                </Button>
                            </>
                        )
                    }}
                </Popup>
                <PathCleanFilterToggle filters={filter} onChange={setFilter} />
            </Row>
        </>
    )
}
Example #10
Source File: EmptyStates.tsx    From posthog-foss with MIT License 5 votes vote down vote up
export function FunnelSingleStepState(): JSX.Element {
    const { insightProps } = useValues(insightLogic)
    const { filters, clickhouseFeaturesEnabled } = useValues(funnelLogic(insightProps))
    const { setFilters } = useActions(funnelLogic(insightProps))
    const { addFilter } = useActions(entityFilterLogic({ setFilters, filters, typeKey: 'EditFunnel-action' }))

    return (
        <div className="insight-empty-state funnels-empty-state">
            <div className="empty-state-inner">
                <div className="illustration-main">
                    <PlusCircleOutlined />
                </div>
                <h2 className="funnels-empty-state__title">Add another step!</h2>
                <p className="funnels-empty-state__description">
                    You’re almost there! Funnels require at least two steps before calculating.
                    {clickhouseFeaturesEnabled
                        ? ' Once you have two steps defined, additional changes will recalculate automatically.'
                        : ''}
                </p>
                <div className="mt text-center">
                    <Button
                        size="large"
                        onClick={() => addFilter()}
                        data-attr="add-action-event-button-empty-state"
                        icon={<PlusCircleOutlined />}
                        className="add-action-event-button"
                    >
                        Add funnel step
                    </Button>
                </div>
                <div className="mt text-center">
                    <a
                        data-attr="funnels-single-step-help"
                        href="https://posthog.com/docs/user-guides/funnels?utm_medium=in-product&utm_campaign=funnel-empty-state"
                        target="_blank"
                        rel="noopener"
                        className="flex-center"
                        style={{ justifyContent: 'center' }}
                    >
                        Learn more about funnels in our support documentation
                        <IconOpenInNew style={{ marginLeft: 4, fontSize: '0.85em' }} />
                    </a>
                </div>
            </div>
        </div>
    )
}
Example #11
Source File: TaxonomicBreakdownButton.tsx    From posthog-foss with MIT License 5 votes vote down vote up
export function TaxonomicBreakdownButton({
    breakdownType,
    onChange,
    onlyCohorts,
    buttonType = 'link',
}: TaxonomicBreakdownButtonProps): JSX.Element {
    const [open, setOpen] = useState(false)
    const { allEventNames } = useValues(insightLogic)
    const { groupsTaxonomicTypes } = useValues(groupsModel)

    return (
        <Popup
            overlay={
                <TaxonomicFilter
                    groupType={breakdownType}
                    onChange={(taxonomicGroup, value) => {
                        if (value) {
                            onChange(value, taxonomicGroup)
                            setOpen(false)
                        }
                    }}
                    eventNames={allEventNames}
                    taxonomicGroupTypes={
                        onlyCohorts
                            ? [TaxonomicFilterGroupType.CohortsWithAllUsers]
                            : [
                                  TaxonomicFilterGroupType.EventProperties,
                                  TaxonomicFilterGroupType.PersonProperties,
                                  ...groupsTaxonomicTypes,
                                  TaxonomicFilterGroupType.CohortsWithAllUsers,
                              ]
                    }
                />
            }
            placement={'bottom-start'}
            fallbackPlacements={['bottom-end']}
            visible={open}
            onClickOutside={() => setOpen(false)}
        >
            {({ setRef }) => (
                <Button
                    type={buttonType}
                    icon={<PlusCircleOutlined />}
                    data-attr="add-breakdown-button"
                    onClick={() => setOpen(!open)}
                    className="taxonomic-breakdown-filter tag-button"
                    ref={setRef}
                >
                    <PropertyKeyInfo
                        value={
                            breakdownType === TaxonomicFilterGroupType.CohortsWithAllUsers
                                ? 'Add cohort'
                                : 'Add breakdown'
                        }
                    />
                </Button>
            )}
        </Popup>
    )
}
Example #12
Source File: Comparison.tsx    From fe-v5 with Apache License 2.0 5 votes vote down vote up
render() {
    const { curComparison } = this.state;
    const handleClick = (e) => {
      const index = this.state.curComparison.findIndex(cc => cc === e.key)
      let newCurComparison
      if (index === -1) {
        newCurComparison = [
          ...this.state.curComparison,
          e.key
        ]
        this.setState({
          curComparison: newCurComparison
        })
      } else {
        let curComparisonCopy = [...this.state.curComparison]
        curComparisonCopy.splice(index, 1)
        newCurComparison = curComparisonCopy
        this.setState({
          curComparison: curComparisonCopy
        })
      }
      const { onChange, relativeTimeComparison, comparisonOptions } = this.props;
      onChange({
        ...this.refresh(),
        comparison: newCurComparison,
        relativeTimeComparison,
        comparisonOptions,
      });
    }
    const menu = (
      <Menu onClick={handleClick} selectedKeys={curComparison}>
        <Menu.Item key='1d'>1d</Menu.Item>
        <Menu.Item key='7d'>7d</Menu.Item>
      </Menu>
    )
    return (
      <div className="graph-config-inner-comparison">
        {/* <Select
          dropdownMatchSelectWidth={false}
          mode="multiple"
          style={{ minWidth: 80, width: 'auto', verticalAlign: 'middle' }}
          value={curComparison}
          onChange={this.handleComparisonChange}>
          <Option key={'1d'} value={'1d'}>1天</Option>
          <Option key={'7d'} value={'7d'}>7天</Option>
        </Select> */}
        {this.state.curComparison.map(cc =>
          <Tag key={cc} closable onClose={e => {
            handleClick({key: cc})
          }}>
            {cc}
          </Tag>
        )}
        <Dropdown overlay={menu}>
          <a className="ant-dropdown-link" onClick={e => e.preventDefault()}>
            <PlusCircleOutlined />
          </a>
        </Dropdown>
      </div>
    );
  }
Example #13
Source File: PathItemFilters.tsx    From posthog-foss with MIT License 5 votes vote down vote up
export function PathItemFilters({
    propertyFilters = null,
    onChange = null,
    pageKey,
    style = {},
    taxonomicGroupTypes,
    wildcardOptions,
}: PropertyFiltersProps): JSX.Element {
    const logicProps = { propertyFilters, onChange, pageKey, urlOverride: 'exclude_events' }
    const { filters } = useValues(propertyFilterLogic(logicProps))
    const { setFilter, remove, setFilters } = useActions(propertyFilterLogic(logicProps))

    useEffect(() => {
        if (propertyFilters && !objectsEqual(propertyFilters, filters)) {
            setFilters([...propertyFilters, {}])
        }
    }, [propertyFilters])

    return (
        <div className="mb" style={style}>
            <BindLogic logic={propertyFilterLogic} props={logicProps}>
                {filters?.length &&
                    filters.map((filter, index) => {
                        return (
                            <div key={index} style={{ margin: '0.25rem 0', padding: '0.25rem 0' }}>
                                <PathItemSelector
                                    pathItem={filter.value as string | undefined}
                                    onChange={(pathItem) => setFilter(index, pathItem, pathItem, null, 'event')}
                                    index={index}
                                    taxonomicGroupTypes={taxonomicGroupTypes}
                                    wildcardOptions={wildcardOptions}
                                >
                                    {!filter.value ? (
                                        <Button
                                            className="new-prop-filter"
                                            data-attr={'new-prop-filter-' + pageKey}
                                            type="link"
                                            style={{ paddingLeft: 0 }}
                                            icon={<PlusCircleOutlined />}
                                        >
                                            Add exclusion
                                        </Button>
                                    ) : (
                                        <Row align="middle">
                                            <FilterButton>{filter.value as string}</FilterButton>
                                            {!!Object.keys(filters[index]).length && (
                                                <CloseButton
                                                    onClick={(e: Event) => {
                                                        remove(index)
                                                        e.stopPropagation()
                                                    }}
                                                    style={{
                                                        cursor: 'pointer',
                                                        float: 'none',
                                                        paddingLeft: 8,
                                                        alignSelf: 'center',
                                                    }}
                                                />
                                            )}
                                        </Row>
                                    )}
                                </PathItemSelector>
                            </div>
                        )
                    })}
            </BindLogic>
        </div>
    )
}
Example #14
Source File: MainMenu.tsx    From mayoor with MIT License 5 votes vote down vote up
MainMenu: React.FC = () => {
	const { t } = useTranslation();
	const { currentUser } = useAppState();
	return (
		<StyledMenu>
			<li>
				<CategoryName>{t('Orders')}</CategoryName>
				<LinkItem icon={<PlusCircleOutlined />} name={t('Add order')} to={'/orders/new'} />
				<LinkItem
					icon={<FileSearchOutlined />}
					name={t('List orders')}
					to={'/orders/list'}
				/>
				<LinkItem
					icon={<BgColorsOutlined />}
					name={t('To be printed')}
					to={'/orders/print'}
				/>
				<LinkItem
					icon={<HighlightOutlined />}
					name={t('Waiting for production')}
					to={'/orders/production'}
				/>
			</li>
			<li>
				<CategoryName>{t('Customers')}</CategoryName>
				<LinkItem
					icon={<UserAddOutlined />}
					name={t('Add customer')}
					to={'/customers/new'}
				/>
				<LinkItem icon={<TeamOutlined />} name={t('Customers')} to={'/customers/list'} />
			</li>
			{currentUser?.role === UserRole.EXECUTIVE && (
				<li>
					<CategoryName>{t('Administration')}</CategoryName>
					<LinkItem icon={<FileTextOutlined />} name={t('Material')} to={'/materials'} />
					<LinkItem icon={<TeamOutlined />} name={t('Users')} to={'/users'} />
				</li>
			)}
		</StyledMenu>
	);
}
Example #15
Source File: index.tsx    From fe-v5 with Apache License 2.0 5 votes vote down vote up
export default function index({ targets }) {
  const namePrefix = ['overrides'];

  return (
    <Form.List name={namePrefix}>
      {(fields, { add, remove }) =>
        fields.map(({ key, name, ...restField }) => {
          return (
            <Panel
              key={key}
              isInner
              header='override'
              extra={
                <Space>
                  <PlusCircleOutlined
                    onClick={() => {
                      add({
                        type: 'special',
                      });
                    }}
                  />
                  {fields.length > 1 && (
                    <MinusCircleOutlined
                      onClick={() => {
                        remove(name);
                      }}
                    />
                  )}
                </Space>
              }
            >
              <Form.Item label='查询条件名称' {...restField} name={[name, 'matcher', 'value']}>
                <Select suffixIcon={<CaretDownOutlined />} allowClear>
                  {_.map(targets, (target) => {
                    return (
                      <Select.Option key={target.refId} value={target.refId}>
                        {target.refId}
                      </Select.Option>
                    );
                  })}
                </Select>
              </Form.Item>
              <ValueMappings preNamePrefix={namePrefix} namePrefix={[name, 'properties', 'valueMappings']} />
              <StandardOptions preNamePrefix={namePrefix} namePrefix={[name, 'properties', 'standardOptions']} />
            </Panel>
          );
        })
      }
    </Form.List>
  );
}
Example #16
Source File: editModal.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
editModal: React.FC<Props> = ({ isModalVisible, editModalFinish }) => {
  const { t, i18n } = useTranslation();

  const [form] = Form.useForm();
  const { clusters: clusterList } = useSelector<RootState, CommonStoreState>((state) => state.common);

  const [contactList, setInitContactList] = useState([]);
  const [notifyGroups, setNotifyGroups] = useState([]);

  const [field, setField] = useState<string>('cluster');
  const [refresh, setRefresh] = useState(true);

  useEffect(() => {
    getNotifyChannel();
    getGroups('');

    return () => {};
  }, []);

  const enableDaysOfWeekOptions = [t('周日'), t('周一'), t('周二'), t('周三'), t('周四'), t('周五'), t('周六')].map((v, i) => {
    return <Option value={String(i)} key={i}>{`${v}`}</Option>;
  });

  const contactListCheckboxes = contactList.map((c: { key: string; label: string }) => (
    <Checkbox value={c.key} key={c.label}>
      {c.label}
    </Checkbox>
  ));

  const notifyGroupsOptions = notifyGroups.map((ng: any) => (
    <Option value={ng.id} key={ng.id}>
      {ng.name}
    </Option>
  ));

  const getNotifyChannel = async () => {
    const res = await getNotifiesList();
    let contactList = res || [];
    setInitContactList(contactList);
  };

  const getGroups = async (str) => {
    const res = await getTeamInfoList({ query: str });
    const data = res.dat || res;
    setNotifyGroups(data || []);
  };

  const debounceFetcher = useCallback(debounce(getGroups, 800), []);

  const modelOk = () => {
    form.validateFields().then(async (values) => {
      const data = { ...values };
      switch (values.field) {
        case 'enable_time':
          data.enable_stime = values.enable_time[0].format('HH:mm');
          data.enable_etime = values.enable_time[1].format('HH:mm');
          delete data.enable_time;
          break;
        case 'disabled':
          data.disabled = !values.enable_status ? 1 : 0;
          delete data.enable_status;
          break;
        case 'enable_in_bg':
          data.enable_in_bg = values.enable_in_bg ? 1 : 0;
          break;
        case 'callbacks':
          data.callbacks = values.callbacks.map((item) => item.url);
          break;
        case 'notify_recovered':
          data.notify_recovered = values.notify_recovered ? 1 : 0;
          break;
        default:
          break;
      }
      delete data.field;
      Object.keys(data).forEach((key) => {
        // 因为功能上有清除备注的需求,需要支持传空
        if (data[key] === undefined) {
          data[key] = '';
        }
        if (Array.isArray(data[key])) {
          data[key] = data[key].join(' ');
        }
      });
      editModalFinish(true, data);
    });
  };

  const editModalClose = () => {
    editModalFinish(false);
  };

  const fieldChange = (val) => {
    setField(val);
  };

  return (
    <>
      <Modal
        title={t('批量更新')}
        visible={isModalVisible}
        onOk={modelOk}
        onCancel={() => {
          editModalClose();
        }}
      >
        <Form
          {...layout}
          form={form}
          className='strategy-form'
          layout={refresh ? 'horizontal' : 'horizontal'}
          initialValues={{
            prom_eval_interval: 15,
            disabled: 0, // 0:立即启用 1:禁用
            enable_status: true, // true:立即启用 false:禁用
            notify_recovered: 1, // 1:启用
            enable_time: [moment('00:00', 'HH:mm'), moment('23:59', 'HH:mm')],
            cluster: clusterList[0] || 'Default', // 生效集群
            enable_days_of_week: ['1', '2', '3', '4', '5', '6', '0'],
            field: 'cluster',
          }}
        >
          <Form.Item
            label={t('字段:')}
            name='field'
            rules={[
              {
                required: false,
              },
            ]}
          >
            <Select suffixIcon={<CaretDownOutlined />} style={{ width: '100%' }} onChange={fieldChange}>
              {fields.map((item) => (
                <Option key={item.id} value={item.field}>
                  {item.name}
                </Option>
              ))}
            </Select>
          </Form.Item>
          {(() => {
            switch (field) {
              case 'note':
                return (
                  <>
                    <Form.Item
                      label={t('改为:')}
                      name='note'
                      rules={[
                        {
                          required: false,
                        },
                      ]}
                    >
                      <Input placeholder={t('请输入规则备注')} />
                    </Form.Item>
                  </>
                );
              case 'runbook_url':
                return (
                  <>
                    <Form.Item label={t('改为:')} name='runbook_url'>
                      <Input />
                    </Form.Item>
                  </>
                );

              case 'cluster':
                return (
                  <>
                    <Form.Item
                      label={t('改为:')}
                      name='cluster'
                      rules={[
                        {
                          required: false,
                          message: t('生效集群不能为空'),
                        },
                      ]}
                    >
                      <Select suffixIcon={<CaretDownOutlined />}>
                        {clusterList?.map((item) => (
                          <Option value={item} key={item}>
                            {item}
                          </Option>
                        ))}
                      </Select>
                    </Form.Item>
                  </>
                );
              case 'severity':
                return (
                  <>
                    <Form.Item
                      label={t('改为:')}
                      name='severity'
                      initialValue={2}
                      rules={[
                        {
                          required: false,
                          message: t('告警级别不能为空'),
                        },
                      ]}
                    >
                      <Radio.Group>
                        <Radio value={1}>{t('一级报警')}</Radio>
                        <Radio value={2}>{t('二级报警')}</Radio>
                        <Radio value={3}>{t('三级报警')}</Radio>
                      </Radio.Group>
                    </Form.Item>
                  </>
                );
              case 'disabled':
                return (
                  <>
                    <Form.Item
                      label={t('改为:')}
                      name='enable_status'
                      rules={[
                        {
                          required: false,
                          message: t('立即启用不能为空'),
                        },
                      ]}
                      valuePropName='checked'
                    >
                      <Switch />
                    </Form.Item>
                  </>
                );
              case 'enable_in_bg':
                return (
                  <>
                    <Form.Item label={t('改为:')} name='enable_in_bg' valuePropName='checked'>
                      <SwitchWithLabel label='根据告警事件中的ident归属关系判断' />
                    </Form.Item>
                  </>
                );
              case 'prom_eval_interval':
                return (
                  <>
                    <Form.Item
                      label={t('改为:')}
                      rules={[
                        {
                          required: false,
                          message: t('执行频率不能为空'),
                        },
                      ]}
                    >
                      <Space>
                        <Form.Item style={{ marginBottom: 0 }} name='prom_eval_interval' initialValue={15} wrapperCol={{ span: 10 }}>
                          <InputNumber
                            min={1}
                            onChange={(val) => {
                              setRefresh(!refresh);
                            }}
                          />
                        </Form.Item>
                        秒
                        <Tooltip title={t(`每隔${form.getFieldValue('prom_eval_interval')}秒,把PromQL作为查询条件,去查询后端存储,如果查到了数据就表示当次有监控数据触发了规则`)}>
                          <QuestionCircleFilled />
                        </Tooltip>
                      </Space>
                    </Form.Item>
                  </>
                );
              case 'prom_for_duration':
                return (
                  <>
                    <Form.Item
                      label={t('改为:')}
                      rules={[
                        {
                          required: false,
                          message: t('持续时长不能为空'),
                        },
                      ]}
                    >
                      <Space>
                        <Form.Item style={{ marginBottom: 0 }} name='prom_for_duration' initialValue={60} wrapperCol={{ span: 10 }}>
                          <InputNumber min={0} />
                        </Form.Item>
                        秒
                        <Tooltip
                          title={t(
                            `通常持续时长大于执行频率,在持续时长内按照执行频率多次执行PromQL查询,每次都触发才生成告警;如果持续时长置为0,表示只要有一次PromQL查询触发阈值,就生成告警`,
                          )}
                        >
                          <QuestionCircleFilled />
                        </Tooltip>
                      </Space>
                    </Form.Item>
                  </>
                );
              case 'notify_channels':
                return (
                  <>
                    <Form.Item label={t('改为:')} name='notify_channels'>
                      <Checkbox.Group>{contactListCheckboxes}</Checkbox.Group>
                    </Form.Item>
                  </>
                );
              case 'notify_groups':
                return (
                  <>
                    <Form.Item label={t('改为:')} name='notify_groups'>
                      <Select mode='multiple' showSearch optionFilterProp='children' filterOption={false} onSearch={(e) => debounceFetcher(e)} onBlur={() => getGroups('')}>
                        {notifyGroupsOptions}
                      </Select>
                    </Form.Item>
                  </>
                );
              case 'notify_recovered':
                return (
                  <>
                    <Form.Item label={t('改为:')} name='notify_recovered' valuePropName='checked'>
                      <Switch />
                    </Form.Item>
                  </>
                );
              case 'recover_duration':
                return (
                  <>
                    <Form.Item label={t('改为:')}>
                      <Space>
                        <Form.Item
                          style={{ marginBottom: 0 }}
                          name='recover_duration'
                          initialValue={0}
                          wrapperCol={{ span: 10 }}
                          rules={[
                            {
                              required: false,
                              message: t('留观时长不能为空'),
                            },
                          ]}
                        >
                          <InputNumber
                            min={0}
                            onChange={(val) => {
                              setRefresh(!refresh);
                            }}
                          />
                        </Form.Item>
                        秒
                        <Tooltip title={t(`持续${form.getFieldValue('recover_duration') || 0}秒没有再次触发阈值才发送恢复通知`)}>
                          <QuestionCircleFilled />
                        </Tooltip>
                      </Space>
                    </Form.Item>
                  </>
                );
              case 'notify_repeat_step':
                return (
                  <>
                    <Form.Item label={t('改为:')}>
                      <Space>
                        <Form.Item
                          style={{ marginBottom: 0 }}
                          name='notify_repeat_step'
                          initialValue={60}
                          wrapperCol={{ span: 10 }}
                          rules={[
                            {
                              required: false,
                              message: t('重复发送频率不能为空'),
                            },
                          ]}
                        >
                          <InputNumber
                            min={0}
                            onChange={(val) => {
                              setRefresh(!refresh);
                            }}
                          />
                        </Form.Item>
                        分钟
                        <Tooltip title={t(`如果告警持续未恢复,间隔${form.getFieldValue('notify_repeat_step')}分钟之后重复提醒告警接收组的成员`)}>
                          <QuestionCircleFilled />
                        </Tooltip>
                      </Space>
                    </Form.Item>
                  </>
                );
              case 'callbacks':
                return (
                  <>
                    <Form.Item label={t('改为:')}>
                      <Form.List name='callbacks' initialValue={[{}]}>
                        {(fields, { add, remove }, { errors }) => (
                          <>
                            {fields.map((field, index) => (
                              <Row gutter={[10, 0]} key={field.key}>
                                <Col span={22}>
                                  <Form.Item name={[field.name, 'url']}>
                                    <Input />
                                  </Form.Item>
                                </Col>

                                <Col span={1}>
                                  <MinusCircleOutlined className='control-icon-normal' onClick={() => remove(field.name)} />
                                </Col>
                              </Row>
                            ))}
                            <PlusCircleOutlined className='control-icon-normal' onClick={() => add()} />
                          </>
                        )}
                      </Form.List>
                    </Form.Item>
                  </>
                );
              case 'append_tags':
                return (
                  <>
                    <Form.Item label='附加标签' name='append_tags' rules={[{ required: false, message: '请填写至少一项标签!' }, isValidFormat]}>
                      <Select mode='tags' tokenSeparators={[' ']} open={false} placeholder={'标签格式为 key=value ,使用回车或空格分隔'} tagRender={tagRender} />
                    </Form.Item>
                  </>
                );
              case 'enable_time':
                return (
                  <>
                    <Form.Item
                      label={t('改为:')}
                      name='enable_days_of_week'
                      rules={[
                        {
                          required: false,
                          message: t('生效时间不能为空'),
                        },
                      ]}
                    >
                      <Select mode='tags'>{enableDaysOfWeekOptions}</Select>
                    </Form.Item>
                    <Form.Item
                      name='enable_time'
                      {...tailLayout}
                      rules={[
                        {
                          required: false,
                          message: t('生效时间不能为空'),
                        },
                      ]}
                    >
                      <TimePicker.RangePicker
                        format='HH:mm'
                        onChange={(val, val2) => {
                          form.setFieldsValue({
                            enable_stime: val2[0],
                            enable_etime: val2[1],
                          });
                        }}
                      />
                    </Form.Item>
                  </>
                );
              default:
                return null;
            }
          })()}
        </Form>
      </Modal>
    </>
  );
}
Example #17
Source File: Graph.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
export default function Graph(props: IProps) {
  const { metric, match, range, step, onClose } = props;
  const newGroups = _.map(
    _.filter(match.dimensionLabels, (item) => !_.isEmpty(item.value)),
    'label',
  );
  const [refreshFlag, setRefreshFlag] = useState(_.uniqueId('refreshFlag_'));
  const [calcFunc, setCalcFunc] = useState('');
  const [comparison, setComparison] = useState<string[]>([]);
  const [aggrFunc, setAggrFunc] = useState('avg');
  const [aggrGroups, setAggrGroups] = useState<string[]>(newGroups);
  const [labels, setLabels] = useState<string[]>([]);
  const [series, setSeries] = useState<any[]>([]);
  const [highLevelConfig, setHighLevelConfig] = useState({
    shared: true,
    sharedSortDirection: 'desc',
    legend: true,
    util: 'none',
    colorRange: colors[0].value,
    reverseColorOrder: false,
    colorDomainAuto: true,
    colorDomain: [],
    chartheight: 300,
  });
  const [chartType, setChartType] = useState('line');
  const [reduceFunc, setReduceFunc] = useState('last');
  const lineGraphProps = {
    custom: {
      drawStyle: 'lines',
      fillOpacity: 0,
      stack: 'hidden',
      lineInterpolation: 'smooth',
    },
    options: {
      legend: {
        displayMode: highLevelConfig.legend ? 'list' : 'hidden',
      },
      tooltip: {
        mode: highLevelConfig.shared ? 'all' : 'single',
        sort: highLevelConfig.sharedSortDirection,
      },
      standardOptions: {
        util: highLevelConfig.util,
      },
    },
  };
  const hexbinGraphProps = {
    custom: {
      calc: reduceFunc,
      colorRange: highLevelConfig.colorRange,
      reverseColorOrder: highLevelConfig.reverseColorOrder,
      colorDomainAuto: highLevelConfig.colorDomainAuto,
      colorDomain: highLevelConfig.colorDomain,
    },
    options: {
      standardOptions: {
        util: highLevelConfig.util,
      },
    },
  };
  const graphStandardOptions = {
    line: <LineGraphStandardOptions highLevelConfig={highLevelConfig} setHighLevelConfig={setHighLevelConfig} />,
    hexbin: <HexbinGraphStandardOptions highLevelConfig={highLevelConfig} setHighLevelConfig={setHighLevelConfig} />,
  };

  useEffect(() => {
    setAggrGroups(newGroups);
  }, [JSON.stringify(newGroups)]);

  useEffect(() => {
    const matchStr = getMatchStr(match);
    getLabels(`${metric}${matchStr}`, range).then((res) => {
      setLabels(res);
    });
  }, [refreshFlag, JSON.stringify(match), JSON.stringify(range)]);

  useEffect(() => {
    getQueryRange({
      metric,
      match: getMatchStr(match),
      range,
      step,
      aggrFunc,
      aggrGroups,
      calcFunc,
      comparison,
    }).then((res) => {
      setSeries(res);
    });
  }, [refreshFlag, metric, JSON.stringify(match), JSON.stringify(range), step, calcFunc, comparison, aggrFunc, aggrGroups]);

  return (
    <Card
      size='small'
      style={{ marginBottom: 10 }}
      title={metric}
      className='n9e-metric-views-metrics-graph'
      extra={
        <Space>
          <Space size={0} style={{ marginRight: 10 }}>
            <LineChartOutlined
              className={classNames({
                'button-link-icon': true,
                active: chartType === 'line',
              })}
              onClick={() => {
                setChartType('line');
              }}
            />
            <Divider type='vertical' />
            <HexbinIcon
              className={classNames({
                'button-link-icon': true,
                active: chartType === 'hexbin',
              })}
              onClick={() => {
                setChartType('hexbin');
              }}
            />
          </Space>
          <Popover placement='left' content={graphStandardOptions[chartType]} trigger='click' autoAdjustOverflow={false} getPopupContainer={() => document.body}>
            <a>
              <SettingOutlined />
            </a>
          </Popover>
          <a>
            <SyncOutlined
              onClick={() => {
                setRefreshFlag(_.uniqueId('refreshFlag_'));
              }}
            />
          </a>
          <a>
            <ShareAltOutlined
              onClick={() => {
                const curCluster = localStorage.getItem('curCluster');
                const dataProps = {
                  type: 'timeseries',
                  version: '2.0.0',
                  name: metric,
                  step,
                  range,
                  ...lineGraphProps,
                  targets: _.map(
                    getExprs({
                      metric,
                      match: getMatchStr(match),
                      aggrFunc,
                      aggrGroups,
                      calcFunc,
                      comparison,
                    }),
                    (expr) => {
                      return {
                        expr,
                      };
                    },
                  ),
                };
                setTmpChartData([
                  {
                    configs: JSON.stringify({
                      curCluster,
                      dataProps,
                    }),
                  },
                ]).then((res) => {
                  const ids = res.dat;
                  window.open('/chart/' + ids);
                });
              }}
            />
          </a>
          <a>
            <CloseCircleOutlined onClick={onClose} />
          </a>
        </Space>
      }
    >
      <div>
        <Space>
          <div>
            计算函数:
            <Dropdown
              overlay={
                <Menu onClick={(e) => setCalcFunc(e.key === 'clear' ? '' : e.key)} selectedKeys={[calcFunc]}>
                  <Menu.Item key='rate_1m'>rate_1m</Menu.Item>
                  <Menu.Item key='rate_5m'>rate_5m</Menu.Item>
                  <Menu.Item key='increase_1m'>increase_1m</Menu.Item>
                  <Menu.Item key='increase_5m'>increase_5m</Menu.Item>
                  <Menu.Divider></Menu.Divider>
                  <Menu.Item key='clear'>clear</Menu.Item>
                </Menu>
              }
            >
              <a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
                {calcFunc || '无'} <DownOutlined />
              </a>
            </Dropdown>
          </div>
          <div>
            环比:
            {comparison.map((ag) => (
              <Tag
                key={ag}
                closable
                onClose={() => {
                  setComparison(_.without(comparison, ag));
                }}
              >
                {ag}
              </Tag>
            ))}
            <Dropdown
              overlay={
                <Menu
                  onClick={(e) => {
                    if (comparison.indexOf(e.key) === -1) {
                      setComparison([...comparison, e.key]);
                    } else {
                      setComparison(_.without(comparison, e.key));
                    }
                  }}
                  selectedKeys={comparison}
                >
                  <Menu.Item key='1d'>1d</Menu.Item>
                  <Menu.Item key='7d'>7d</Menu.Item>
                </Menu>
              }
              overlayStyle={{ maxHeight: 400, overflow: 'auto' }}
            >
              <a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
                <PlusCircleOutlined />
              </a>
            </Dropdown>
          </div>
          <div>
            聚合函数:
            <Dropdown
              overlay={
                <Menu onClick={(e) => setAggrFunc(e.key)} selectedKeys={[aggrFunc]}>
                  <Menu.Item key='sum'>sum</Menu.Item>
                  <Menu.Item key='avg'>avg</Menu.Item>
                  <Menu.Item key='max'>max</Menu.Item>
                  <Menu.Item key='min'>min</Menu.Item>
                </Menu>
              }
            >
              <a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
                {aggrFunc} <DownOutlined />
              </a>
            </Dropdown>
          </div>
          {aggrFunc ? (
            <div className='graph-config-inner-item'>
              聚合维度:
              {aggrGroups.map((ag) => (
                <Tag
                  key={ag}
                  closable
                  onClose={() => {
                    setAggrGroups(_.without(aggrGroups, ag));
                  }}
                >
                  {ag}
                </Tag>
              ))}
              <Dropdown
                overlay={
                  <Menu
                    onClick={(e) => {
                      if (aggrGroups.indexOf(e.key) === -1) {
                        setAggrGroups([...aggrGroups, e.key]);
                      } else {
                        setAggrGroups(_.without(aggrGroups, e.key));
                      }
                    }}
                    selectedKeys={aggrGroups}
                  >
                    {_.map(
                      _.filter(labels, (n) => n !== '__name__'),
                      (ag) => (
                        <Menu.Item key={ag}>{ag}</Menu.Item>
                      ),
                    )}
                  </Menu>
                }
                overlayStyle={{ maxHeight: 400, overflow: 'auto' }}
              >
                <a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
                  <PlusCircleOutlined />
                </a>
              </Dropdown>
            </div>
          ) : null}
          {chartType === 'hexbin' && (
            <div>
              取值计算:
              <Dropdown
                overlay={
                  <Menu onClick={(e) => setReduceFunc(e.key)} selectedKeys={[reduceFunc]}>
                    {_.map(calcsOptions, (val, key) => {
                      return <Menu.Item key={key}>{val.name}</Menu.Item>;
                    })}
                  </Menu>
                }
              >
                <a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
                  {calcsOptions[reduceFunc]?.name} <DownOutlined />
                </a>
              </Dropdown>
            </div>
          )}
        </Space>
      </div>
      <div>
        {chartType === 'line' && <Timeseries inDashboard={false} values={lineGraphProps as any} series={series} />}
        {chartType === 'hexbin' && (
          <div style={{ padding: '20px 0 0 0', height: highLevelConfig.chartheight }}>
            <Hexbin values={hexbinGraphProps as any} series={series} />
          </div>
        )}
      </div>
    </Card>
  );
}
Example #18
Source File: Form.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
function FormCpt(props: ModalWrapProps & IProps) {
  const { t } = useTranslation();
  const { action, visible, initialValues, destroy, range, onOk, admin } = props;
  const [form] = Form.useForm();
  const [labels, setLabels] = useState<string[]>([]);
  const [filteredLabels, setFilteredLabels] = useState<string[]>([]);
  const [previewVisible, setPreviewVisible] = useState(false);
  const [previewLoading, setPreviewLoading] = useState(false);
  const [previewData, setPreviewData] = useState([]);
  const [activeKey, setActiveKey] = useState('form');
  const getLablesOptions = (_labels) => {
    return _.map(_labels, (label) => {
      return (
        <Select.Option key={label} value={label}>
          {label}
        </Select.Option>
      );
    });
  };

  useEffect(() => {
    getLabels('', range).then((res) => {
      setLabels(res);
      setFilteredLabels(res);
    });
  }, [JSON.stringify(range)]);

  return (
    <Modal
      className='n9e-metric-views-modal'
      title={
        <Tabs className='custom-import-title' activeKey={activeKey} onChange={setActiveKey}>
          <TabPane tab={titleMap[action]} key='form' />
          {action === 'add' && <TabPane tab='导入快捷视图' key='import' />}
        </Tabs>
      }
      visible={visible}
      onCancel={() => {
        destroy();
      }}
      onOk={() => {
        form.validateFields().then((values) => {
          let _values = _.cloneDeep(values);
          if (activeKey === 'form') {
            _values.dynamicLabels = _.map(_values.dynamicLabels, (item) => {
              return {
                label: item,
                value: '',
              };
            });
            _values.dimensionLabels = _.map(_values.dimensionLabels, (item) => {
              return {
                label: item,
                value: '',
              };
            });
          }
          if (activeKey === 'import') {
            try {
              const config = JSON.parse(values.import);
              _values = {
                name: values.name,
                cate: values.cate,
                ...config,
              };
            } catch (e) {
              console.log(e);
              return;
            }
          }
          const { name, cate } = _values;
          const configs = JSON.stringify(_.omit(_values, ['name', 'cate']));
          const data: any = {
            name,
            cate: cate ? 0 : 1,
            configs,
          };
          if (action === 'add') {
            addMetricView(data).then((res) => {
              message.success('添加成功');
              onOk(res);
              destroy();
            });
          } else if (action === 'edit') {
            data.id = initialValues.id;
            updateMetricView(data).then(() => {
              message.success('修改成功');
              onOk();
              destroy();
            });
          }
        });
      }}
    >
      {activeKey === 'form' && (
        <Form
          layout='vertical'
          initialValues={
            initialValues || {
              cate: false,
            }
          }
          form={form}
          onValuesChange={(changedValues, allValues) => {
            if (changedValues.filters) {
              const filtersStr = getFiltersStr(allValues.filters);
              getLabels(`${filtersStr ? `{${filtersStr}}` : ''}`, range).then((res) => {
                setFilteredLabels(res);
              });
            }
          }}
        >
          <Form.Item label='视图名称' name='name' rules={[{ required: true }]}>
            <Input />
          </Form.Item>
          {admin && (
            <Form.Item label='是否公开' name='cate' rules={[{ required: true }]} valuePropName='checked'>
              <Switch />
            </Form.Item>
          )}
          <Form.List name='filters'>
            {(fields, { add, remove }) => (
              <>
                <div style={{ paddingBottom: 8 }}>
                  前置过滤条件{' '}
                  <PlusCircleOutlined
                    onClick={() => {
                      add({
                        oper: '=',
                      });
                    }}
                  />
                </div>
                {fields.map(({ key, name }) => {
                  return (
                    <Space key={key}>
                      <Form.Item name={[name, 'label']} rules={[{ required: true }]}>
                        <Select suffixIcon={<CaretDownOutlined />} allowClear showSearch style={{ width: 170 }}>
                          {getLablesOptions(labels)}
                        </Select>
                      </Form.Item>
                      <Form.Item name={[name, 'oper']} rules={[{ required: true }]}>
                        <Select suffixIcon={<CaretDownOutlined />} style={{ width: 60 }}>
                          <Select.Option value='='>=</Select.Option>
                          <Select.Option value='!='>!=</Select.Option>
                          <Select.Option value='=~'>=~</Select.Option>
                          <Select.Option value='!~'>!~</Select.Option>
                        </Select>
                      </Form.Item>
                      <Form.Item name={[name, 'value']} rules={[{ required: true }]}>
                        <Input style={{ width: 200 }} />
                      </Form.Item>
                      <Form.Item>
                        <MinusCircleOutlined
                          onClick={() => {
                            remove(name);
                          }}
                        />
                      </Form.Item>
                    </Space>
                  );
                })}
              </>
            )}
          </Form.List>
          <Form.Item label='动态过滤标签' name='dynamicLabels'>
            <Select allowClear showSearch mode='multiple'>
              {getLablesOptions(filteredLabels)}
            </Select>
          </Form.Item>
          <Form.Item label='展开维度标签' name='dimensionLabels' rules={[{ required: true }]}>
            <Select allowClear showSearch mode='multiple'>
              {getLablesOptions(filteredLabels)}
            </Select>
          </Form.Item>
          <div style={{ textAlign: 'right', marginBottom: 10 }}>
            <Button
              onClick={() => {
                const values = form.getFieldsValue();
                setPreviewVisible(true);
                setPreviewLoading(true);
                const filtersStr = getFiltersStr(values.filters);
                const _labels = _.compact(_.concat(values.dynamicLabels, values.dimensionLabels));
                const requests = _.map(_labels, (item) => {
                  return getLabelValues(item, range, filtersStr ? `{${filtersStr}}` : '');
                });
                Promise.all(requests).then((res) => {
                  const data = _.map(_labels, (item, idx) => {
                    return {
                      label: item,
                      values: res[idx],
                    };
                  });
                  setPreviewData(data);
                  setPreviewLoading(false);
                });
              }}
            >
              预览
            </Button>
          </div>
          {previewVisible && (
            <Table
              size='small'
              rowKey='label'
              columns={[
                {
                  title: 'Lable Key',
                  dataIndex: 'label',
                },
                {
                  title: 'Lable Value 数量',
                  dataIndex: 'values',
                  render: (text) => {
                    return text.length;
                  },
                },
                {
                  title: 'Lable Value 样例',
                  dataIndex: 'values',
                  render: (text) => {
                    return (
                      <Tooltip
                        placement='right'
                        title={
                          <div>
                            {_.map(text, (item) => {
                              return <div key={item}>{item}</div>;
                            })}
                          </div>
                        }
                      >{`${_.head(text)}...`}</Tooltip>
                    );
                  },
                },
              ]}
              dataSource={previewData}
              loading={previewLoading}
            />
          )}
        </Form>
      )}
      {activeKey === 'import' && (
        <Form
          form={form}
          preserve={false}
          layout='vertical'
          initialValues={
            initialValues || {
              cate: false,
            }
          }
        >
          <Form.Item label='视图名称' name='name' rules={[{ required: true }]}>
            <Input />
          </Form.Item>
          {admin && (
            <Form.Item label='是否公开' name='cate' rules={[{ required: true }]} valuePropName='checked'>
              <Switch />
            </Form.Item>
          )}
          <Form.Item
            label='配置JSON:'
            name='import'
            rules={[
              {
                required: true,
                message: t('请输入配置'),
                validateTrigger: 'trigger',
              },
            ]}
          >
            <Input.TextArea className='code-area' placeholder={t('请输入配置')} rows={4} />
          </Form.Item>
        </Form>
      )}
    </Modal>
  );
}
Example #19
Source File: chartConfigModal.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
// 新增图表和编辑图表均在此组件

export default function ChartConfigModal(props: Props) {
  const { t } = useTranslation();
  const { busiId, groupId, show, onVisibleChange, initialValue, variableConfig, cluster, id } = props;
  const layout = initialValue?.configs.layout;
  const [innerVariableConfig, setInnerVariableConfig] = useState<VariableType | undefined>(variableConfig);
  const [chartForm] = Form.useForm();
  const [initialQL, setInitialQL] = useState([{ PromQL: '' }]);
  const [legend, setLegend] = useState<boolean>(initialValue?.configs.legend || false);
  const [step, setStep] = useState<number | null>(null);
  const [highLevelConfig, setHighLevelConfig] = useState<HighLevelConfigType>(
    initialValue?.configs.highLevelConfig || {
      shared: true,
      sharedSortDirection: 'desc',
      precision: 'short',
      formatUnit: 1000,
    },
  );
  const [range, setRange] = useState<Range>({
    start: 0,
    end: 0,
  });
  useEffect(() => {
    if (initialValue) {
      chartForm.setFieldsValue(initialValue.configs);
      setInitialQL(initialValue.configs.QL);
    }
  }, [initialValue]);

  const handleAddChart = async (e) => {
    try {
      await chartForm.validateFields();
      let formData: ChartConfig = Object.assign(
        chartForm.getFieldsValue(),
        { legend, highLevelConfig },
        {
          version: 1, // Temporarily, hardcode 1
          layout,
        },
      );
      if (initialValue && initialValue.id) {
        await updateCharts(busiId, [
          {
            configs: formData,
            weight: 0,
            group_id: groupId,
            id: initialValue.id,
          },
        ]);
      } else {
        await createChart(busiId, {
          configs: JSON.stringify(formData),
          weight: 0,
          group_id: groupId,
        });
      }

      onVisibleChange(true);
    } catch (errorInfo) {
      console.log('Failed:', errorInfo);
    }
  };

  const PromqlEditorField = ({ onChange = (e: any) => {}, value = '', fields, remove, add, index, name }) => {
    return (
      <div style={{ display: 'flex', alignItems: 'center' }}>
        <PromqlEditor
          xCluster='Default'
          onChange={onChange}
          value={value}
          style={{
            width: '310px',
            // flex: 1,
          }}
        />
        {fields.length > 1 ? (
          <MinusCircleOutlined
            style={{ marginLeft: 10 }}
            onClick={() => {
              remove(name);
            }}
          />
        ) : null}
        {index === fields.length - 1 && (
          <PlusCircleOutlined
            style={{ marginLeft: 10 }}
            onClick={() => {
              add();
            }}
          />
        )}
      </div>
    );
  };

  const handleVariableChange = (value) => {
    setInnerVariableConfig(value);
  };

  const aggrFuncMenu = (
    <Menu
      onClick={(sort) => {
        setHighLevelConfig({ ...highLevelConfig, sharedSortDirection: (sort as { key: 'desc' | 'asc' }).key });
      }}
      selectedKeys={[highLevelConfig.sharedSortDirection]}
    >
      <Menu.Item key='desc'>desc</Menu.Item>
      <Menu.Item key='asc'>asc</Menu.Item>
    </Menu>
  );

  const precisionMenu = (
    <Menu
      onClick={(precision) => {
        const precisionKey = isNaN(Number(precision.key)) ? precision.key : Number(precision.key);
        setHighLevelConfig({ ...highLevelConfig, formatUnit: precisionKey as 1024 | 1000 | 'humantime' });
      }}
      selectedKeys={[String(highLevelConfig.formatUnit)]}
    >
      <Menu.Item key={'1000'}>Ki, Mi, Gi by 1000</Menu.Item>
      <Menu.Item key={'1024'}>Ki, Mi, Gi by 1024</Menu.Item>
      <Menu.Item key={'humantime'}>Human time duration</Menu.Item>
    </Menu>
  );

  const formatUnitInfoMap = {
    1024: 'Ki, Mi, Gi by 1024',
    1000: 'Ki, Mi, Gi by 1000',
    humantime: 'Human time duration',
  };

  const getContent = () => {
    const aggrFuncMenu = (
      <Menu
        onClick={(sort) => {
          setHighLevelConfig({ ...highLevelConfig, sharedSortDirection: (sort as { key: 'desc' | 'asc' }).key });
        }}
        selectedKeys={[highLevelConfig.sharedSortDirection]}
      >
        <Menu.Item key='desc'>desc</Menu.Item>
        <Menu.Item key='asc'>asc</Menu.Item>
      </Menu>
    );
    const precisionMenu = (
      <Menu
        onClick={(precision) => {
          const precisionKey = isNaN(Number(precision.key)) ? precision.key : Number(precision.key);
          setHighLevelConfig({ ...highLevelConfig, formatUnit: precisionKey as 1024 | 1000 | 'humantime' });
        }}
        selectedKeys={[String(highLevelConfig.formatUnit)]}
      >
        <Menu.Item key={'1000'}>Ki, Mi, Gi by 1000</Menu.Item>
        <Menu.Item key={'1024'}>Ki, Mi, Gi by 1024</Menu.Item>
        <Menu.Item key={'humantime'}>Human time duration</Menu.Item>
      </Menu>
    );
    return (
      <div>
        <Checkbox
          checked={highLevelConfig.shared}
          onChange={(e) => {
            setHighLevelConfig({ ...highLevelConfig, shared: e.target.checked });
          }}
        >
          Multi Series in Tooltip, order value
        </Checkbox>
        <Dropdown overlay={aggrFuncMenu}>
          <a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
            {highLevelConfig.sharedSortDirection} <DownOutlined />
          </a>
        </Dropdown>
        <br />
        <Checkbox
          checked={legend}
          onChange={(e) => {
            setLegend(e.target.checked);
          }}
        >
          Show Legend
        </Checkbox>
        <br />
        <Checkbox
          checked={highLevelConfig.precision === 'short'}
          onChange={(e) => {
            setHighLevelConfig({ ...highLevelConfig, precision: e.target.checked ? 'short' : 'origin' });
          }}
        >
          Value format with:{' '}
        </Checkbox>
        <Dropdown overlay={precisionMenu}>
          <a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
            {formatUnitInfoMap[highLevelConfig.formatUnit]} <DownOutlined />
          </a>
        </Dropdown>
      </div>
    );
  };

  return (
    <Modal
      title={
        <div style={{ display: 'flex', alignItems: 'center' }}>
          <div>{initialValue ? t('编辑图表') : t('新建图表')}</div>
          <div style={{ flex: 1, display: 'flex', justifyContent: 'flex-end', alignItems: 'center', fontSize: 12, lineHeight: '20px' }}>
            <DateRangePicker onChange={(e) => setRange(e)} />
            <Resolution onChange={(v) => setStep(v)} initialValue={step} />
            <CloseOutlined
              style={{ fontSize: 18 }}
              onClick={() => {
                onVisibleChange(false);
              }}
            />
          </div>
        </div>
      }
      width={900}
      visible={show}
      destroyOnClose={true}
      onOk={handleAddChart}
      closable={false}
      onCancel={() => {
        onVisibleChange(false);
      }}
    >
      <Form {...layout} form={chartForm} preserve={false}>
        <Row>
          <Col span={12}>
            <VariableConfig onChange={handleVariableChange} value={innerVariableConfig} editable={false} cluster={cluster} range={range} id={id} />
            <br />
            <Form.Item
              label={t('标题')}
              name='name'
              labelCol={{
                span: 4,
              }}
              wrapperCol={{
                span: 20,
              }}
              rules={[
                {
                  required: true,
                  message: t('图表名称'),
                },
              ]}
            >
              <Input />
            </Form.Item>
            <Form.Item label={t('下钻链接')} name='link' labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
              <Input />
            </Form.Item>

            <Form.Item
              wrapperCol={{
                span: 24,
              }}
              style={{
                marginBottom: '0px',
              }}
            >
              <Form.List name='QL' initialValue={initialQL}>
                {(fields, { add, remove }, { errors }) => {
                  return (
                    <>
                      {fields.length ? (
                        fields.map(({ key, name, fieldKey, ...restField }, index) => {
                          return (
                            <div key={name + fieldKey}>
                              <Form.Item
                                label='PromQL'
                                name={[name, 'PromQL']}
                                labelCol={{
                                  span: 4,
                                }}
                                wrapperCol={{
                                  span: 20,
                                }}
                                validateTrigger={['onBlur']}
                                rules={[
                                  {
                                    required: true,
                                    message: t('请输入PromQL'),
                                  },
                                ]}
                              >
                                <PromqlEditorField key={name + fieldKey} name={name} fields={fields} index={index} remove={remove} add={add} />
                              </Form.Item>
                              <Form.Item
                                label='Legend'
                                name={[name, 'Legend']}
                                tooltip={{
                                  getPopupContainer: () => document.body,
                                  title:
                                    'Controls the name of the time series, using name or pattern. For example {{hostname}} will be replaced with label value for the label hostname.',
                                }}
                                labelCol={{
                                  span: 4,
                                }}
                                wrapperCol={{
                                  span: 20,
                                }}
                              >
                                <Input />
                              </Form.Item>
                            </div>
                          );
                        })
                      ) : (
                        <PlusCircleOutlined
                          onClick={() => {
                            add();
                          }}
                        />
                      )}
                      <Form.ErrorList errors={errors} />
                    </>
                  );
                }}
              </Form.List>
            </Form.Item>
            <Row>
              <Col span={11}>
                <Form.Item label={t('预警值')} name='yplotline1' labelCol={{ span: 9 }} wrapperCol={{ span: 16 }}>
                  <InputNumber />
                </Form.Item>
              </Col>
              <Col span={12} offset={1}>
                <Form.Item label={t('危险值')} name='yplotline2' labelCol={{ span: 7 }} wrapperCol={{ span: 20 }}>
                  <InputNumber />
                </Form.Item>
              </Col>

              {/* <Col span={23} offset={1}>
                <Form.Item>
                  <Checkbox
                    checked={highLevelConfig.shared}
                    onChange={(e) => {
                      setHighLevelConfig({ ...highLevelConfig, shared: e.target.checked });
                    }}
                  >
                    Multi Series in Tooltip, order value
                  </Checkbox>
                  <Dropdown overlay={aggrFuncMenu}>
                    <a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
                      {highLevelConfig.sharedSortDirection} <DownOutlined />
                    </a>
                  </Dropdown>
                </Form.Item>
              </Col>
              <Col span={23} offset={1}>
                <Form.Item>
                  <Checkbox
                    checked={legend}
                    onChange={(e) => {
                      setLegend(e.target.checked);
                    }}
                  >
                    Show Legend
                  </Checkbox>
                </Form.Item>
              </Col>
              <Col span={23} offset={1}>
                <Form.Item>
                  <Checkbox
                    checked={highLevelConfig.precision === 'short'}
                    onChange={(e) => {
                      setHighLevelConfig({ ...highLevelConfig, precision: e.target.checked ? 'short' : 'origin' });
                    }}
                  >
                    Value format with:{' '}
                  </Checkbox>
                  <Dropdown overlay={precisionMenu}>
                    <a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
                      {formatUnitInfoMap[highLevelConfig.formatUnit]} <DownOutlined />
                    </a>
                  </Dropdown>
                </Form.Item>
              </Col> */}
            </Row>
          </Col>
          <Col span={12}>
            <Form.Item
              wrapperCol={{ span: 22, offset: 2 }}
              shouldUpdate={(prevValues, curValues) =>
                prevValues.QL !== curValues.QL || prevValues.multi === curValues.multi || prevValues.legend === curValues.legend || prevValues.format === curValues.format
              }
            >
              {({ getFieldsValue }) => {
                const { QL = [], yplotline1, yplotline2 } = getFieldsValue();
                const promqls = QL.filter((item) => item && item.PromQL).map((item) =>
                  innerVariableConfig ? replaceExpressionVars(item.PromQL, innerVariableConfig, innerVariableConfig.var.length, id) : item.PromQL,
                );
                const legendTitleFormats = QL.map((item) => item && item.Legend);
                return (
                  <div className={legend ? 'graph-container graph-container-hasLegend' : 'graph-container'}>
                    <div className='graph-header' style={{ height: '35px', lineHeight: '35px', display: 'flex', justifyContent: 'space-between' }}>
                      <div>预览图表</div>
                      <div className='graph-extra'>
                        <span className='graph-operationbar-item' key='info'>
                          <Popover placement='left' content={getContent()} trigger='click' autoAdjustOverflow={false} getPopupContainer={() => document.body}>
                            <Button className='' type='link' size='small' onClick={(e) => e.preventDefault()}>
                              <SettingOutlined />
                            </Button>
                          </Popover>
                        </span>
                      </div>
                    </div>
                    <Graph
                      showHeader={false}
                      graphConfigInnerVisible={false}
                      highLevelConfig={highLevelConfig}
                      data={{
                        yAxis: {
                          plotLines: [
                            {
                              value: yplotline1 ? yplotline1 : undefined,
                              color: 'orange',
                            },
                            {
                              value: yplotline2 ? yplotline2 : undefined,
                              color: 'red',
                            },
                          ],
                        },
                        legend: legend,
                        step,
                        range,
                        promqls,
                        legendTitleFormats,
                      }}
                    />
                  </div>
                );
                // ) : null;
              }}
            </Form.Item>
          </Col>
        </Row>
      </Form>
    </Modal>
  );
}
Example #20
Source File: index.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
UserForm = React.forwardRef<ReactNode, UserAndPasswordFormProps>((props, ref) => {
  const { t } = useTranslation();
  const { userId } = props;
  const [form] = Form.useForm();
  const [initialValues, setInitialValues] = useState<User>();
  const [loading, setLoading] = useState<boolean>(true);
  const [contactsList, setContactsList] = useState<ContactsItem[]>([]);
  const [roleList, setRoleList] = useState<{ name: string; note: string }[]>([]);

  useImperativeHandle(ref, () => ({
    form: form,
  }));
  useEffect(() => {
    if (userId) {
      getUserInfoDetail(userId);
    } else {
      setLoading(false);
    }

    getContacts();
    getRoles().then((res) => setRoleList(res));
  }, []);

  const getContacts = () => {
    getNotifyChannels().then((data: Array<ContactsItem>) => {
      setContactsList(data);
    });
  };

  const getUserInfoDetail = (id: string) => {
    getUserInfo(id).then((data: User) => {
      let contacts: Array<Contacts> = [];

      if (data.contacts) {
        Object.keys(data.contacts).forEach((item: string) => {
          let val: Contacts = {
            key: item,
            value: data.contacts[item],
          };
          contacts.push(val);
        });
      }

      setInitialValues(
        Object.assign({}, data, {
          contacts,
        }),
      );
      setLoading(false);
    });
  };

  return !loading ? (
    <Form {...layout} form={form} initialValues={initialValues} preserve={false}>
      {!userId && (
        <Form.Item
          label={t('用户名')}
          name='username'
          rules={[
            {
              required: true,
              message: t('用户名不能为空!'),
            },
          ]}
        >
          <Input />
        </Form.Item>
      )}
      <Form.Item label={t('显示名')} name='nickname'>
        <Input />
      </Form.Item>
      {!userId && (
        <>
          <Form.Item
            name='password'
            label={t('密码')}
            rules={[
              {
                required: true,
                message: t('请输入密码!'),
              },
            ]}
            hasFeedback
          >
            <Input.Password />
          </Form.Item>

          <Form.Item
            name='confirm'
            label={t('确认密码')}
            dependencies={['password']}
            hasFeedback
            rules={[
              {
                required: true,
                message: t('请确认密码!'),
              },
              ({ getFieldValue }) => ({
                validator(_, value) {
                  if (!value || getFieldValue('password') === value) {
                    return Promise.resolve();
                  }

                  return Promise.reject(new Error('密码不一致!'));
                },
              }),
            ]}
          >
            <Input.Password />
          </Form.Item>
        </>
      )}
      <Form.Item
        label={t('角色')}
        name='roles'
        rules={[
          {
            required: true,
            message: t('角色不能为空!'),
          },
        ]}
      >
        <Select mode='multiple'>
          {roleList.map((item, index) => (
            <Option value={item.name} key={index}>
              <div>
                <div>{item.name}</div>
                <div style={{ color: '#8c8c8c' }}>{item.note}</div>
              </div>
            </Option>
          ))}
        </Select>
      </Form.Item>
      <Form.Item label={t('邮箱')} name='email'>
        <Input />
      </Form.Item>
      <Form.Item label={t('手机')} name='phone'>
        <Input />
      </Form.Item>
      <Form.Item label={t('更多联系方式')}>
        <Form.List name='contacts'>
          {(fields, { add, remove }) => (
            <>
              {fields.map(({ key, name, fieldKey, ...restField }) => (
                <Space
                  key={key}
                  style={{
                    display: 'flex',
                  }}
                  align='baseline'
                >
                  <Form.Item
                    style={{
                      width: '180px',
                    }}
                    {...restField}
                    name={[name, 'key']}
                    fieldKey={[fieldKey, 'key']}
                    rules={[
                      {
                        required: true,
                        message: t('联系方式不能为空'),
                      },
                    ]}
                  >
                    <Select suffixIcon={<CaretDownOutlined />} placeholder={t('请选择联系方式')}>
                      {contactsList.map((item, index) => (
                        <Option value={item.key} key={index}>
                          {item.label}
                        </Option>
                      ))}
                    </Select>
                  </Form.Item>
                  <Form.Item
                    {...restField}
                    style={{
                      width: '170px',
                    }}
                    name={[name, 'value']}
                    fieldKey={[fieldKey, 'value']}
                    rules={[
                      {
                        required: true,
                        message: t('值不能为空'),
                      },
                    ]}
                  >
                    <Input placeholder={t('请输入值')} />
                  </Form.Item>
                  <MinusCircleOutlined className='control-icon-normal' onClick={() => remove(name)} />
                </Space>
              ))}
              <PlusCircleOutlined className='control-icon-normal' onClick={() => add()} />
            </>
          )}
        </Form.List>
      </Form.Item>
    </Form>
  ) : null;
})
Example #21
Source File: operateForm.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
OperateForm: React.FC<Props> = ({ detail = {}, type, tagsObj = {} }) => {
  const btimeDefault = new Date().getTime();
  const etimeDefault = new Date().getTime() + 1 * 60 * 60 * 1000; // 默认时长1h
  const { t, i18n } = useTranslation();
  const { clusters: clusterList } = useSelector<RootState, CommonStoreState>((state) => state.common);
  const layout = {
    labelCol: {
      span: 24,
    },
    wrapperCol: {
      span: 24,
    },
  };
  const tailLayout = {
    labelCol: {
      span: 0,
    },
    wrapperCol: {
      span: 24,
    },
  };

  const [form] = Form.useForm(null as any);
  const history = useHistory();
  const [btnLoading, setBtnLoading] = useState<boolean>(false);
  const [timeLen, setTimeLen] = useState('1h');
  const { curBusiItem, busiGroups } = useSelector<RootState, CommonStoreState>((state) => state.common);

  useEffect(() => {
    const btime = form.getFieldValue('btime');
    const etime = form.getFieldValue('etime');
    if (!!etime && !!btime) {
      const d = moment.duration(etime - btime).days();
      const h = moment.duration(etime - btime).hours();
      const m = moment.duration(etime - btime).minutes();
      const s = moment.duration(etime - btime).seconds();
    }
    if (curBusiItem) {
      form.setFieldsValue({ busiGroup: curBusiItem.id });
    } else if (busiGroups.length > 0) {
      form.setFieldsValue({ busiGroup: busiGroups[0].id });
    } else {
      message.warning('无可用业务组');
      history.push('/alert-mutes');
    }
    return () => {};
  }, [form]);

  useEffect(() => {
    // 只有add 的时候才传入tagsObj
    if (tagsObj?.tags && tagsObj?.tags.length > 0) {
      const tags = tagsObj?.tags?.map((item) => {
        return {
          ...item,
          value: item.func === 'in' ? item.value.split(' ') : item.value,
        };
      });
      form.setFieldsValue({
        tags: tags || [{}],
        cluster: tagsObj.cluster,
      });
    }
  }, [tagsObj]);

  const timeChange = () => {
    const btime = form.getFieldValue('btime');
    const etime = form.getFieldValue('etime');
    if (!!etime && !!btime) {
      const d = Math.floor(moment.duration(etime - btime).asDays());
      const h = Math.floor(moment.duration(etime - btime).hours());
      const m = Math.floor(moment.duration(etime - btime).minutes());
      const s = Math.floor(moment.duration(etime - btime).seconds());
      const timeLen = `${d ? `${d}d ` : ''}${h ? `${h}h ` : ''}${m ? `${m}m ` : ''}${s ? `${s}s` : ''}`;
      setTimeLen(timeLen);
    }
  };

  const onFinish = (values) => {
    setBtnLoading(true);
    const tags = values?.tags?.map((item) => {
      return {
        ...item,
        value: Array.isArray(item.value) ? item.value.join(' ') : item.value,
      };
    });
    const params = {
      ...values,
      btime: moment(values.btime).unix(),
      etime: moment(values.etime).unix(),
      tags,
    };
    const curBusiItemId = form.getFieldValue('busiGroup');
    addShield(params, curBusiItemId)
      .then((_) => {
        message.success(t('新建告警屏蔽成功'));
        history.push('/alert-mutes');
      })
      .finally(() => {
        setBtnLoading(false);
      });
  };
  const onFinishFailed = () => {
    setBtnLoading(false);
  };

  const timeLenChange = (val: string) => {
    setTimeLen(val);

    const time = new Date().getTime();
    if (val === 'forever') {
      const longTime = 7 * 24 * 3600 * 1000 * 10000;
      form.setFieldsValue({
        btime: moment(time),
        etime: moment(time).add({
          seconds: longTime,
        }),
      });
      return;
    }
    const unit = val.charAt(val.length - 1);
    const num = val.substr(0, val.length - 1);
    form.setFieldsValue({
      btime: moment(time),
      etime: moment(time).add({
        [unit]: num,
      }),
    });
  };

  const content = (
    <Form
      form={form}
      {...layout}
      layout='vertical'
      className='operate-form'
      onFinish={onFinish}
      onFinishFailed={onFinishFailed}
      initialValues={{
        ...detail,
        btime: detail?.btime ? moment(detail.btime * 1000) : moment(btimeDefault),
        etime: detail?.etime ? moment(detail.etime * 1000) : moment(etimeDefault),
        cluster: clusterList[0] || 'Default',
      }}
    >
      <Card>
        <Form.Item label={t('业务组:')} name='busiGroup'>
          <Select suffixIcon={<CaretDownOutlined />}>
            {busiGroups?.map((item) => (
              <Option value={item.id} key={item.id}>
                {item.name}
              </Option>
            ))}
          </Select>
        </Form.Item>

        <Form.Item
          label={t('生效集群:')}
          name='cluster'
          rules={[
            {
              required: true,
              message: t('生效集群不能为空'),
            },
          ]}
        >
          <Select suffixIcon={<CaretDownOutlined />}>
            {clusterList?.map((item) => (
              <Option value={item} key={item}>
                {item}
              </Option>
            ))}
          </Select>
        </Form.Item>

        <Row gutter={10}>
          <Col span={8}>
            <Form.Item label={t('屏蔽开始时间:')} name='btime'>
              <DatePicker showTime onChange={timeChange} />
            </Form.Item>
          </Col>
          <Col span={8}>
            <Form.Item label={t('屏蔽时长:')}>
              <Select suffixIcon={<CaretDownOutlined />} placeholder={t('请选择屏蔽时长')} onChange={timeLenChange} value={timeLen}>
                {timeLensDefault.map((item: any, index: number) => (
                  <Option key={index} value={item.value}>
                    {item.value}
                  </Option>
                ))}
              </Select>
            </Form.Item>
          </Col>
          <Col span={8}>
            <Form.Item label={t('屏蔽结束时间:')} name='etime'>
              <DatePicker showTime onChange={timeChange} />
            </Form.Item>
          </Col>
        </Row>
        <Row gutter={[10, 10]} style={{ marginBottom: '8px' }}>
          <Col span={5}>
            {t('屏蔽事件标签Key:')}
            <Tooltip title={t(`这里的标签是指告警事件的标签,通过如下标签匹配规则过滤告警事件`)}>
              <QuestionCircleFilled />
            </Tooltip>
          </Col>
          <Col span={3}>{t('运算符:')}</Col>
          <Col span={16}>{t('标签Value:')}</Col>
        </Row>
        <Form.List name='tags' initialValue={[{}]}>
          {(fields, { add, remove }) => (
            <>
              {fields.map((field, index) => (
                <TagItem field={field} key={index} remove={remove} form={form} />
              ))}
              <Form.Item>
                <PlusCircleOutlined className='control-icon-normal' onClick={() => add()} />
              </Form.Item>
            </>
          )}
        </Form.List>

        {/* <Form.Item label={t('屏蔽时间')} name='time'>
          <RangeDatePicker />
        </Form.Item> */}
        <Form.Item
          label={t('屏蔽原因')}
          name='cause'
          rules={[
            {
              required: true,
              message: t('请填写屏蔽原因'),
            },
          ]}
        >
          <TextArea rows={3} />
        </Form.Item>
        <Form.Item {...tailLayout}>
          <Row gutter={[10, 10]}>
            <Col span={1}>
              <Button type='primary' htmlType='submit'>
                {type === 2 ? t('克隆') : t('创建')}
              </Button>
            </Col>

            <Col
              span={1}
              style={{
                marginLeft: 40,
              }}
            >
              <Button onClick={() => window.history.back()}>{t('取消')}</Button>
            </Col>
          </Row>
        </Form.Item>
      </Card>
    </Form>
  );
  return <div className='operate-form-index'>{content}</div>;
}
Example #22
Source File: FunctionDebuggerToolbar.tsx    From next-basics with GNU General Public License v3.0 4 votes vote down vote up
export function FunctionDebuggerToolbar({
  type,
  status,
  saveDisabled,
  onButtonClick,
}: FunctionDebuggerToolbarProps): React.ReactElement {
  const refinedType = type ?? "input";
  const isInput = refinedType === "input" || refinedType === "test-input";

  const handleRunClick = useCallback(() => {
    onButtonClick?.({ action: "run" });
  }, [onButtonClick]);

  const handleSaveClick = useCallback(() => {
    if (!saveDisabled) {
      onButtonClick?.({ action: "save" });
    }
  }, [onButtonClick, saveDisabled]);

  const handleDeleteClick = useCallback(() => {
    onButtonClick?.({ action: "delete" });
  }, [onButtonClick]);

  return (
    <div
      className={classNames(
        styles.debuggerToolbar,
        status && styles[status],
        refinedType === "input" || refinedType === "output"
          ? styles.debug
          : styles.test,
        isInput ? styles.input : styles.output
      )}
      data-override-theme="dark"
    >
      <div className={styles.header}>
        {refinedType === "input"
          ? "Input"
          : refinedType === "test-input"
          ? "Test Input"
          : refinedType === "test-output"
          ? "Expect Output"
          : "Output"}
        {isInput && (
          <span className={styles.headerSuffix}>
            &nbsp;(argument list in JSON format)
          </span>
        )}
      </div>
      {isInput ? (
        <div className={styles.buttons}>
          <Tooltip title="Run">
            <div className={styles.debuggerButton} onClick={handleRunClick}>
              <PlayCircleOutlined />
            </div>
          </Tooltip>
          <Tooltip
            title={refinedType === "input" ? "Add as a test case" : "Update"}
          >
            <div
              className={classNames(styles.debuggerButton, {
                [styles.disabled]: saveDisabled,
              })}
              onClick={handleSaveClick}
            >
              {refinedType === "input" ? (
                <PlusCircleOutlined />
              ) : (
                <SaveOutlined />
              )}
            </div>
          </Tooltip>
          {refinedType === "test-input" && (
            <Tooltip title="Delete">
              <div
                className={styles.debuggerButton}
                onClick={handleDeleteClick}
              >
                <DeleteOutlined />
              </div>
            </Tooltip>
          )}
        </div>
      ) : (
        refinedType === "test-output" && (
          <div className={styles.secondHeader}>
            {status === "ok" ? (
              <>
                <span className={styles.secondHeaderIcon}>
                  <CheckOutlined />
                </span>
                <span>Test: passed</span>
              </>
            ) : status === "failed" ? (
              <>
                <span className={styles.secondHeaderIcon}>
                  <CloseOutlined />
                </span>
                <span>Test: failed</span>
              </>
            ) : (
              <>
                <span className={styles.secondHeaderIcon}>
                  <QuestionOutlined />
                </span>
                <span>Test: expired</span>
              </>
            )}
          </div>
        )
      )}
    </div>
  );
}
Example #23
Source File: operateForm.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
operateForm: React.FC<Props> = ({ type, detail = {} }) => {
  const { t, i18n } = useTranslation();
  const history = useHistory(); // 创建的时候默认选中的值

  const [form] = Form.useForm();
  const { clusters: clusterList } = useSelector<RootState, CommonStoreState>((state) => state.common);
  const { curBusiItem } = useSelector<RootState, CommonStoreState>((state) => state.common);

  const [contactList, setInitContactList] = useState([]);
  const [notifyGroups, setNotifyGroups] = useState<any[]>([]);
  const [initVal, setInitVal] = useState<any>({});
  const [refresh, setRefresh] = useState(true);
  useEffect(() => {
    getNotifyChannel();
    getGroups('');

    return () => {};
  }, []);

  useEffect(() => {
    const data = {
      ...detail,
      enable_time: detail?.enable_stime ? [detail.enable_stime, detail.enable_etime] : [],
      enable_status: detail?.disabled === undefined ? true : !detail?.disabled,
    };
    setInitVal(data);
    if (type == 1) {
      const groups = (detail.notify_groups_obj ? detail.notify_groups_obj.filter((item) => !notifyGroups.find((i) => item.id === i.id)) : []).concat(notifyGroups);
      setNotifyGroups(groups);
    }
  }, [JSON.stringify(detail)]);

  const enableDaysOfWeekOptions = [t('周日'), t('周一'), t('周二'), t('周三'), t('周四'), t('周五'), t('周六')].map((v, i) => {
    return <Option value={String(i)} key={i}>{`${v}`}</Option>;
  });

  const contactListCheckboxes = contactList.map((c: { key: string; label: string }) => (
    <Checkbox value={c.key} key={c.label}>
      {c.label}
    </Checkbox>
  ));

  const notifyGroupsOptions = notifyGroups.map((ng: any) => (
    <Option value={String(ng.id)} key={ng.id}>
      {ng.name}
    </Option>
  ));

  const getNotifyChannel = async () => {
    const res = await getNotifiesList();
    let contactList = res || [];
    setInitContactList(contactList);
  };

  const getGroups = async (str) => {
    const res = await getTeamInfoList({ query: str });
    const data = res.dat || res;
    const combineData = (detail.notify_groups_obj ? detail.notify_groups_obj.filter((item) => !data.find((i) => item.id === i.id)) : []).concat(data);
    setNotifyGroups(combineData || []);
  };

  const addSubmit = () => {
    form.validateFields().then(async (values) => {
      const res = await prometheusQuery({ query: values.prom_ql }, values.cluster);
      if (res.error) {
        notification.error({
          message: res.error,
        });
        return false;
      }
      const callbacks = values.callbacks.map((item) => item.url);
      const data = {
        ...values,
        enable_stime: values.enable_time[0].format('HH:mm'),
        enable_etime: values.enable_time[1].format('HH:mm'),
        disabled: !values.enable_status ? 1 : 0,
        notify_recovered: values.notify_recovered ? 1 : 0,
        enable_in_bg: values.enable_in_bg ? 1 : 0,
        callbacks,
      };
      let reqBody,
        method = 'Post';
      if (type === 1) {
        reqBody = data;
        method = 'Put';
        const res = await EditStrategy(reqBody, curBusiItem.id, detail.id);
        if (res.err) {
          message.error(res.error);
        } else {
          message.success(t('编辑成功!'));
          history.push('/alert-rules');
        }
      } else {
        reqBody = [data];
        const { dat } = await addOrEditStrategy(reqBody, curBusiItem.id, method);
        let errorNum = 0;
        const msg = Object.keys(dat).map((key) => {
          dat[key] && errorNum++;
          return dat[key];
        });

        if (!errorNum) {
          message.success(`${type === 2 ? t('告警规则克隆成功') : t('告警规则创建成功')}`);
          history.push('/alert-rules');
        } else {
          message.error(t(msg));
        }
      }
    });
  };

  const debounceFetcher = useCallback(debounce(getGroups, 800), []);
  return (
    <div className='operate_con'>
      <Form
        {...layout}
        form={form}
        className='strategy-form'
        layout={refresh ? 'horizontal' : 'horizontal'}
        initialValues={{
          prom_eval_interval: 15,
          prom_for_duration: 60,
          severity: 2,
          disabled: 0, // 0:立即启用 1:禁用  待修改
          // notify_recovered: 1, // 1:启用
          cluster: clusterList[0] || 'Default', // 生效集群
          enable_days_of_week: ['1', '2', '3', '4', '5', '6', '0'],
          ...detail,
          enable_in_bg: detail?.enable_in_bg === 1,
          enable_time: detail?.enable_stime ? [moment(detail.enable_stime, 'HH:mm'), moment(detail.enable_etime, 'HH:mm')] : [moment('00:00', 'HH:mm'), moment('23:59', 'HH:mm')],
          enable_status: detail?.disabled === undefined ? true : !detail?.disabled,
          notify_recovered: detail?.notify_recovered === 1 || detail?.notify_recovered === undefined ? true : false, // 1:启用 0:禁用
          recover_duration: detail?.recover_duration || 0,
          callbacks: !!detail?.callbacks
            ? detail.callbacks.map((item) => ({
                url: item,
              }))
            : [{}],
        }}
      >
        <Space direction='vertical' style={{ width: '100%' }}>
          <Card title={t('基本配置')}>
            <Form.Item
              label={t('规则标题:')}
              name='name'
              rules={[
                {
                  required: true,
                  message: t('规则标题不能为空'),
                },
              ]}
            >
              <Input placeholder={t('请输入规则标题')} />
            </Form.Item>
            <Form.Item
              label={t('规则备注:')}
              name='note'
              rules={[
                {
                  required: false,
                },
              ]}
            >
              <Input placeholder={t('请输入规则备注')} />
            </Form.Item>
            <Form.Item
              label={t('告警级别')}
              name='severity'
              rules={[
                {
                  required: true,
                  message: t('告警级别不能为空'),
                },
              ]}
            >
              <Radio.Group>
                <Radio value={1}>{t('一级报警')}</Radio>
                <Radio value={2}>{t('二级报警')}</Radio>
                <Radio value={3}>{t('三级报警')}</Radio>
              </Radio.Group>
            </Form.Item>
            <Form.Item
              label={t('生效集群')}
              name='cluster'
              rules={[
                {
                  required: true,
                  message: t('生效集群不能为空'),
                },
              ]}
            >
              <Select suffixIcon={<CaretDownOutlined />}>
                {clusterList?.map((item) => (
                  <Option value={item} key={item}>
                    {item}
                  </Option>
                ))}
              </Select>
            </Form.Item>
            <AdvancedWrap>
              <AbnormalDetection form={form} />
            </AdvancedWrap>
            <Form.Item noStyle shouldUpdate={(prevValues, curValues) => prevValues.cluster !== curValues.cluster}>
              {() => {
                return (
                  <Form.Item label='PromQL' className={'Promeql-content'} required>
                    <Form.Item name='prom_ql' validateTrigger={['onBlur']} trigger='onChange' rules={[{ required: true, message: t('请输入PromQL') }]}>
                      <PromQLInput
                        url='/api/n9e/prometheus'
                        headers={{
                          'X-Cluster': form.getFieldValue('cluster'),
                          Authorization: `Bearer ${localStorage.getItem('access_token') || ''}`,
                        }}
                      />
                    </Form.Item>
                  </Form.Item>
                );
              }}
            </Form.Item>
            <Form.Item required label={t('执行频率')}>
              <Space>
                <Form.Item
                  style={{ marginBottom: 0 }}
                  name='prom_eval_interval'
                  initialValue={15}
                  wrapperCol={{ span: 10 }}
                  rules={[
                    {
                      required: true,
                      message: t('执行频率不能为空'),
                    },
                  ]}
                >
                  <InputNumber
                    min={1}
                    onChange={(val) => {
                      setRefresh(!refresh);
                    }}
                  />
                </Form.Item>
                秒
                <Tooltip title={t(`每隔${form.getFieldValue('prom_eval_interval')}秒,把PromQL作为查询条件,去查询后端存储,如果查到了数据就表示当次有监控数据触发了规则`)}>
                  <QuestionCircleFilled />
                </Tooltip>
              </Space>
            </Form.Item>
            <Form.Item
              required
              label={t('持续时长')}
              rules={[
                {
                  required: true,
                  message: t('持续时长不能为空'),
                },
              ]}
            >
              <Space>
                <Form.Item style={{ marginBottom: 0 }} name='prom_for_duration' wrapperCol={{ span: 10 }}>
                  <InputNumber min={0} />
                </Form.Item>
                秒
                <Tooltip
                  title={t(
                    `通常持续时长大于执行频率,在持续时长内按照执行频率多次执行PromQL查询,每次都触发才生成告警;如果持续时长置为0,表示只要有一次PromQL查询触发阈值,就生成告警`,
                  )}
                >
                  <QuestionCircleFilled />
                </Tooltip>
              </Space>
            </Form.Item>
            <Form.Item label='附加标签' name='append_tags' rules={[{ required: false, message: '请填写至少一项标签!' }, isValidFormat]}>
              <Select mode='tags' tokenSeparators={[' ']} open={false} placeholder={'标签格式为 key=value ,使用回车或空格分隔'} tagRender={tagRender} />
            </Form.Item>
            <Form.Item label={t('预案链接')} name='runbook_url'>
              <Input />
            </Form.Item>
          </Card>
          <Card title={t('生效配置')}>
            <Form.Item
              label={t('立即启用')}
              name='enable_status'
              rules={[
                {
                  required: true,
                  message: t('立即启用不能为空'),
                },
              ]}
              valuePropName='checked'
            >
              <Switch />
            </Form.Item>
            <Form.Item
              label={t('生效时间')}
              name='enable_days_of_week'
              rules={[
                {
                  required: true,
                  message: t('生效时间不能为空'),
                },
              ]}
            >
              <Select mode='tags'>{enableDaysOfWeekOptions}</Select>
            </Form.Item>
            <Form.Item
              name='enable_time'
              {...tailLayout}
              rules={[
                {
                  required: true,
                  message: t('生效时间不能为空'),
                },
              ]}
            >
              <TimePicker.RangePicker
                format='HH:mm'
                onChange={(val, val2) => {
                  form.setFieldsValue({
                    enable_stime: val2[0],
                    enable_etime: val2[1],
                  });
                }}
              />
            </Form.Item>
            <Form.Item label={t('仅在本业务组生效')} name='enable_in_bg' valuePropName='checked'>
              <SwitchWithLabel label='根据告警事件中的ident归属关系判断' />
            </Form.Item>
          </Card>
          <Card title={t('通知配置')}>
            <Form.Item label={t('通知媒介')} name='notify_channels'>
              <Checkbox.Group>{contactListCheckboxes}</Checkbox.Group>
            </Form.Item>
            <Form.Item label={t('告警接收组')} name='notify_groups'>
              <Select mode='multiple' showSearch optionFilterProp='children' filterOption={false} onSearch={(e) => debounceFetcher(e)} onBlur={() => getGroups('')}>
                {notifyGroupsOptions}
              </Select>
            </Form.Item>
            <Form.Item label={t('启用恢复通知')}>
              <Space>
                <Form.Item name='notify_recovered' valuePropName='checked' style={{ marginBottom: 0 }}>
                  <Switch />
                </Form.Item>
                <Tooltip title={t(`告警恢复时也发送通知`)}>
                  <QuestionCircleFilled />
                </Tooltip>
              </Space>
            </Form.Item>
            <Form.Item label={t('留观时长')} required>
              <Space>
                <Form.Item style={{ marginBottom: 0 }} name='recover_duration' initialValue={0} wrapperCol={{ span: 10 }}>
                  <InputNumber
                    min={0}
                    onChange={(val) => {
                      setRefresh(!refresh);
                    }}
                  />
                </Form.Item>
                秒
                <Tooltip title={t(`持续${form.getFieldValue('recover_duration')}秒没有再次触发阈值才发送恢复通知`)}>
                  <QuestionCircleFilled />
                </Tooltip>
              </Space>
            </Form.Item>
            <Form.Item label={t('重复发送频率')} required>
              <Space>
                <Form.Item
                  style={{ marginBottom: 0 }}
                  name='notify_repeat_step'
                  initialValue={60}
                  wrapperCol={{ span: 10 }}
                  rules={[
                    {
                      required: true,
                      message: t('重复发送频率不能为空'),
                    },
                  ]}
                >
                  <InputNumber
                    min={0}
                    onChange={(val) => {
                      setRefresh(!refresh);
                    }}
                  />
                </Form.Item>
                分钟
                <Tooltip title={t(`如果告警持续未恢复,间隔${form.getFieldValue('notify_repeat_step')}分钟之后重复提醒告警接收组的成员`)}>
                  <QuestionCircleFilled />
                </Tooltip>
              </Space>
            </Form.Item>
            <Form.Item label={t('回调地址')}>
              <Form.List name='callbacks' initialValue={[{}]}>
                {(fields, { add, remove }) => (
                  <>
                    {fields.map((field) => (
                      <Row gutter={[10, 0]} key={field.key}>
                        <Col span={22}>
                          <Form.Item name={[field.name, 'url']} fieldKey={[field.fieldKey, 'url']}>
                            <Input />
                          </Form.Item>
                        </Col>

                        <Col span={1}>
                          <MinusCircleOutlined className='control-icon-normal' onClick={() => remove(field.name)} />
                        </Col>
                      </Row>
                    ))}
                    <PlusCircleOutlined className='control-icon-normal' onClick={() => add()} />
                  </>
                )}
              </Form.List>
            </Form.Item>
          </Card>
          <Form.Item
            // {...tailLayout}
            style={{
              marginTop: 20,
            }}
          >
            <Button type='primary' onClick={addSubmit} style={{ marginRight: '8px' }}>
              {type === 1 ? t('编辑') : type === 2 ? t('克隆') : t('创建')}
            </Button>
            {type === 1 && (
              <Button
                danger
                style={{ marginRight: '8px' }}
                onClick={() => {
                  Modal.confirm({
                    title: t('是否删除该告警规则?'),
                    onOk: () => {
                      deleteStrategy([detail.id], curBusiItem.id).then(() => {
                        message.success(t('删除成功'));
                        history.push('/alert-rules');
                      });
                    },

                    onCancel() {},
                  });
                }}
              >
                {t('删除')}
              </Button>
            )}

            <Button
              onClick={() => {
                history.push('/alert-rules');
              }}
            >
              {t('取消')}
            </Button>
          </Form.Item>
        </Space>
      </Form>
    </div>
  );
}
Example #24
Source File: operateForm.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
OperateForm: React.FC<Props> = ({ detail = {}, type }) => {
  const { t, i18n } = useTranslation();
  const { clusters: clusterList } = useSelector<RootState, CommonStoreState>((state) => state.common);
  const layout = {
    labelCol: {
      span: 24,
    },
    wrapperCol: {
      span: 24,
    },
  };
  const tailLayout = {
    labelCol: {
      span: 24,
    },
    wrapperCol: {
      span: 24,
    },
  };

  const [form] = Form.useForm(null as any);
  const history = useHistory();
  const { curBusiItem } = useSelector<RootState, CommonStoreState>((state) => state.common);
  const [btnLoading, setBtnLoading] = useState<boolean>(false);
  const [ruleModalShow, setRuleModalShow] = useState<boolean>(false);
  const [ruleCur, setRuleCur] = useState<any>();
  const [contactList, setInitContactList] = useState([]);
  const [littleAffect, setLittleAffect] = useState(true);
  const [notifyGroups, setNotifyGroups] = useState<any[]>([]);

  useEffect(() => {
    getNotifyChannel();
    getGroups('');
  }, []);

  useEffect(() => {
    setRuleCur({
      id: detail.rule_id || 0,
      name: detail.rule_name,
    });
  }, [detail.rule_id]);

  const notifyGroupsOptions = (detail.user_groups ? detail.user_groups.filter((item) => !notifyGroups.find((i) => item.id === i.id)) : []).concat(notifyGroups).map((ng: any) => (
    <Option value={String(ng.id)} key={ng.id}>
      {ng.name}
    </Option>
  ));

  const getNotifyChannel = async () => {
    const res = await getNotifiesList();
    let contactList = res || [];
    setInitContactList(contactList);
  };

  const getGroups = async (str) => {
    const res = await getTeamInfoList({ query: str });
    const data = res.dat || res;
    setNotifyGroups(data || []);
  };

  const debounceFetcher = useCallback(_.debounce(getGroups, 800), []);

  const onFinish = (values) => {
    setBtnLoading(true);
    const tags = values?.tags?.map((item) => {
      return {
        ...item,
        value: Array.isArray(item.value) ? item.value.join(' ') : item.value,
      };
    });
    const params = {
      ...values,
      tags,
      redefine_severity: values.redefine_severity ? 1 : 0,
      redefine_channels: values.redefine_channels ? 1 : 0,
      rule_id: ruleCur.id,
      user_group_ids: values.user_group_ids ? values.user_group_ids.join(' ') : '',
      new_channels: values.new_channels ? values.new_channels.join(' ') : '',
    };
    if (type === 1) {
      editSubscribe([{ ...params, id: detail.id }], curBusiItem.id)
        .then((_) => {
          message.success(t('编辑订阅规则成功'));
          history.push('/alert-subscribes');
        })
        .finally(() => {
          setBtnLoading(false);
        });
    } else {
      addSubscribe(params, curBusiItem.id)
        .then((_) => {
          message.success(t('新建订阅规则成功'));
          history.push('/alert-subscribes');
        })
        .finally(() => {
          setBtnLoading(false);
        });
    }
  };

  const onFinishFailed = () => {
    setBtnLoading(false);
  };

  const chooseRule = () => {
    setRuleModalShow(true);
  };

  const subscribeRule = (val) => {
    setRuleModalShow(false);
    setRuleCur(val);
    form.setFieldsValue({
      rile_id: val.id || 0,
    });
  };

  return (
    <>
      <div className='operate-form-index' id={littleAffect ? 'littleAffect' : ''}>
        <Form
          form={form}
          {...layout}
          layout='vertical'
          className='operate-form'
          onFinish={onFinish}
          onFinishFailed={onFinishFailed}
          initialValues={{
            ...detail,
            cluster: clusterList[0] || 'Default',
            redefine_severity: detail?.redefine_severity ? true : false,
            redefine_channels: detail?.redefine_channels ? true : false,
            user_group_ids: detail?.user_group_ids ? detail?.user_group_ids?.split(' ') : [],
            new_channels: detail?.new_channels?.split(' '),
          }}
        >
          <Card>
            <Form.Item
              label={t('生效集群:')}
              name='cluster'
              rules={[
                {
                  required: true,
                  message: t('生效集群不能为空'),
                },
              ]}
            >
              <Select suffixIcon={<CaretDownOutlined />}>
                {clusterList?.map((item, index) => (
                  <Option value={item} key={index}>
                    {item}
                  </Option>
                ))}
              </Select>
            </Form.Item>
            <Form.Item label={t('订阅告警规则:')}>
              {!!ruleCur?.id && (
                <Button
                  type='primary'
                  ghost
                  style={{ marginRight: '8px' }}
                  onClick={() => {
                    ruleCur?.id && history.push(`/alert-rules/edit/${ruleCur?.id}`);
                  }}
                >
                  {ruleCur?.name}
                </Button>
              )}

              <EditOutlined style={{ cursor: 'pointer', fontSize: '18px' }} onClick={chooseRule} />
              {!!ruleCur?.id && <DeleteOutlined style={{ cursor: 'pointer', fontSize: '18px', marginLeft: 5 }} onClick={() => subscribeRule({})} />}
            </Form.Item>
            <Row gutter={[10, 10]} style={{ marginBottom: '8px' }}>
              <Col span={5}>
                {t('订阅事件标签Key:')}
                <Tooltip title={t(`这里的标签是指告警事件的标签,通过如下标签匹配规则过滤告警事件`)}>
                  <QuestionCircleFilled />
                </Tooltip>
              </Col>
              <Col span={3}>{t('运算符:')}</Col>
              <Col span={16}>{t('标签Value:')}</Col>
            </Row>
            <Form.List name='tags' initialValue={[{}]}>
              {(fields, { add, remove }) => (
                <>
                  {fields.map((field, index) => (
                    <TagItem field={field} fields={fields} key={index} remove={remove} add={add} form={form} />
                  ))}
                  <Form.Item>
                    <PlusCircleOutlined className='control-icon-normal' onClick={() => add()} />
                  </Form.Item>
                </>
              )}
            </Form.List>
            <Form.Item label={t('告警级别:')} name='redefine_severity' valuePropName='checked'>
              <Checkbox
                value={1}
                style={{ lineHeight: '32px' }}
                onChange={(e) => {
                  form.setFieldsValue({
                    redefine_severity: e.target.checked ? 1 : 0,
                  });
                  setLittleAffect(!littleAffect);
                }}
              >
                {t('重新定义')}
              </Checkbox>
            </Form.Item>
            <Form.Item label={t('新的告警级别:')} name='new_severity' initialValue={2} style={{ display: form.getFieldValue('redefine_severity') ? 'block' : 'none' }}>
              <Radio.Group>
                <Radio key={1} value={1}>
                  {t('一级报警')}
                </Radio>
                <Radio key={2} value={2}>
                  {t('二级报警')}
                </Radio>
                <Radio key={3} value={3}>
                  {t('三级报警')}
                </Radio>
              </Radio.Group>
            </Form.Item>

            <Form.Item label={t('通知媒介:')} name='redefine_channels' valuePropName='checked'>
              <Checkbox
                value={1}
                style={{ lineHeight: '32px' }}
                onChange={(e) => {
                  form.setFieldsValue({
                    redefine_channels: e.target.checked ? 1 : 0,
                  });
                  setLittleAffect(!littleAffect);
                }}
              >
                {t('重新定义')}
              </Checkbox>
            </Form.Item>
            <Form.Item label={t('新的通知媒介:')} name='new_channels' style={{ display: form.getFieldValue('redefine_channels') ? 'block' : 'none' }}>
              <Checkbox.Group>
                {contactList.map((c: { key: string; label: string }) => (
                  <Checkbox value={c.key} key={c.label}>
                    {c.label}
                  </Checkbox>
                ))}
              </Checkbox.Group>
            </Form.Item>

            <Form.Item label={t('订阅告警接收组:')} name='user_group_ids' rules={[{ required: true, message: t('告警接收组不能为空') }]}>
              <Select mode='multiple' showSearch optionFilterProp='children' filterOption={false} onSearch={(e) => debounceFetcher(e)} onBlur={() => getGroups('')}>
                {notifyGroupsOptions}
              </Select>
            </Form.Item>
            <Form.Item {...tailLayout}>
              <Button type='primary' htmlType='submit' style={{ marginRight: '8px' }}>
                {type === 1 ? t('编辑') : type === 2 ? t('克隆') : t('创建')}
              </Button>
              {type === 1 && (
                <Button
                  danger
                  style={{ marginRight: '8px' }}
                  onClick={() => {
                    Modal.confirm({
                      title: t('是否删除该告警规则?'),
                      onOk: () => {
                        detail?.id &&
                          deleteSubscribes({ ids: [detail.id] }, curBusiItem.id).then(() => {
                            message.success(t('删除成功'));
                            history.push('/alert-subscribes');
                          });
                      },

                      onCancel() {},
                    });
                  }}
                >
                  {t('删除')}
                </Button>
              )}
              <Button onClick={() => window.history.back()}>{t('取消')}</Button>
            </Form.Item>
          </Card>
        </Form>
        <RuleModal
          visible={ruleModalShow}
          ruleModalClose={() => {
            setRuleModalShow(false);
          }}
          subscribe={subscribeRule}
        />
      </div>
    </>
  );
}
Example #25
Source File: index.tsx    From condo with MIT License 4 votes vote down vote up
ContactsEditor: React.FC<IContactEditorProps> = (props) => {
    const intl = useIntl()
    const FullNameLabel = intl.formatMessage({ id: 'contact.Contact.ContactsEditor.Name' })
    const PhoneLabel = intl.formatMessage({ id: 'contact.Contact.ContactsEditor.Phone' })
    const AddNewContactLabel = intl.formatMessage({ id: 'contact.Contact.ContactsEditor.AddNewContact' })
    const AnotherContactLabel = intl.formatMessage({ id: 'contact.Contact.ContactsEditor.AnotherContact' })
    const CannotCreateContactMessage = intl.formatMessage({ id: 'contact.Contact.ContactsEditor.CannotCreateContact' })
    const TicketFromResidentMessage = intl.formatMessage({ id: 'pages.condo.ticket.title.TicketFromResident' })
    const TicketNotFromResidentMessage = intl.formatMessage({ id: 'pages.condo.ticket.title.TicketNotFromResident' })

    const { form, fields, value: initialValue, onChange, organization, role, property, unitName, unitType, allowLandLine } = props
    const isNotContact = useMemo(() => !initialValue.id && initialValue.phone, [initialValue.id, initialValue.phone])

    const [selectedContact, setSelectedContact] = useState(null)
    const [value, setValue] = useState(initialValue)
    const [editableFieldsChecked, setEditableFieldsChecked] = useState(false)
    // We need this to keep manually typed information preserved between rerenders
    // with different set of prefetched contacts. For example, when a different unitName is selected,
    // manually typed information should not be lost.
    const [manuallyTypedContact, setManuallyTypedContact] = useState({ id: undefined, name: '', phone: '' })
    const [displayEditableContactFields, setDisplayEditableContactFields] = useState(false)
    const [isInitialContactsLoaded, setIsInitialContactsLoaded] = useState<boolean>()
    const [initialContacts, setInitialContacts] = useState<IContactUIState[]>([])

    const initialContactsQuery = useMemo(() => ({
        organization: { id: organization },
        property: { id: property ? property : null },
        unitName: unitName ? unitName : undefined,
        unitType: unitType ? unitType : undefined,
    }), [organization, property, unitName, unitType])

    const initialEmployeesQuery = useMemo(() => ({
        organization: { id: organization },
    }), [organization])

    const {
        objs: fetchedContacts,
        loading: contactsLoading,
        error,
        refetch: refetchContacts,
    } = Contact.useObjects({
        where: initialContactsQuery,
        first: 100,
    })

    const {
        objs: fetchedEmployees,
        refetch: refetchEmployees,
    } = OrganizationEmployee.useObjects({
        where: initialEmployeesQuery,
        first: 100,
    })

    const { phoneValidator } = useValidations({ allowLandLine })
    const validations = {
        phone: [phoneValidator],
    }

    useEffect(() => {
        if (!isInitialContactsLoaded && !contactsLoading) {
            setInitialContacts(fetchedContacts)
            setIsInitialContactsLoaded(true)
        }
    }, [contactsLoading, fetchedContacts, isInitialContactsLoaded])

    // It's not enough to have `value` props of `Input` set.
    useEffect(() => {
        if (initialValue) {
            form.setFieldsValue({
                [fields.id]: initialValue.id,
                [fields.name]: initialValue.name,
                [fields.phone]: initialValue.phone,
            })
        }
    }, [])

    // When `unitName` was changed from outside, selection is not relevant anymore
    useEffect(() => {
        setIsInitialContactsLoaded(false)
        setSelectedContact(null)
        setManuallyTypedContact(null)
    }, [unitName, unitType])

    const handleClickOnPlusButton = () => {
        setDisplayEditableContactFields(true)
        setSelectedContact(null)
        setEditableFieldsChecked(true)
    }

    const handleClickOnMinusButton = () => {
        setDisplayEditableContactFields(false)
        setSelectedContact(fetchedContacts[0])
        setEditableFieldsChecked(false)
    }

    const triggerOnChange = (contact: ContactValue, isNew) => {
        form.setFieldsValue({
            [fields.id]: contact.id,
            [fields.name]: contact.name,
            [fields.phone]: contact.phone,
        })
        setValue(contact)
        setSelectedContact(contact)
        onChange && onChange(contact, isNew)
    }

    const handleSelectContact = (contact) => {
        setSelectedContact(contact)
        setEditableFieldsChecked(false)
        triggerOnChange(contact, false)
    }

    const handleChangeContact = debounce((contact) => {
        // User can manually type phone and name, that will match already existing contact,
        // so, it should be connected with ticket
        const fetchedContact = find(fetchedContacts, { ...contact, unitName: unitName || null })
        const contactToSet = fetchedContact || contact

        triggerOnChange(contactToSet, !fetchedContact)

        setManuallyTypedContact(contact)
        setEditableFieldsChecked(true)
        setSelectedContact(null)
    }, DEBOUNCE_TIMEOUT)

    const handleSyncedFieldsChecked = () => {
        setSelectedContact(null)
        setEditableFieldsChecked(true)

        if (isNotContact) {
            handleChangeContact(initialValue)
        }
    }

    const handleChangeEmployee = debounce((contact) => {
        form.setFieldsValue({
            [fields.id]: null,
            [fields.name]: contact.name,
            [fields.phone]: contact.phone,
        })
        const employeeContact = { ...contact, id: null }

        setValue(employeeContact)
        setManuallyTypedContact(employeeContact)
        setEditableFieldsChecked(true)

        onChange && onChange(employeeContact, false)
    }, DEBOUNCE_TIMEOUT)

    const initialValueIsPresentedInFetchedContacts = useMemo(() => initialContacts
        && initialValue && initialValue.name && initialValue.phone &&
        initialContacts.find(contact => contact.name === initialValue.name && contact.phone === initialValue.phone),
    [initialContacts, initialValue])

    const isContactSameAsInitial = (contact) => (
        initialValue && initialValue.name === contact.name && initialValue.phone === contact.phone && initialValue.id === contact.id
    )

    const isContactSelected = useCallback((contact) => {
        if (selectedContact) return selectedContact.id === contact.id

        if (!editableFieldsChecked) {
            if (isContactSameAsInitial(contact)) return true
        }

        return false
    }, [editableFieldsChecked, isContactSameAsInitial, selectedContact])

    const contactOptions = useMemo(() => initialContacts.map((contact) => (
        <ContactOption
            key={contact.id}
            contact={contact}
            onSelect={handleSelectContact}
            selected={isContactSelected(contact)}
        />
    )), [handleSelectContact, initialContacts, isContactSelected])

    const handleTabChange = useCallback((tab) => {
        setSelectedContact(null)
        setEditableFieldsChecked(false)

        if (tab === CONTACT_EDITOR_TABS.NOT_FROM_RESIDENT) {
            handleChangeEmployee(value)
        }
    }, [handleChangeEmployee, value])

    const className = props.disabled ? 'disabled' : ''

    if (error) {
        console.warn(error)
        throw error
    }

    return (
        <Col span={24}>
            <ContactsInfoFocusContainer className={className}>
                <Tabs
                    defaultActiveKey={isNotContact ? CONTACT_EDITOR_TABS.NOT_FROM_RESIDENT : CONTACT_EDITOR_TABS.FROM_RESIDENT}
                    style={TABS_STYLE}
                    onChange={handleTabChange}
                >
                    <TabPane tab={TicketFromResidentMessage} key={CONTACT_EDITOR_TABS.FROM_RESIDENT}>
                        <Row gutter={TAB_PANE_ROW_GUTTERS}>
                            <Labels
                                left={PhoneLabel}
                                right={FullNameLabel}
                            />
                            {isEmpty(initialContacts) || !unitName ? (
                                <ContactSyncedAutocompleteFields
                                    refetch={refetchContacts}
                                    initialQuery={initialContactsQuery}
                                    initialValue={initialValue.id ? initialValue : manuallyTypedContact}
                                    onChange={handleChangeContact}
                                    contacts={fetchedContacts}
                                />
                            ) : (
                                <>
                                    {contactOptions}
                                    <>
                                        {(displayEditableContactFields || (initialValue.id && !initialValueIsPresentedInFetchedContacts)) ? (
                                            <>
                                                <Labels
                                                    left={AnotherContactLabel}
                                                />
                                                <ContactSyncedAutocompleteFields
                                                    initialQuery={initialContactsQuery}
                                                    refetch={refetchContacts}
                                                    initialValue={initialValue.id ? initialValue : manuallyTypedContact}
                                                    onChange={handleChangeContact}
                                                    onChecked={handleSyncedFieldsChecked}
                                                    checked={editableFieldsChecked}
                                                    contacts={fetchedContacts}
                                                    displayMinusButton={true}
                                                    onClickMinusButton={handleClickOnMinusButton}
                                                />
                                                {(!get(role, 'canManageContacts')) && (
                                                    <Col span={24}>
                                                        <ErrorsWrapper>
                                                            {CannotCreateContactMessage}
                                                        </ErrorsWrapper>
                                                    </Col>
                                                )}
                                            </>
                                        ) : (
                                            <Col span={24}>
                                                <Button
                                                    type="link"
                                                    style={BUTTON_STYLE}
                                                    onClick={handleClickOnPlusButton}
                                                    icon={<PlusCircleOutlined style={BUTTON_ICON_STYLE}/>}
                                                >
                                                    {AddNewContactLabel}
                                                </Button>
                                            </Col>
                                        )}
                                    </>
                                </>
                            )}
                        </Row>
                    </TabPane>
                    <TabPane
                        tab={TicketNotFromResidentMessage}
                        key={CONTACT_EDITOR_TABS.NOT_FROM_RESIDENT}
                    >
                        <Row gutter={TAB_PANE_ROW_GUTTERS}>
                            <Labels
                                left={PhoneLabel}
                                right={FullNameLabel}
                            />
                            <ContactSyncedAutocompleteFields
                                initialQuery={initialEmployeesQuery}
                                refetch={refetchEmployees}
                                initialValue={!initialValue.id ? initialValue : manuallyTypedContact}
                                onChange={handleChangeEmployee}
                                contacts={fetchedEmployees}
                            />
                        </Row>
                    </TabPane>
                </Tabs>
            </ContactsInfoFocusContainer>

            {/*
                    This is a place for items of external form, this component is embedded into.
                    Why not to use them in place of actual inputs?
                    Because we have many inputs ;)
                    1. Input pairs, imitating radio group for select
                    2. Text inputs for manual typing
                    Logic of displaying `Form.Item`, depending on what is currently selected:
                    radio-like pair, or manual input pair, — will be complex.
                    The simplest solution, i currently know, — is to keep it in one place.
                    So, we use hidden inputs here, but reveal validation errors.
                */}
            <Row gutter={TAB_PANE_ROW_GUTTERS}>
                <Col span={10}>
                    <Form.Item name={fields.id} hidden>
                        <Input value={get(value, 'id')}/>
                    </Form.Item>
                    <ErrorContainerOfHiddenControl>
                        <Form.Item
                            name={fields.phone}
                            validateFirst
                            rules={validations.phone}>
                            <Input value={get(value, 'phone')}/>
                        </Form.Item>
                    </ErrorContainerOfHiddenControl>
                </Col>
                <Col span={10}>
                    <ErrorContainerOfHiddenControl>
                        <Form.Item name={fields.name}>
                            <Input value={get(value, 'name')}/>
                        </Form.Item>
                    </ErrorContainerOfHiddenControl>
                </Col>
                <Col span={2}/>
                <Col span={2}/>
            </Row>
        </Col>
    )
}
Example #26
Source File: index.tsx    From dashboard with Apache License 2.0 4 votes vote down vote up
AutoReply: React.FC<AutoReplyProps> = (props) => {
  const {welcomeMsg, setWelcomeMsg, isFetchDone} = props;
  const [modalVisible, setModalVisible] = useState(false);
  const [attachments, setAttachments] = useState<Attachment[]>([]);
  const [currentIndex, setCurrentIndex] = useState<number>(0);
  const [currentMode, setCurrentMode] = useState<MsgType>('image');
  const [linkFetching, setLinkFetching] = useState(false);
  const [content, setContent] = useState('');
  const contentRef = useRef<React.RefObject<HTMLElement>>();
  const imageModalFormRef = useRef<FormInstance>();
  const linkModalFormRef = useRef<FormInstance>();
  const miniAppModalFormRef = useRef<FormInstance>();

  const UploadFileFn = async (req: UploadRequestOption, ref: MutableRefObject<any | undefined>, inputName: string) => {
    const file = req.file as File;
    if (!file.name) {
      message.error('非法参数');
      return;
    }

    const hide = message.loading('上传中');
    try {
      const res = await GetSignedURL(file.name)
      const data = res.data as GetSignedURLResult
      if (res.code === 0) {
        const uploadRes = (await fetch(data.upload_url, {
          method: 'PUT',
          body: file
        }));
        hide();
        if (uploadRes.ok && ref) {
          ref.current?.setFieldsValue({[inputName]: data.download_url});
          return;
        }

        message.error('上传图片失败');

        return;
      }

      hide();
      message.error('获取上传地址失败');
      return;

    } catch (e) {
      message.error('上传图片失败');
      console.log(e);
    }
  };

  useEffect(() => {
    const formData = itemDataToFormData(welcomeMsg);
    setAttachments(formData.attachments || []);
    setContent(formData.text || '');
  }, [isFetchDone]);

  useEffect(() => {
    setWelcomeMsg({
      text: content || '',
      attachments: attachments || [],
    });
  }, [content, attachments]);

  return (
    <>
      <div className={styles.replyEditor}>
        <div className={'preview-container'}>
          <div className={styles.replyEditorPreview}>
            <img src={phoneImage} className='bg'/>
            <div className='content'>
              <ul className='reply-list'>
                {content && (
                  <li><img
                    src={avatarDefault}/>
                    <div className='msg text' dangerouslySetInnerHTML={{__html: content}}/>
                  </li>
                )}
                {attachments && attachments.length > 0 && (
                  attachments.map((attachment) => {
                    if (attachment.msgtype === 'image') {
                      return (
                        <li key={attachment.id}>
                          <img src={avatarDefault}/>
                          <div className={`msg image`}>
                            <img src={attachment.image?.pic_url}/>
                          </div>
                        </li>
                      );
                    }

                    if (attachment.msgtype === 'link') {
                      return (
                        <li key={attachment.id}>
                          <img src={avatarDefault}/>
                          <div className='msg link'><p className='title'>{attachment.link?.title}</p>
                            <div className='link-inner'><p
                              className='desc'>{attachment.link?.desc}</p>
                              <img src={attachment.link?.picurl}/>
                            </div>
                          </div>
                        </li>
                      );
                    }

                    if (attachment.msgtype === 'miniprogram') {
                      return (
                        <li key={attachment.id}>
                          <img src={avatarDefault}/>
                          <div className='msg miniprogram'>
                            <p className='m-title'>
                              <IconFont
                                type={'icon-weixin-mini-app'}
                                style={{marginRight: 4, fontSize: 14}}
                              />
                              {attachment.miniprogram?.title}
                            </p>
                            <img src={attachment.miniprogram?.pic_media_id}/>
                            <p className='l-title'>
                              <IconFont type={'icon-weixin-mini-app'} style={{marginRight: 4}}/>
                              小程序
                            </p>
                          </div>
                        </li>
                      );
                    }

                    return '';
                  })
                )}
              </ul>
            </div>
          </div>
        </div>

        <div className='text-area-container'>
          <div className={styles.msgTextareaContainer} style={{border: 'none'}}>
            {props.enableQuickInsert && (
              <div className='insert-btn '>
                    <span
                      className='clickable no-select'
                      onClick={() => {
                        setContent(`${content}[客户昵称]`);
                      }}
                    >[插入客户昵称]</span>
              </div>
            )}
            <div className='textarea-container '>
              <ContentEditable
                // @ts-ignore
                innerRef={contentRef}
                onKeyDown={(event) => {
                  if (event.key === 'Enter') {
                    document.execCommand('insertLineBreak');
                    event.preventDefault();
                  }
                }}
                className={'textarea'}
                html={content}
                onChange={(e) => {
                  setContent(e.target.value);
                }}/>
              <div className='flex-row align-side'>
                <p className='text-cnt'>{content.length}/600</p>
              </div>
            </div>
          </div>
        </div>
        <div className='option-area-container'>
          {attachments && attachments.length > 0 && (
            <ReactSortable handle={'.draggable-button'} tag='ul' className={'select-msg-options'} list={attachments} setList={setAttachments}>
              {attachments.map((attachment, index) => (
                <li key={attachment.id} className='flex-row'>
                      <span>
                        <MinusCircleOutlined
                          onClick={() => {
                            const items = [...attachments];
                            items.splice(index, 1);
                            setAttachments(items);
                          }}
                        />
                        【{msgTypes[attachment.msgtype]}】:
                        <span
                          className='col-1'>{attachment?.name}</span>
                      </span>
                  <span className='d-action-container'>
                      <EditOutlined
                        onClick={() => {
                          setCurrentMode(attachment.msgtype);
                          imageModalFormRef.current?.setFieldsValue(attachment.image);
                          linkModalFormRef.current?.setFieldsValue(attachment.link);
                          miniAppModalFormRef.current?.setFieldsValue(attachment.miniprogram);
                          setCurrentIndex(index);
                          setModalVisible(true);
                        }}
                      />
                      <DragOutlined
                        className={'draggable-button'}
                        style={{cursor: 'grabbing'}}
                      />
                    </span>
                </li>
              ))}
            </ReactSortable>
          )}
          <div className='option-container'>
            <Dropdown
              placement='topLeft'
              trigger={['click']}
              overlay={(
                <Menu style={{minWidth: 120}}>
                  <Menu.Item
                    key={'image'}
                    icon={<FileImageOutlined/>}
                    onClick={() => {
                      setCurrentMode('image');
                      setCurrentIndex(attachments.length);
                      imageModalFormRef.current?.resetFields();
                      setModalVisible(true);
                    }}
                  >
                    图片
                  </Menu.Item>
                  <Menu.Item
                    key={'link'}
                    icon={<LinkOutlined/>}
                    onClick={() => {
                      setCurrentMode('link');
                      setCurrentIndex(attachments.length);
                      setModalVisible(true);
                    }}
                  >
                    链接
                  </Menu.Item>
                  <Menu.Item
                    key={'miniApp'}
                    icon={<IconFont type={'icon-weixin-mini-app'}/>}
                    onClick={() => {
                      setCurrentMode('miniprogram');
                      setCurrentIndex(attachments.length);
                      setModalVisible(true);
                    }}
                  >
                    小程序
                  </Menu.Item>
                </Menu>
              )}
            >
              <a className='ant-dropdown-link' onClick={e => e.preventDefault()}>
                <PlusCircleOutlined/> 添加附件
              </a>
            </Dropdown>
          </div>
        </div>
      </div>

      <ModalForm
        formRef={imageModalFormRef}
        className={'dialog from-item-label-100w'}
        layout={'horizontal'}
        width={'560px'}
        visible={currentMode === 'image' && modalVisible}
        onVisibleChange={setModalVisible}
        onFinish={async (params: { title: string, pic_url: string, msgtype: MsgType }) => {
          attachments[currentIndex] = {
            id: new Date().getTime().toString(),
            msgtype: params.msgtype,
            name: params.title,
            image: {...params},
          };
          setAttachments(attachments);
          return true;
        }}
      >
        <h2 className='dialog-title'> 添加图片附件 </h2>
        <ProForm.Item initialValue={'image'} name={'msgtype'} noStyle={true}>
          <input type={'hidden'}/>
        </ProForm.Item>
        <ProFormText
          name='title'
          label='图片名称'
          placeholder={'请输入图片名称'}
          width='md'
          rules={[
            {
              required: true,
              message: '请输入图片名称!',
            },
          ]}
        />
        <Form.Item
          label='上传图片'
          name='pic_url'
          rules={[
            {
              required: true,
              message: '请上传图片!',
            },
          ]}
        >
          <ImageUploader
            customRequest={async (req) => {
              await UploadFileFn(req, imageModalFormRef, 'pic_url')
            }}
          />
        </Form.Item>
      </ModalForm>


      <ModalForm
        formRef={linkModalFormRef}
        className={'dialog from-item-label-100w'}
        layout={'horizontal'}
        width={'560px'}
        visible={currentMode === 'link' && modalVisible}
        onVisibleChange={setModalVisible}
        onFinish={async (params) => {
          attachments[currentIndex] = {
            id: new Date().getTime().toString(),
            msgtype: params.msgtype,
            name: params.title,
            // @ts-ignore
            link: {...params},
          };
          setAttachments(attachments);
          return true;
        }}
      >
        <Spin spinning={linkFetching}>
          <h2 className='dialog-title'> 添加链接附件 </h2>
          <ProForm.Item initialValue={'link'} name={'msgtype'} noStyle={true}>
            <input type={'hidden'}/>
          </ProForm.Item>
          <ProFormText
            name='url'
            label='链接地址'
            width='md'
            fieldProps={{
              disabled: linkFetching,
              addonAfter: (
                <Tooltip title="点击抓取远程链接,自动填充标题,描述,图片">
                  <div
                    onClick={async () => {
                      setLinkFetching(true);
                      const res = await ParseURL(linkModalFormRef.current?.getFieldValue('url'))
                      setLinkFetching(false);
                      if (res.code !== 0) {
                        message.error(res.message);
                      } else {
                        message.success('解析链接成功');
                        linkModalFormRef?.current?.setFieldsValue({
                          customer_link_enable: 1,
                          title: res.data.title,
                          desc: res.data.desc,
                          picurl: res.data.img_url,
                        })
                      }
                    }}
                    style={{
                      cursor: "pointer",
                      width: 32,
                      height: 30,
                      display: 'flex',
                      alignItems: 'center',
                      justifyContent: 'center'
                    }}>
                    <SyncOutlined/>
                  </div>
                </Tooltip>
              )
            }}
            rules={[
              {
                required: true,
                message: '请输入链接地址',
              },
              {
                type: 'url',
                message: '请填写正确的的URL,必须是http或https开头',
              },
            ]}
          />
          <ProFormSwitch
            label={'高级设置'}
            checkedChildren='开启'
            unCheckedChildren='关闭'
            name='customer_link_enable'
            tooltip={'开启后可以自定义链接所有信息'}
          />
          <ProFormDependency name={['customer_link_enable']}>
            {({customer_link_enable}) => {
              if (customer_link_enable) {
                return (
                  <>
                    <ProFormText
                      name='title'
                      label='链接标题'
                      width='md'
                      rules={[
                        {
                          required: true,
                          message: '请输入链接标题',
                        },
                      ]}
                    />
                    <ProFormTextArea
                      name='desc'
                      label='链接描述'
                      width='md'
                    />
                    <Form.Item
                      label='链接封面'
                      name='picurl'
                      rules={[
                        {
                          required: true,
                          message: '请上传链接图片!',
                        },
                      ]}
                    >
                      <ImageUploader
                        customRequest={async (req) => {
                          await UploadFileFn(req, linkModalFormRef, 'picurl')
                        }}
                      />
                    </Form.Item>
                  </>
                );
              }
              return <></>;
            }}
          </ProFormDependency>
        </Spin>
      </ModalForm>


      <ModalForm
        formRef={miniAppModalFormRef}
        className={'dialog from-item-label-100w'}
        layout={'horizontal'}
        width={'560px'}
        labelCol={{
          md: 6,
        }}
        visible={currentMode === 'miniprogram' && modalVisible}
        onVisibleChange={setModalVisible}
        onFinish={async (params) => {
          attachments[currentIndex] = {
            id: new Date().getTime().toString(),
            msgtype: params.msgtype,
            name: params.title,
            // @ts-ignore
            miniprogram: {...params},
          };
          setAttachments(attachments);
          return true;
        }}
      >
        <h2 className='dialog-title'> 添加小程序附件 </h2>

        <Alert
          showIcon={true}
          type='info'
          message={
            '请填写企业微信后台绑定的小程序id和路径,否则会造成发送失败'
          }
          style={{marginBottom: 20}}
        />

        <ProForm.Item initialValue={'miniprogram'} name={'msgtype'} noStyle={true}>
          <input type={'hidden'}/>
        </ProForm.Item>

        <ProFormText
          name='title'
          label='小程序标题'
          width='md'
          rules={[
            {
              required: true,
              message: '请输入链接标题',
            },
          ]}
        />

        <ProFormText
          // 帮助指引
          name='app_id'
          label='小程序AppID'
          width='md'
          rules={[
            {
              required: true,
              message: '请输入小程序AppID',
            },
          ]}
        />

        <ProFormText
          name='page'
          label='小程序路径'
          width='md'
          rules={[
            {
              required: true,
              message: '请输入小程序路径',
            },
          ]}
        />

        <Form.Item
          label='小程序封面'
          name='pic_media_id'
          rules={[
            {
              required: true,
              message: '请小程序封面!',
            },
          ]}
        >
          <ImageUploader
            customRequest={async (req) => {
              await UploadFileFn(req, miniAppModalFormRef, 'pic_media_id')
            }}
          />
        </Form.Item>

      </ModalForm>

    </>
  );
}
Example #27
Source File: OrderForm.tsx    From mayoor with MIT License 4 votes vote down vote up
OrderForm: React.FC<Props> = (props) => {
	const { t } = useTranslation();
	const { currencyFormatter } = useCurrencyFormatter();

	return (
		<Formik<OrderFormValues>
			initialValues={props.initialValues}
			onSubmit={async (values, { resetForm }) => {
				await props.onSubmit(values, resetForm);
			}}
			validationSchema={getOrderValidationSchema(t)}
			enableReinitialize
		>
			{({ handleSubmit, values, handleChange, setFieldValue }) => (
				<StyledForm onSubmit={handleSubmit}>
					<Row gutter={8}>
						<Col lg={4}>
							<StyledOrderNumberWrapper>
								<FormInput
									name="number"
									label={t('Order number')}
									icon={<NumberOutlined />}
									withLabel
									type="number"
									disabled={!props.isNumberEditable}
								/>
							</StyledOrderNumberWrapper>
						</Col>
						<Col lg={7}>
							<CustomerPicker extraCustomer={props.extraCustomer} />
						</Col>
						<Col lg={6}>
							<OrderStatusSelect />
						</Col>
					</Row>
					<StyledDivider />
					<Row gutter={6}>
						<Col sm={4}>
							<StyledLabel>{t('Material')}</StyledLabel>
						</Col>
						<Col sm={7}>
							<StyledLabel>{t('Name')}</StyledLabel>
						</Col>
						<Col sm={2}>
							<StyledLabel>{t('Width')}</StyledLabel>
						</Col>
						<Col sm={2}>
							<StyledLabel>{t('Height')}</StyledLabel>
						</Col>
						<Col sm={2}>
							<StyledLabel>{t('Pieces')}</StyledLabel>
						</Col>
						<Col sm={1}></Col>
						<Col sm={3}>
							<StyledLabel>{t('Price')}</StyledLabel>
						</Col>
						<Col sm={3}>
							<StyledLabel>{t('Tax')}</StyledLabel>
						</Col>
					</Row>
					<FieldArray
						name="items"
						render={(arrayHelpers) => (
							<>
								{values.items.length > 0 &&
									values.items.map((item, index) => (
										<OrderItemField
											key={item.id || index}
											index={index}
											arrayHelpers={arrayHelpers}
										/>
									))}
								<Row>
									<Col>
										<Button
											icon={<PlusCircleOutlined />}
											onClick={() => arrayHelpers.push(dummyMaterialItem)}
										>
											{t('Add item')}
										</Button>
									</Col>
									<Col style={{ textAlign: 'right' }}>
										<Button
											icon={<CalculatorOutlined />}
											onClick={() => {
												const { totalPrice, totalTax } = calculateSummary(
													values,
												);
												setFieldValue('totalPrice', totalPrice);
												setFieldValue('totalTax', totalTax);
											}}
											style={{ marginLeft: 10 }}
											data-test-id="order-sum-items-button"
										>
											{t('Sum items')}
										</Button>
									</Col>
								</Row>
							</>
						)}
					/>
					<Row gutter={8} style={{ marginTop: 15 }}>
						<Col sm={10}>
							<StyledFormItem>
								<StyledLabel>{t('Note')}</StyledLabel>
								<Input.TextArea
									rows={4}
									name="note"
									placeholder={t('note_placeholder')}
									onChange={handleChange}
									data-test-id="order-form-note"
									value={values.note || ''}
								/>
							</StyledFormItem>
						</Col>
						<Col sm={8}>
							<Row>
								<Col sm={18} offset={3}>
									<UrgentSlider />
								</Col>
							</Row>
						</Col>
						<Col sm={6}>
							<OrderSummaryWrapper>
								<FormInput
									name="totalPrice"
									label={t('Total price')}
									type="number"
									suffix={CURRENCY_SUFFIX}
									withLabel
								/>
								<FormInput
									name="totalTax"
									label={t('Total tax')}
									type="number"
									suffix={CURRENCY_SUFFIX}
									withLabel
								/>
								{!!getTotalPriceIncludingTax(values) && (
									<div>
										<StyledLabel>{t('Total price including tax')}</StyledLabel>
										<span>
											{currencyFormatter(
												getTotalPriceIncludingTax(values) || 0,
											)}
										</span>
									</div>
								)}
							</OrderSummaryWrapper>
						</Col>
					</Row>
					{props.submitButton}
				</StyledForm>
			)}
		</Formik>
	);
}
Example #28
Source File: info.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
export default function Info() {
  const { t } = useTranslation();
  const [form] = Form.useForm();
  const [isModalVisible, setIsModalVisible] = useState(false);
  const [contactsList, setContactsList] = useState<ContactsItem[]>([]);
  let { profile } = useSelector<RootState, accountStoreState>((state) => state.account);
  const [selectAvatar, setSelectAvatar] = useState<string>(profile.portrait || '/image/avatar1.png');
  const [customAvatar, setCustomAvatar] = useState('');
  const dispatch = useDispatch();
  useEffect(() => {
    const { id, nickname, email, phone, contacts, portrait } = profile;
    form.setFieldsValue({
      nickname,
      email,
      phone,
      contacts,
    });
    if (portrait.startsWith('http')) {
      setCustomAvatar(portrait);
    }
  }, [profile]);
  useEffect(() => {
    getNotifyChannels().then((data: Array<ContactsItem>) => {
      setContactsList(data);
    });
  }, []);

  const handleSubmit = async () => {
    try {
      await form.validateFields();
      updateProfile();
    } catch (err) {
      console.log(t('输入有误'), err);
    }
  };

  const handleOk = () => {
    if (customAvatar) {
      if (!customAvatar.startsWith('http')) {
        message.error(t('自定义头像需以http开头'));
        return;
      }

      fetch(customAvatar, { mode: 'no-cors' })
        .then((res) => {
          setIsModalVisible(false);
          handleSubmit();
        })
        .catch((err) => {
          message.error(t('自定义头像') + err);
        });
    } else {
      setIsModalVisible(false);
      handleSubmit();
    }
  };

  const handleCancel = () => {
    setIsModalVisible(false);
  };

  const updateProfile = () => {
    const { nickname, email, phone, moreContacts } = form.getFieldsValue();
    let { contacts } = form.getFieldsValue();

    if (moreContacts && moreContacts.length > 0) {
      moreContacts.forEach((item) => {
        const { key, value } = item;

        if (key && value) {
          if (contacts) {
            contacts[key] = value;
          } else {
            contacts = {
              [key]: value,
            };
          }
        }
      });
    }

    for (let key in contacts) {
      if (!contacts[key]) {
        delete contacts[key];
      }
    }

    dispatch({
      type: 'account/updateProfile',
      data: {
        ...profile,
        portrait: customAvatar || selectAvatar,
        nickname,
        email,
        phone,
        contacts,
      },
    });
    message.success(t('信息保存成功'));
  };

  const avatarList = new Array(8).fill(0).map((_, i) => i + 1);

  const handleImgClick = (i) => {
    setSelectAvatar(`/image/avatar${i}.png`);
  };

  return (
    <>
      <Form form={form} layout='vertical'>
        <Row
          gutter={16}
          style={{
            marginBottom: '24px',
          }}
        >
          <Col span={20}>
            <Row
              gutter={16}
              style={{
                marginBottom: '24px',
              }}
            >
              <Col span={4}>
                <div>
                  <label>{t('用户名')}:</label>
                  <span>{profile.username}</span>
                </div>
              </Col>
              <Col span={4}>
                <div>
                  <label>{t('角色')}:</label>
                  <span>{profile.roles.join(', ')}</span>
                </div>
              </Col>
            </Row>
            <Form.Item label={<span>{t('显示名')}:</span>} name='nickname'>
              <Input placeholder={t('请输入显示名')} />
            </Form.Item>
            <Form.Item label={<span>{t('邮箱')}:</span>} name='email'>
              <Input placeholder={t('请输入邮箱')} />
            </Form.Item>
            <Form.Item label={<span>{t('手机')}:</span>} name='phone'>
              <Input placeholder={t('请输入手机号')} />
            </Form.Item>

            {profile.contacts &&
              Object.keys(profile.contacts)
                .sort()
                .map((key, i) => {
                  let contact = contactsList.find((item) => item.key === key);
                  return (
                    <>
                      {contact ? (
                        <Form.Item label={contact.label + ':'} name={['contacts', key]} key={i}>
                          <Input placeholder={`${t('请输入')}${key}`} />
                        </Form.Item>
                      ) : null}
                    </>
                  );
                })}

            <Form.Item label={t('更多联系方式')}>
              <Form.List name='moreContacts'>
                {(fields, { add, remove }) => (
                  <>
                    {fields.map(({ key, name, fieldKey, ...restField }) => (
                      <Space
                        key={key}
                        style={{
                          display: 'flex',
                        }}
                        align='baseline'
                      >
                        <Form.Item
                          style={{
                            width: '180px',
                          }}
                          {...restField}
                          name={[name, 'key']}
                          fieldKey={[fieldKey, 'key']}
                          rules={[
                            {
                              required: true,
                              message: t('联系方式不能为空'),
                            },
                          ]}
                        >
                          <Select suffixIcon={<CaretDownOutlined />} placeholder={t('请选择联系方式')}>
                            {contactsList.map((item, index) => (
                              <Option value={item.key} key={index}>
                                {item.label}
                              </Option>
                            ))}
                          </Select>
                        </Form.Item>
                        <Form.Item
                          {...restField}
                          style={{
                            width: '330px',
                          }}
                          name={[name, 'value']}
                          fieldKey={[fieldKey, 'value']}
                          rules={[
                            {
                              required: true,
                              message: t('值不能为空'),
                            },
                          ]}
                        >
                          <Input placeholder={t('请输入值')} />
                        </Form.Item>
                        <MinusCircleOutlined className='control-icon-normal' onClick={() => remove(name)} />
                      </Space>
                    ))}
                    <PlusCircleOutlined className='control-icon-normal' onClick={() => add()} />
                  </>
                )}
              </Form.List>
            </Form.Item>

            <Form.Item>
              <Button type='primary' onClick={handleSubmit}>
                {t('确认修改')}
              </Button>
            </Form.Item>
          </Col>
          <Col span={4}>
            <div className='avatar'>
              <img src={profile.portrait || '/image/avatar1.png'} />
              <Button type='primary' className='update-avatar' onClick={() => setIsModalVisible(true)}>
                {t('更换头像')}
              </Button>
            </div>
          </Col>
        </Row>
      </Form>
      <Modal title={t('更换头像')} visible={isModalVisible} onOk={handleOk} onCancel={handleCancel} wrapClassName='avatar-modal'>
        <div className='avatar-content'>
          {avatarList.map((i) => {
            return (
              <div key={i} className={`/image/avatar${i}.png` === selectAvatar ? 'avatar active' : 'avatar'} onClick={() => handleImgClick(i)}>
                <img src={`/image/avatar${i}.png`} />
              </div>
            );
          })}
        </div>
        <Input addonBefore={<span>{t('头像URL')}:</span>} onChange={(e) => setCustomAvatar(e.target.value)} value={customAvatar} />
      </Modal>
    </>
  );
}
Example #29
Source File: GraphConfigInner.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
render() {
    const { data, onChange } = this.props;
    const { now, start, end, comparison } = data;
    const handleAggrFuncClick = (e) => {
      this.handleAggrFuncChange(e.key);
    };
    const aggrFuncMenu = (
      <Menu onClick={handleAggrFuncClick} selectedKeys={[this.state.curAggrFunc]}>
        <Menu.Item key='sum'>sum</Menu.Item>
        <Menu.Item key='avg'>avg</Menu.Item>
        <Menu.Item key='max'>max</Menu.Item>
        <Menu.Item key='min'>min</Menu.Item>
      </Menu>
    );

    const calcFuncMenu = (
      <Menu onClick={(e) => this.handleCalcFuncChange(e.key === 'clear' ? '' : e.key)} selectedKeys={[this.state.calcFunc]}>
        <Menu.Item key='rate_1m'>rate_1m</Menu.Item>
        <Menu.Item key='rate_5m'>rate_5m</Menu.Item>
        <Menu.Item key='increase_1m'>increase_1m</Menu.Item>
        <Menu.Item key='increase_5m'>increase_5m</Menu.Item>
        <Menu.Divider></Menu.Divider>
        <Menu.Item key='clear'>clear</Menu.Item>
      </Menu>
    );

    const handleAggrGroupsClick = (ag) => {
      const index = this.state.curAggrGroups.findIndex((cag) => cag === ag.key);
      let newCurAggrGroups;
      if (index === -1) {
        newCurAggrGroups = [...this.state.curAggrGroups, ag.key];
        this.setState({
          curAggrGroups: newCurAggrGroups,
        });
      } else {
        let curComparisonCopy = [...this.state.curAggrGroups];
        curComparisonCopy.splice(index, 1);
        newCurAggrGroups = curComparisonCopy;
        this.setState({
          curAggrGroups: curComparisonCopy,
        });
      }
      this.handleAggrGroupsChange(newCurAggrGroups);
    };

    const aggrGroupsMenu = (
      <Menu onClick={handleAggrGroupsClick} selectedKeys={this.state.curAggrGroups}>
        {this.state.aggrGroups
          .filter((n) => n !== '__name__')
          .map((ag) => (
            <Menu.Item key={ag}>{ag}</Menu.Item>
          ))}
      </Menu>
    );

    const handleDeleteAggrGroupClick = (ag) => {
      let newCurAggrGroups = [...this.state.curAggrGroups];
      let idx = newCurAggrGroups.findIndex((cag) => cag === ag);
      if (idx >= 0) newCurAggrGroups.splice(idx, 1);
      this.handleAggrGroupsChange(newCurAggrGroups);
    };

    return (
      <div className='graph-config-inner'>
        <div className='graph-config-inner-item'>
          计算函数 :
          <Dropdown overlay={calcFuncMenu}>
            <a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
              {this.state.calcFunc} <DownOutlined />
            </a>
          </Dropdown>
        </div>
        <div className='graph-config-inner-item'>
          环比:
          <Comparison
            comparison={comparison}
            relativeTimeComparison={data.relativeTimeComparison}
            comparisonOptions={data.comparisonOptions}
            graphConfig={data}
            onChange={this.handleComparisonChange}
          />
          <input
            style={{
              position: 'fixed',
              left: -10000,
            }}
            id={`hiddenInput${data.id}`}
          />
        </div>
        <div className='graph-config-inner-item'>
          聚合函数 :
          <Dropdown overlay={aggrFuncMenu}>
            <a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
              {this.state.curAggrFunc} <DownOutlined />
            </a>
          </Dropdown>
        </div>
        {this.state.curAggrFunc ? (
          <div className='graph-config-inner-item'>
            <span>聚合维度 :</span>
            {/* <Select
                mode="multiple"
                size="small"
                style={{ minWidth: 60 }}
                dropdownMatchSelectWidth={false}
                value={this.state.curAggrGroups}
                onChange={this.handleAggrGroupsChange}
              >
                {this.state.aggrGroups.map(ag => <Option key={ag} value={ag}>{ag}</Option>)}
              </Select> */}
            {this.state.curAggrGroups.map((ag) => (
              <Tag
                key={ag}
                closable
                onClose={(e) => {
                  handleDeleteAggrGroupClick(ag);
                }}
              >
                {ag}
              </Tag>
            ))}
            <Dropdown overlay={aggrGroupsMenu} overlayStyle={{ maxHeight: 400, overflow: 'auto' }}>
              <a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
                <PlusCircleOutlined />
              </a>
            </Dropdown>
          </div>
        ) : null}
      </div>
    );
  }