antd/lib/table#ColumnsType TypeScript Examples

The following examples show how to use antd/lib/table#ColumnsType. 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: ExportTableButton.tsx    From ant-table-extensions with MIT License 6 votes vote down vote up
getFieldsFromColumns = (
  columns: ColumnsType<any>
): ITableExportFields => {
  const fields = {};
  columns?.forEach((column: ColumnWithDataIndex) => {
    const { title, key, dataIndex } = column;
    const fieldName =
      (Array.isArray(dataIndex) ? dataIndex.join(".") : dataIndex) ?? key;
    if (fieldName) {
      set(fields, fieldName, title);
    }
  });

  return fields;
}
Example #2
Source File: Members.tsx    From subscan-multisig-react with Apache License 2.0 5 votes vote down vote up
Members = ({ record }: { record: KeyringAddress }) => {
  const { t } = useTranslation();
  const isExtensionAccount = useIsInjected();
  const columnsNested: ColumnsType<KeyringJson> = [
    {
      dataIndex: 'name',
      render: (name) => <div>{name}</div>,
    },
    {
      dataIndex: 'address',
      render: (address) => (
        <Space size="small" className="flex items-center">
          <BaseIdentityIcon theme="polkadot" size={32} value={address} />
          <SubscanLink address={address} />
        </Space>
      ),
    },
    {
      key: 'isInject',
      render: (_, pair) => t(isExtensionAccount(pair.address) ? 'injected' : 'external'),
    },
  ];

  return (
    <div
      className="bg-gray-100 small-table"
      style={{
        padding: '20px',
      }}
    >
      <Table
        columns={columnsNested}
        dataSource={record.meta.addressPair as KeyringJson[]}
        pagination={false}
        bordered
        rowKey="address"
        className="table-without-head hidden lg:block"
      />

      <MemberList data={record} />
    </div>
  );
}
Example #3
Source File: PersonalAPIKeys.tsx    From posthog-foss with MIT License 5 votes vote down vote up
function PersonalAPIKeysTable(): JSX.Element {
    const { keys } = useValues(personalAPIKeysLogic) as { keys: PersonalAPIKeyType[] }
    const { deleteKey } = useActions(personalAPIKeysLogic)

    const columns: ColumnsType<Record<string, any>> = [
        {
            title: 'Label',
            dataIndex: 'label',
            key: 'label',
        },
        {
            title: 'Value',
            dataIndex: 'value',
            key: 'value',
            className: 'ph-no-capture',
            render: RowValue,
        },
        {
            title: 'Last Used',
            dataIndex: 'last_used_at',
            key: 'lastUsedAt',
            render: (lastUsedAt: string | null) => humanFriendlyDetailedTime(lastUsedAt),
        },
        {
            title: 'Created',
            dataIndex: 'created_at',
            key: 'createdAt',
            render: (createdAt: string | null) => humanFriendlyDetailedTime(createdAt),
        },
        {
            title: '',
            key: 'actions',
            align: 'center',
            render: RowActionsCreator(deleteKey),
        },
    ]

    return (
        <Table
            dataSource={keys}
            columns={columns}
            rowKey="id"
            pagination={{ pageSize: 50, hideOnSinglePage: true }}
            style={{ marginTop: '1rem' }}
        />
    )
}
Example #4
Source File: Args.tsx    From subscan-multisig-react with Apache License 2.0 5 votes vote down vote up
export function Args({ args, className, section, method }: ArgsProps) {
  const { t } = useTranslation();
  const { chain } = useApi();
  const columns: ColumnsType<ArgObj> = [
    {
      key: 'name',
      dataIndex: 'name',
      render(name: string, record) {
        return name || record.type;
      },
    },
    {
      key: 'value',
      dataIndex: 'value',
      className: 'value-column',
      // eslint-disable-next-line complexity
      render(value, record) {
        const { type, name } = record;
        const isAddr = type ? isAddressType(type) : isSS58Address(value);

        if (isObject(value)) {
          return (
            <Args
              args={Object.entries(value).map(([prop, propValue]) => ({ name: prop, value: propValue }))}
              section={section}
              method={method}
            />
          );
          // return JSON.stringify(value);
        }

        if (isAddr) {
          return formatAddressValue(value, chain);
        }

        // balances(transfer) kton(transfer)
        if (isBalanceType(type || name) || isCrabValue(name) || section === 'balances' || method === 'transfer') {
          const formatValue = toString(value).replaceAll(',', '');
          return formatBalance(formatValue, +chain.tokens[0].decimal, {
            noDecimal: false,
            withThousandSplit: true,
          }); // FIXME: decimal issue;
        }

        if (isDownloadType(value)) {
          return (
            <a href={value}>
              {t('download')} <DownloadOutlined />
            </a>
          );
        }

        if (isValueType(name)) {
          return value;
        }

        return <div style={{ wordBreak: 'break-all' }}>{value}</div>;
      },
    },
  ];

  return (
    <Table
      columns={columns}
      /* antd form data source require object array */
      dataSource={args.map((arg) => (isObject(arg) ? arg : { value: arg }))}
      pagination={false}
      bordered
      rowKey="name"
      showHeader={false}
      className={className}
    />
  );
}
Example #5
Source File: InternalMetricsTab.tsx    From posthog-foss with MIT License 5 votes vote down vote up
function QueryTable(props: {
    showAnalyze?: boolean
    queries?: QuerySummary[]
    loading: boolean
    columnExtra?: Record<string, any>
}): JSX.Element {
    const { openAnalyzeModalWithQuery } = useActions(systemStatusLogic)
    const columns: ColumnsType<QuerySummary> = [
        {
            title: 'duration',
            dataIndex: 'duration',
            key: 'duration',
            sorter: (a, b) => +a.duration - +b.duration,
        },
        {
            title: 'query',
            dataIndex: 'query',
            render: function RenderAnalyze({}, item: QuerySummary) {
                if (!props.showAnalyze) {
                    return item.query
                }
                return (
                    <Link to="#" onClick={() => openAnalyzeModalWithQuery(item.query)}>
                        {item.query}
                    </Link>
                )
            },
            key: 'query',
        },
    ]

    if (props.queries && props.queries.length > 0) {
        Object.keys(props.queries[0]).forEach((column) => {
            if (column !== 'duration' && column !== 'query') {
                columns.push({ title: column, dataIndex: column, key: column })
            }
        })
    }

    return (
        <Table
            dataSource={props.queries || []}
            columns={columns}
            loading={props.loading}
            pagination={{ pageSize: 30, hideOnSinglePage: true }}
            size="small"
            bordered
            style={{ overflowX: 'auto', overflowY: 'auto' }}
            locale={{ emptyText: 'No queries found' }}
        />
    )
}
Example #6
Source File: Members.tsx    From posthog-foss with MIT License 5 votes vote down vote up
export function Members({ user }: MembersProps): JSX.Element {
    const { members, membersLoading } = useValues(membersLogic)

    const columns: ColumnsType<OrganizationMemberType> = [
        {
            key: 'user_profile_picture',
            render: function ProfilePictureRender(_, member) {
                return <ProfilePicture name={member.user.first_name} email={member.user.email} />
            },
            width: 32,
        },
        {
            title: 'Name',
            key: 'user_first_name',
            render: (_, member) =>
                member.user.uuid == user.uuid ? `${member.user.first_name} (me)` : member.user.first_name,
            sorter: (a, b) => a.user.first_name.localeCompare(b.user.first_name),
        },
        {
            title: 'Email',
            key: 'user_email',
            render: (_, member) => member.user.email,
            sorter: (a, b) => a.user.email.localeCompare(b.user.email),
        },
        {
            title: 'Level',
            dataIndex: 'level',
            key: 'level',
            render: function LevelRender(_, member) {
                return LevelComponent(member)
            },
            sorter: (a, b) => a.level - b.level,
            defaultSortOrder: 'descend',
        },
        {
            title: 'Joined At',
            dataIndex: 'joined_at',
            key: 'joined_at',
            render: (joinedAt: string) => humanFriendlyDetailedTime(joinedAt),
            sorter: (a, b) => a.joined_at.localeCompare(b.joined_at),
            defaultSortOrder: 'ascend',
        },
        {
            dataIndex: 'actions',
            key: 'actions',
            align: 'center',
            render: function ActionsRender(_, member) {
                return ActionsComponent(member)
            },
        },
    ]

    return (
        <>
            <h2 className="subtitle">Members</h2>
            <Table
                dataSource={members}
                columns={columns}
                rowKey="id"
                pagination={false}
                style={{ marginTop: '1rem' }}
                loading={membersLoading}
                data-attr="org-members-table"
            />
        </>
    )
}
Example #7
Source File: TeamMembers.tsx    From posthog-foss with MIT License 5 votes vote down vote up
export function TeamMembers({ user }: MembersProps): JSX.Element {
    const { allMembers, allMembersLoading } = useValues(teamMembersLogic)

    const columns: ColumnsType<FusedTeamMemberType> = [
        {
            key: 'user_profile_picture',
            render: function ProfilePictureRender(_, member) {
                return <ProfilePicture name={member.user.first_name} email={member.user.email} />
            },
            width: 32,
        },
        {
            title: 'Name',
            key: 'user_first_name',
            render: (_, member) =>
                member.user.uuid == user.uuid ? `${member.user.first_name} (me)` : member.user.first_name,
            sorter: (a, b) => a.user.first_name.localeCompare(b.user.first_name),
        },
        {
            title: 'Email',
            key: 'user_email',
            render: (_, member) => member.user.email,
            sorter: (a, b) => a.user.email.localeCompare(b.user.email),
        },
        {
            title: 'Level',
            key: 'level',
            render: function LevelRender(_, member) {
                return LevelComponent(member)
            },
            sorter: (a, b) => a.level - b.level,
            defaultSortOrder: 'descend',
        },
        {
            title: 'Joined At',
            dataIndex: 'joined_at',
            key: 'joined_at',
            render: (joinedAt: string) => humanFriendlyDetailedTime(joinedAt),
            sorter: (a, b) => a.joined_at.localeCompare(b.joined_at),
            defaultSortOrder: 'ascend',
        },
        {
            key: 'actions',
            align: 'center',
            render: function ActionsRender(_, member) {
                return ActionsComponent(member)
            },
        },
    ]
    return (
        <>
            <h2 className="subtitle" id="members-with-project-access" style={{ justifyContent: 'space-between' }}>
                Members with Project Access
                <RestrictedArea
                    Component={AddMembersModalWithButton}
                    minimumAccessLevel={OrganizationMembershipLevel.Admin}
                    scope={RestrictionScope.Project}
                />
            </h2>
            <Table
                dataSource={allMembers}
                columns={columns}
                rowKey="id"
                pagination={false}
                style={{ marginTop: '1rem' }}
                loading={allMembersLoading}
                data-attr="team-members-table"
            />
        </>
    )
}
Example #8
Source File: resourceTable.tsx    From fe-v5 with Apache License 2.0 5 votes vote down vote up
function ResourceTable({ onSelect }: IProps) {
  const { t } = useTranslation();
  const [selectRowKeys, setSelectRowKeys] = useState<React.Key[]>([]);
  const { currentGroup } = useSelector<RootState, resourceStoreState>(
    (state) => state.resource,
  );
  const [query, setQuery] = useState<string>('');
  const columns: ColumnsType<resourceItem> = [
    {
      title: t('资源标识'),
      dataIndex: 'ident',
    },
    {
      title: t('资源名称'),
      dataIndex: 'alias',
    },
  ];

  const handleSearchResource = (keyword: string) => {
    setQuery(keyword);
  };

  const handleFetchList = (list: Array<resourceItem>) => {
    const idents = list.map((item) => item.ident);
    setSelectRowKeys(idents);
    onSelect && onSelect(idents);
  };

  return (
    <div>
      <div className='table-search-area'>
        <SearchInput
          placeholder={t('资源信息或标签')}
          onSearch={handleSearchResource}
        ></SearchInput>
      </div>
      <BaseTable
        columns={columns}
        rowKey={'ident'}
        fetchParams={{
          id: currentGroup?.id || '',
          query,
        }}
        fetchHandle={getResourceList}
        rowSelection={{
          selectedRowKeys: selectRowKeys,
          onChange: (selectedRowKeys: React.Key[]) => {
            setSelectRowKeys(selectedRowKeys);
            onSelect && onSelect(selectedRowKeys as Array<string>);
          },
        }}
        paginationProps={{
          simple: true,
        }}
        onFetchList={handleFetchList}
      ></BaseTable>
    </div>
  );
}
Example #9
Source File: index.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
AddUser: React.FC<TeamProps> = (props: TeamProps) => {
  const { t } = useTranslation();
  const { teamId, onSelect } = props;
  const [teamInfo, setTeamInfo] = useState<Team>();
  const [selectedUser, setSelectedUser] = useState<React.Key[]>([]);
  const [selectedUserRows, setSelectedUserRows] = useState<User[]>([]);
  const [query, setQuery] = useState('');
  const userColumn: ColumnsType<User> = [
    {
      title: t('用户名'),
      dataIndex: 'username',
      ellipsis: true,
    },
    {
      title: t('显示名'),
      dataIndex: 'nickname',
      ellipsis: true,
      render: (text: string, record) => record.nickname || '-',
    },
    {
      title: t('邮箱'),
      dataIndex: 'email',
      render: (text: string, record) => record.email || '-',
    },
    {
      title: t('手机'),
      dataIndex: 'phone',
      render: (text: string, record) => record.phone || '-',
    },
  ];
  useEffect(() => {
    getTeam();
  }, []);

  const getTeam = () => {
    if (!teamId) return;
    getTeamInfo(teamId).then((data) => {
      setTeamInfo(data.user_group);
    });
  };

  const handleClose = (val) => {
    let newList = selectedUserRows.filter((item) => item.id !== val.id);
    let newId = newList.map((item) => item.id);
    setSelectedUserRows(newList);
    setSelectedUser(newId);
  };

  const onSelectChange = (newKeys: [], newRows: []) => {
    onSelect(newKeys);
    setSelectedUser(newKeys);
    setSelectedUserRows(newRows);
  };

  return (
    <div>
      <div>
        <span>{t('团队名称')}:</span>
        {teamInfo && teamInfo.name}
      </div>
      <div
        style={{
          margin: '20px 0 16px',
        }}
      >
        {selectedUser.length > 0 && (
          <span>
            {t('已选择')}
            {selectedUser.length}
            {t('项')}:
          </span>
        )}
        {selectedUserRows.map((item, index) => {
          return (
            <Tag
              style={{
                marginBottom: '4px',
              }}
              closable
              onClose={() => handleClose(item)}
              key={item.id}
            >
              {item.username}
            </Tag>
          );
        })}
      </div>
      <Input
        className={'searchInput'}
        prefix={<SearchOutlined />}
        placeholder={t('用户名、邮箱或电话')}
        onPressEnter={(e) => {
          setQuery((e.target as HTMLInputElement).value);
        }}
      />
      <BaseTable
        fetchHandle={getUserInfoList}
        columns={userColumn}
        rowKey='id'
        needPagination={true}
        pageSize={5}
        fetchParams={{
          query,
        }}
        rowSelection={{
          preserveSelectedRowKeys: true,
          selectedRowKeys: selectedUser,
          onChange: onSelectChange,
        }}
      ></BaseTable>
    </div>
  );
}
Example #10
Source File: Wallets.tsx    From subscan-multisig-react with Apache License 2.0 4 votes vote down vote up
export function Wallets() {
  const { t } = useTranslation();
  const history = useHistory();
  const { api, chain, network } = useApi();
  const [multisigAccounts, setMultisigAccounts] = useState<KeyringAddress[]>([]);
  const isExtensionAccount = useIsInjected();
  const [isCalculating, setIsCalculating] = useState<boolean>(true);
  const [searchKeyword, setSearchKeyword] = useState('');

  const displayMultisigAccounts = useMemo(() => {
    if (!searchKeyword.trim()) {
      return multisigAccounts;
    }
    return multisigAccounts.filter(
      (account) =>
        (account.meta.name && account.meta.name.indexOf(searchKeyword.trim()) >= 0) ||
        account.address.indexOf(searchKeyword.trim()) >= 0
    );
  }, [multisigAccounts, searchKeyword]);

  const { linkColor, mainColor } = useMemo(() => {
    return { linkColor: getLinkColor(network), mainColor: getThemeColor(network) };
  }, [network]);

  const exportAllWallets = () => {
    if (!multisigAccounts || multisigAccounts.length < 1) {
      return;
    }
    const configs: MultisigAccountConfig[] = [];
    multisigAccounts.forEach((multisigAccount) => {
      const config = {
        name: multisigAccount.meta.name || '',
        members: multisigAccount.meta.addressPair as { name: string; address: string }[],
        threshold: multisigAccount.meta.threshold as number,
        scope: getMultiAccountScope(multisigAccount.publicKey),
      };
      configs.push(config);
    });

    const blob = new Blob([JSON.stringify(configs)], { type: 'text/plain;charset=utf-8' });
    saveAs(blob, `multisig_accounts.json`);
  };

  const uploadProps = {
    name: 'file',
    headers: {
      authorization: 'authorization-text',
    },
    onChange(info: any) {
      if (info.file.status !== 'uploading') {
        // console.log(info.file, info.fileList);
      }
      // if (info.file.status === 'done') {
      //   message.success(`${info.file.name} file uploaded successfully`);
      // } else if (info.file.status === 'error') {
      //   message.error(`${info.file.name} file upload failed.`);
      // }
    },
    customRequest(info: any) {
      try {
        const reader = new FileReader();

        reader.onload = (e: any) => {
          // eslint-disable-next-line no-console
          // console.log(e.target.result);

          try {
            const configs = JSON.parse(e.target.result) as MultisigAccountConfig[];
            configs
              .filter((config) => {
                const encodeMembers = config.members.map((member) => {
                  return {
                    name: member.name,
                    address: encodeAddress(member.address, Number(chain.ss58Format)),
                  };
                });
                const publicKey = createKeyMulti(
                  encodeMembers.map((item) => item.address),
                  config.threshold
                );
                const acc = findMultiAccountFromKey(publicKey);
                return !acc;
              })
              .forEach((config) => {
                const encodeMembers = config.members.map((member) => {
                  return {
                    name: member.name,
                    address: encodeAddress(member.address, Number(chain.ss58Format)),
                  };
                });
                const signatories = encodeMembers.map(({ address }) => address);

                const addressPair = config.members.map(({ address, ...other }) => ({
                  ...other,
                  address: encodeAddress(address, Number(chain.ss58Format)),
                }));

                keyring.addMultisig(signatories, config.threshold, {
                  name: config.name,
                  addressPair,
                  genesisHash: api?.genesisHash.toHex(),
                });

                const publicKey = createKeyMulti(
                  encodeMembers.map((item) => item.address),
                  config.threshold
                );

                updateMultiAccountScopeFromKey(publicKey, ShareScope.all, [], network);
              });

            message.success(t('success'));
            refreshMultisigAccounts();
          } catch {
            message.error(t('account config error'));
          }
        };
        reader.readAsText(info.file);
      } catch (err: unknown) {
        if (err instanceof Error) {
          // eslint-disable-next-line no-console
          console.log('err:', err);
        }
      }
    },
  };

  const renderAddress = (address: string) => (
    <Link to={Path.extrinsic + '/' + address + history.location.hash} style={{ color: linkColor }}>
      {address}
    </Link>
  );

  const renderAction = useCallback(
    (row: KeyringAddress) => {
      // const { address } = row;

      return (
        <Space size="middle">
          <div className="flex items-center">
            <Button
              type="primary"
              className="flex items-center justify-center h-7"
              onClick={() => {
                history.push(Path.extrinsic + '/' + row.address + history.location.hash);
              }}
              style={{
                borderRadius: '4px',
              }}
            >
              {t('actions')}
            </Button>

            {(row as unknown as any).entries && (row as unknown as any).entries.length > 0 && (
              <div className="ml-2 bg-red-500 rounded-full w-3 h-3"></div>
            )}
          </div>
        </Space>
      );
    },
    [history, t]
  );

  const columns: ColumnsType<KeyringAddress> = [
    {
      title: t('name'),
      dataIndex: ['meta', 'name'],
    },
    {
      title: t('address'),
      dataIndex: 'address',
      render: renderAddress,
    },
    {
      title: t('balance'),
      key: 'balance',
      render: (account) => {
        return <Space direction="vertical">{renderBalances(account, chain)}</Space>;
      },
    },
    {
      title: t('status.index'),
      key: 'status',
      render: (_, record) => {
        const {
          meta: { addressPair },
        } = record;

        return (addressPair as AddressPair[])?.some((item) => isExtensionAccount(item.address))
          ? t('available')
          : t('watch only');
      },
    },
    {
      // title: t('actions'),
      title: '',
      key: 'action',
      render: (_1: unknown, row) => renderAction(row),
    },
  ];

  const expandedRowRender = (record: KeyringAddress) => {
    const columnsNested: ColumnsType<KeyringJson> = [
      { dataIndex: 'name' },
      {
        dataIndex: 'address',
        render: (address) => (
          <Space size="middle">
            <BaseIdentityIcon theme="polkadot" size={32} value={address} />
            <SubscanLink address={address} />
          </Space>
        ),
      },
      {
        key: 'isInject',
        render: (_, pair) => t(isExtensionAccount(pair.address) ? 'injected' : 'external'),
      },
    ];

    return (
      <div className="multisig-list-expand bg-gray-100 p-5 members">
        <Table
          columns={columnsNested}
          dataSource={record.meta.addressPair as KeyringJson[]}
          pagination={false}
          bordered
          rowKey="address"
          className=" table-without-head"
        />
      </div>
    );
  };

  const refreshMultisigAccounts = useCallback(async () => {
    setIsCalculating(true);

    const accounts = keyring
      .getAccounts()
      .filter((account) => account.meta.isMultisig && isInCurrentScope(account.publicKey, network));
    const balances = await api?.query.system.account.multi(accounts.map(({ address }) => address));
    const entries = await Promise.all(
      accounts.map(async ({ address }) => await api?.query.multisig.multisigs.entries(address))
    );

    setMultisigAccounts(
      accounts.map((item, index) => {
        (item.meta.addressPair as KeyringJson[]).forEach((key) => {
          key.address = convertToSS58(key.address, Number(chain.ss58Format));
        });
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const source = (balances as any)[index] as unknown as any;
        return {
          ...item,
          value: source.data.free.toString(),
          kton: source.data.freeKton?.toString() ?? '0',
          entries: entries[index] || [],
        };
      })
    );
    setIsCalculating(false);
  }, [api, network, chain]);

  useEffect(() => {
    refreshMultisigAccounts();
  }, [refreshMultisigAccounts]);

  const menu = (
    <Menu>
      <Menu.Item key="1" onClick={exportAllWallets}>
        {t('export all')}
      </Menu.Item>

      <Upload {...uploadProps} showUploadList={false}>
        <Menu.Item key="2">{t('import all')}</Menu.Item>
      </Upload>
    </Menu>
  );

  if (!isCalculating && multisigAccounts.length === 0) {
    return (
      <Space
        direction="vertical"
        className="w-full h-full flex flex-col items-center justify-center absolute"
        id="wallets"
      >
        <div className="flex flex-col items-center">
          <AddIcon className="w-24 h-24" />

          <div className="text-black-800 font-semibold text-xl lg:mt-16 lg:mb-10 mt-6 mb-4">
            Please create Multisig wallet first
          </div>

          <Link to={Path.wallet + history.location.hash}>
            <Button type="primary" className="w-48">
              {t('wallet.add')}
            </Button>
          </Link>

          <div className="my-1">{t('or')}</div>

          <Upload {...uploadProps} showUploadList={false}>
            <Button type="primary" className="w-48">
              {t('import all')}
            </Button>
          </Upload>
        </div>
      </Space>
    );
  }

  return (
    <Space direction="vertical" className="absolute top-4 bottom-4 left-4 right-4 overflow-auto" id="wallets">
      <div className="flex flex-col md:justify-between md:flex-row">
        <div className="flex items-center">
          <Link to={Path.wallet + history.location.hash}>
            <Button type="primary" className="w-44">
              {t('wallet.add')}
            </Button>
          </Link>

          <Dropdown overlay={menu} trigger={['click']} placement="bottomCenter">
            <MoreOutlined
              className="ml-4 rounded-full opacity-60 cursor-pointer p-1"
              style={{
                color: mainColor,
                backgroundColor: mainColor + '40',
              }}
              onClick={(e) => e.preventDefault()}
            />
          </Dropdown>

          {/* {multisigAccounts && multisigAccounts.length >= 1 && (
          <Tooltip title={t('export all')}>
            <ExportIcon className="ml-5 mt-1 w-8 h-8 cursor-pointer" onClick={exportAllWallets} />
          </Tooltip>
        )}

        <Tooltip title={t('import all')}>
          <Upload {...uploadProps} showUploadList={false}>
            <ImportIcon className="ml-5 mt-1 w-8 h-8 cursor-pointer" />
          </Upload>
        </Tooltip> */}
        </div>

        <div className="w-56 mt-4 md:mt-0 md:w-72">
          <Input
            placeholder={t('wallet search placeholder')}
            value={searchKeyword}
            onChange={(e) => {
              setSearchKeyword(e.target.value);
            }}
          />
        </div>
      </div>

      <Table
        columns={columns}
        dataSource={displayMultisigAccounts}
        rowKey="address"
        expandable={{ expandedRowRender, expandIcon: genExpandMembersIcon(t('members')), expandIconColumnIndex: 4 }}
        pagination={false}
        loading={isCalculating}
        className="lg:block hidden multisig-list-table"
      />

      <Space direction="vertical" className="lg:hidden block">
        {displayMultisigAccounts.map((account) => {
          const { address, meta } = account;

          return (
            <Collapse
              key={address}
              expandIcon={() => <></>}
              collapsible={
                (meta.addressPair as AddressPair[])?.some((item) => isExtensionAccount(item.address))
                  ? 'header'
                  : 'disabled'
              }
              className="wallet-collapse"
            >
              <Panel
                header={
                  <Space direction="vertical" className="w-full">
                    <Typography.Text className="mr-4">{meta.name}</Typography.Text>

                    <Typography.Text copyable>{address}</Typography.Text>
                  </Space>
                }
                key={address}
                extra={
                  <Space direction="vertical" className="text-right">
                    <Space>{renderBalances(account, chain)}</Space>
                    {renderAction(account)}
                  </Space>
                }
                className="overflow-hidden mb-4"
              >
                <MemberList data={account} />
              </Panel>
            </Collapse>
          );
        })}
      </Space>
    </Space>
  );
}
Example #11
Source File: groups.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
Resource: React.FC = () => {
  const { t } = useTranslation();
  const { type } =
    useParams<{
      type: string;
    }>();
  const [activeKey, setActiveKey] = useState<UserType>(UserType.Team);
  const [visible, setVisible] = useState<boolean>(false);
  const [action, setAction] = useState<ActionType>();
  const [userId, setUserId] = useState<string>('');
  const [teamId, setTeamId] = useState<string>('');
  const [memberId, setMemberId] = useState<string>('');
  const [memberList, setMemberList] = useState<User[]>([]);
  const [allMemberList, setAllMemberList] = useState<User[]>([]);
  const [teamInfo, setTeamInfo] = useState<Team>();
  const [teamList, setTeamList] = useState<Team[]>([]);
  const [memberLoading, setMemberLoading] = useState<boolean>(false);
  const [searchValue, setSearchValue] = useState<string>('');
  const [searchMemberValue, setSearchMemberValue] = useState<string>('');
  const userRef = useRef(null as any);
  let { profile } = useSelector<RootState, accountStoreState>((state) => state.account);
  const userColumn: ColumnsType<User> = [
    {
      title: t('用户名'),
      dataIndex: 'username',
      ellipsis: true,
    },
    {
      title: t('显示名'),
      dataIndex: 'nickname',
      ellipsis: true,
      render: (text: string, record) => record.nickname || '-',
    },
    {
      title: t('邮箱'),
      dataIndex: 'email',
      render: (text: string, record) => record.email || '-',
    },
    {
      title: t('手机'),
      dataIndex: 'phone',
      render: (text: string, record) => record.phone || '-',
    },
  ];

  const teamMemberColumns: ColumnsType<User> = [
    ...userColumn,
    {
      title: t('操作'),
      width: '100px',
      render: (
        text: string,
        record, // <DelPopover
      ) => (
        //   teamId={teamId}
        //   memberId={record.id}
        //   userType='member'
        //   onClose={() => handleClose()}
        // ></DelPopover>
        <a
          style={{
            color: 'red',
          }}
          onClick={() => {
            let params = {
              ids: [record.id],
            };
            confirm({
              title: t('是否删除该成员'),
              onOk: () => {
                deleteMember(teamId, params).then((_) => {
                  message.success(t('成员删除成功'));
                  handleClose('updateMember');
                });
              },
              onCancel: () => {},
            });
          }}
        >
          {t('删除')}
        </a>
      ),
    },
  ];

  useEffect(() => {
    getList(true);
  }, []); //teamId变化触发

  useEffect(() => {
    if (teamId) {
      getTeamInfoDetail(teamId);
    }
  }, [teamId]);

  const getList = (isDeleteOrAdd = false) => {
    getTeamList('', isDeleteOrAdd);
  };

  // 获取团队列表
  const getTeamList = (search?: string, isDelete?: boolean) => {
    getTeamInfoList({ query: search || '' }).then((data) => {
      setTeamList(data.dat || []);
      if ((!teamId || isDelete) && data.dat.length > 0) {
        setTeamId(data.dat[0].id);
      }
    });
  };

  // 获取团队详情
  const getTeamInfoDetail = (id: string) => {
    setMemberLoading(true);
    getTeamInfo(id).then((data: TeamInfo) => {
      setTeamInfo(data.user_group);
      setMemberList(data.users);
      setAllMemberList(data.users);
      setMemberLoading(false);
    });
  };

  const handleSearch = (type?: string, val?: string) => {
    if (type === 'team') {
      getTeamList(val);
    } else {
      if (!val) {
        getTeamInfoDetail(teamId);
      } else {
        setMemberLoading(true);
        let newList = allMemberList.filter(
          (item) =>
            item.username.indexOf(val) !== -1 ||
            item.nickname.indexOf(val) !== -1 ||
            item.id.toString().indexOf(val) !== -1 ||
            item.phone.indexOf(val) !== -1 ||
            item.email.indexOf(val) !== -1,
        );
        setMemberList(newList);
        setMemberLoading(false);
      }
    }
  };

  const handleClick = (type: ActionType, id?: string, memberId?: string) => {
    if (id) {
      setTeamId(id);
    } else {
      setTeamId('');
    }

    if (memberId) {
      setMemberId(memberId);
    } else {
      setMemberId('');
    }

    setAction(type);
    setVisible(true);
  };

  // 弹窗关闭回调
  const handleClose = (isDeleteOrAdd: boolean | string = false) => {
    setVisible(false);
    if (searchValue) {
      handleSearch('team', searchValue);
    } else {
      // 添加、删除成员 不用获取列表
      if (isDeleteOrAdd !== 'updateMember') {
        getList(isDeleteOrAdd !== 'updateName'); // 修改名字,不用选中第一个
      }
    }
    if (teamId && (isDeleteOrAdd === 'update' || isDeleteOrAdd === 'updateMember' || isDeleteOrAdd === 'updateName')) {
      getTeamInfoDetail(teamId);
    }
  };

  return (
    <PageLayout title={t('团队管理')} icon={<UserOutlined />} hideCluster>
      <div className='user-manage-content'>
        <div style={{ display: 'flex', height: '100%' }}>
          <div className='left-tree-area'>
            <div className='sub-title'>
              {t('团队列表')}
              <Button
                style={{
                  height: '30px',
                }}
                size='small'
                type='link'
                onClick={() => {
                  handleClick(ActionType.CreateTeam);
                }}
              >
                {t('新建团队')}
              </Button>
            </div>
            <div style={{ display: 'flex', margin: '5px 0px 12px' }}>
              <Input
                prefix={<SearchOutlined />}
                value={searchValue}
                onChange={(e) => {
                  setSearchValue(e.target.value);
                }}
                placeholder={t('搜索团队名称')}
                onPressEnter={(e) => {
                  // @ts-ignore
                  getTeamList(e.target.value);
                }}
                onBlur={(e) => {
                  // @ts-ignore
                  getTeamList(e.target.value);
                }}
              />
            </div>

            <List
              style={{
                marginBottom: '12px',
                flex: 1,
                overflow: 'auto',
              }}
              dataSource={teamList}
              size='small'
              renderItem={(item) => (
                <List.Item key={item.id} className={teamId === item.id ? 'is-active' : ''} onClick={() => setTeamId(item.id)}>
                  {item.name}
                </List.Item>
              )}
            />
          </div>
          {teamList.length > 0 ? (
            <div className='resource-table-content'>
              <Row className='team-info'>
                <Col
                  span='24'
                  style={{
                    color: '#000',
                    fontSize: '14px',
                    fontWeight: 'bold',
                    display: 'inline',
                  }}
                >
                  {teamInfo && teamInfo.name}
                  <EditOutlined
                    title={t('刷新')}
                    style={{
                      marginLeft: '8px',
                      fontSize: '14px',
                    }}
                    onClick={() => handleClick(ActionType.EditTeam, teamId)}
                  ></EditOutlined>
                  <DeleteOutlined
                    style={{
                      marginLeft: '8px',
                      fontSize: '14px',
                    }}
                    onClick={() => {
                      confirm({
                        title: t('是否删除该团队'),
                        onOk: () => {
                          deleteTeam(teamId).then((_) => {
                            message.success(t('团队删除成功'));
                            handleClose(true);
                          });
                        },
                        onCancel: () => {},
                      });
                    }}
                  />
                </Col>
                <Col
                  style={{
                    marginTop: '8px',
                    color: '#666',
                  }}
                >
                  {t('备注')}:{teamInfo && teamInfo.note ? teamInfo.note : '-'}
                </Col>
              </Row>
              <Row justify='space-between' align='middle'>
                <Col span='12'>
                  <Input
                    prefix={<SearchOutlined />}
                    value={searchMemberValue}
                    className={'searchInput'}
                    onChange={(e) => setSearchMemberValue(e.target.value)}
                    placeholder={t('用户名、显示名、邮箱或手机')}
                    onPressEnter={(e) => handleSearch('member', searchMemberValue)}
                  />
                </Col>
                <Button
                  type='primary'
                  ghost
                  onClick={() => {
                    handleClick(ActionType.AddUser, teamId);
                  }}
                >
                  {t('添加成员')}
                </Button>
              </Row>

              <Table rowKey='id' columns={teamMemberColumns} dataSource={memberList} loading={memberLoading} />
            </div>
          ) : (
            <div className='blank-busi-holder'>
              <p style={{ textAlign: 'left', fontWeight: 'bold' }}>
                <InfoCircleOutlined style={{ color: '#1473ff' }} /> {t('提示信息')}
              </p>
              <p>
                没有与您相关的团队,请先&nbsp;
                <a onClick={() => handleClick(ActionType.CreateTeam)}>创建团队</a>
              </p>
            </div>
          )}
        </div>
        <UserInfoModal
          visible={visible}
          action={action as ActionType}
          width={500}
          userType={activeKey}
          onClose={handleClose}
          onSearch={(val) => {
            setSearchValue(val);
            handleSearch('team', val);
          }}
          userId={userId}
          teamId={teamId}
          memberId={memberId}
        />
      </div>
    </PageLayout>
  );
}
Example #12
Source File: users.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
Resource: React.FC = () => {
  const { t } = useTranslation();

  const [activeKey, setActiveKey] = useState<UserType>(UserType.User);
  const [visible, setVisible] = useState<boolean>(false);
  const [action, setAction] = useState<ActionType>();
  const [userId, setUserId] = useState<string>('');
  const [teamId, setTeamId] = useState<string>('');
  const [memberId, setMemberId] = useState<string>('');
  const [allMemberList, setAllMemberList] = useState<User[]>([]);
  const [teamList, setTeamList] = useState<Team[]>([]);
  const [query, setQuery] = useState<string>('');
  const [searchValue, setSearchValue] = useState<string>('');
  const userRef = useRef(null as any);
  let { profile } = useSelector<RootState, accountStoreState>((state) => state.account);
  const userColumn: ColumnsType<User> = [
    {
      title: t('用户名'),
      dataIndex: 'username',
      ellipsis: true,
    },
    {
      title: t('显示名'),
      dataIndex: 'nickname',
      ellipsis: true,
      render: (text: string, record) => record.nickname || '-',
    },
    {
      title: t('邮箱'),
      dataIndex: 'email',
      render: (text: string, record) => record.email || '-',
    },
    {
      title: t('手机'),
      dataIndex: 'phone',
      render: (text: string, record) => record.phone || '-',
    },
  ];
  const userColumns: ColumnsType<User> = [
    ...userColumn,
    {
      title: t('角色'),
      dataIndex: 'roles',
      render: (text: [], record) => text.join(', '),
    },
    {
      title: t('操作'),
      width: '240px',
      render: (text: string, record) => (
        <>
          <Button className='oper-name' type='link' onClick={() => handleClick(ActionType.EditUser, record.id)}>
            {t('编辑')}
          </Button>
          <Button className='oper-name' type='link' onClick={() => handleClick(ActionType.Reset, record.id)}>
            {t('重置密码')}
          </Button>
          {/* <DelPopover
         userId={record.id}
         userType='user'
         onClose={() => handleClose()}
        ></DelPopover> */}
          <a
            style={{
              color: 'red',
              marginLeft: '16px',
            }}
            onClick={() => {
              confirm({
                title: t('是否删除该用户'),
                onOk: () => {
                  deleteUser(record.id).then((_) => {
                    message.success(t('用户删除成功'));
                    handleClose();
                  });
                },
                onCancel: () => {},
              });
            }}
          >
            {t('删除')}
          </a>
        </>
      ),
    },
  ];

  if (!profile.roles.includes('Admin')) {
    userColumns.pop(); //普通用户不展示操作列
  }

  const getList = () => {
    userRef.current.refreshList();
  };

  const handleClick = (type: ActionType, id?: string, memberId?: string) => {
    if (id) {
      activeKey === UserType.User ? setUserId(id) : setTeamId(id);
    } else {
      activeKey === UserType.User ? setUserId('') : setTeamId('');
    }

    if (memberId) {
      setMemberId(memberId);
    } else {
      setMemberId('');
    }

    setAction(type);
    setVisible(true);
  };

  // 弹窗关闭回调
  const handleClose = () => {
    setVisible(false);
    getList();
  };

  const onSearchQuery = (e) => {
    let val = e.target.value;
    setQuery(val);
  };

  return (
    <PageLayout title={t('用户管理')} icon={<UserOutlined />} hideCluster>
      <div className='user-manage-content'>
        <div className='user-content'>
          <Row className='event-table-search'>
            <div className='event-table-search-left'>
              <Input className={'searchInput'} prefix={<SearchOutlined />} onPressEnter={onSearchQuery} placeholder={t('用户名、邮箱或手机')} />
            </div>
            <div className='event-table-search-right'>
              {activeKey === UserType.User && profile.roles.includes('Admin') && (
                <div className='user-manage-operate'>
                  <Button type='primary' onClick={() => handleClick(activeKey === UserType.User ? ActionType.CreateUser : t('创建团队'))} ghost>
                    {t('创建用户')}
                  </Button>
                </div>
              )}
            </div>
          </Row>
          <BaseTable
            ref={userRef}
            fetchHandle={getUserInfoList}
            columns={userColumns}
            rowKey='id'
            needPagination={true}
            fetchParams={{
              query,
            }}
          ></BaseTable>
        </div>

        <UserInfoModal
          visible={visible}
          action={action as ActionType}
          width={activeKey === UserType.User ? 500 : 700}
          userType={activeKey}
          onClose={handleClose}
          userId={userId}
          teamId={teamId}
          memberId={memberId}
        />
      </div>
    </PageLayout>
  );
}
Example #13
Source File: index.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
Shield: React.FC = () => {
  const { t } = useTranslation();
  const history = useHistory();
  const dispatch = useDispatch();
  const [query, setQuery] = useState<string>('');
  const { curBusiItem } = useSelector<RootState, CommonStoreState>((state) => state.common);
  const [bgid, setBgid] = useState(undefined);
  const [clusters, setClusters] = useState<string[]>([]);
  const [currentShieldDataAll, setCurrentShieldDataAll] = useState<Array<shieldItem>>([]);
  const [currentShieldData, setCurrentShieldData] = useState<Array<shieldItem>>([]);
  const [loading, setLoading] = useState<boolean>(false);

  const columns: ColumnsType = [
    {
      title: t('集群'),
      dataIndex: 'cluster',
      render: (data) => {
        return <div>{data}</div>;
      },
    },
    {
      title: t('标签'),
      dataIndex: 'tags',
      render: (text: any) => {
        return (
          <>
            {text
              ? text.map((tag, index) => {
                  return tag ? (
                    // <ColorTag text={`${tag.key} ${tag.func} ${tag.func === 'in' ? tag.value.split(' ').join(', ') : tag.value}`} key={index}>
                    // </ColorTag>
                    <div key={index} style={{ lineHeight: '16px' }}>{`${tag.key} ${tag.func} ${tag.func === 'in' ? tag.value.split(' ').join(', ') : tag.value}`}</div>
                  ) : null;
                })
              : ''}
          </>
        );
      },
    },
    {
      title: t('屏蔽原因'),
      dataIndex: 'cause',
      render: (text: string, record: shieldItem) => {
        return (
          <>
            <Tooltip placement='topLeft' title={text}>
              <div
                style={{
                  whiteSpace: 'nowrap',
                  textOverflow: 'ellipsis',
                  overflow: 'hidden',
                  lineHeight: '16px',
                }}
              >
                {text}
              </div>
            </Tooltip>
            by {record.create_by}
          </>
        );
      },
    },
    {
      title: t('屏蔽时间'),
      dataIndex: 'btime',
      render: (text: number, record: shieldItem) => {
        return (
          <div className='shield-time'>
            <div>
              {t('开始:')}
              {dayjs(record?.btime * 1000).format('YYYY-MM-DD HH:mm:ss')}
            </div>
            <div>
              {t('结束:')}
              {dayjs(record?.etime * 1000).format('YYYY-MM-DD HH:mm:ss')}
            </div>
          </div>
        );
      },
    },
    // {
    //   title: t('创建人'),
    //   ellipsis: true,
    //   dataIndex: 'create_by',
    // },
    {
      title: t('操作'),
      width: '98px',
      dataIndex: 'operation',
      render: (text: undefined, record: shieldItem) => {
        return (
          <>
            <div className='table-operator-area'>
              <div
                className='table-operator-area-normal'
                style={{
                  cursor: 'pointer',
                  display: 'inline-block',
                }}
                onClick={() => {
                  dispatch({
                    type: 'shield/setCurShieldData',
                    data: record,
                  });
                  curBusiItem?.id && history.push(`/alert-mutes/edit/${record.id}?mode=clone`);
                }}
              >
                {t('克隆')}
              </div>
              <div
                className='table-operator-area-warning'
                style={{
                  cursor: 'pointer',
                  display: 'inline-block',
                }}
                onClick={() => {
                  confirm({
                    title: t('确定删除该告警屏蔽?'),
                    icon: <ExclamationCircleOutlined />,
                    onOk: () => {
                      dismiss(record.id);
                    },

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

  useEffect(() => {
    getList();
  }, [curBusiItem]);

  useEffect(() => {
    filterData();
  }, [query, clusters, currentShieldDataAll]);

  const dismiss = (id: number) => {
    deleteShields({ ids: [id] }, curBusiItem.id).then((res) => {
      refreshList();
      if (res.err) {
        message.success(res.err);
      } else {
        message.success(t('删除成功'));
      }
    });
  };

  const filterData = () => {
    const data = JSON.parse(JSON.stringify(currentShieldDataAll));
    const res = data.filter((item: shieldItem) => {
      const tagFind = item.tags.find((tag) => {
        return tag.key.indexOf(query) > -1 || tag.value.indexOf(query) > -1 || tag.func.indexOf(query) > -1;
      });
      return (item.cause.indexOf(query) > -1 || !!tagFind) && ((clusters && clusters?.indexOf(item.cluster) > -1) || clusters?.length === 0);
    });
    setCurrentShieldData(res || []);
  };

  const getList = async () => {
    if (curBusiItem.id) {
      setLoading(true);
      const { success, dat } = await getShieldList({ id: curBusiItem.id });
      if (success) {
        setCurrentShieldDataAll(dat || []);
        setLoading(false);
      }
    }
  };

  const refreshList = () => {
    getList();
  };

  const onSearchQuery = (e) => {
    let val = e.target.value;
    setQuery(val);
  };

  const clusterChange = (data) => {
    setClusters(data);
  };

  const busiChange = (data) => {
    setBgid(data);
  };

  return (
    <PageLayout title={t('屏蔽规则')} icon={<CloseCircleOutlined />} hideCluster>
      <div className='shield-content'>
        <LeftTree
          busiGroup={{
            // showNotGroupItem: true,
            onChange: busiChange,
          }}
        ></LeftTree>
        {curBusiItem?.id ? (
          <div className='shield-index'>
            <div className='header'>
              <div className='header-left'>
                <RefreshIcon
                  className='strategy-table-search-left-refresh'
                  onClick={() => {
                    refreshList();
                  }}
                />
                <ColumnSelect onClusterChange={(e) => setClusters(e)} />
                <Input onPressEnter={onSearchQuery} className={'searchInput'} prefix={<SearchOutlined />} placeholder={t('搜索标签、屏蔽原因')} />
              </div>
              <div className='header-right'>
                <Button
                  type='primary'
                  className='add'
                  ghost
                  onClick={() => {
                    history.push('/alert-mutes/add');
                  }}
                >
                  {t('新增屏蔽规则')}
                </Button>
              </div>
            </div>
            <Table
              rowKey='id'
              // sticky
              pagination={{
                total: currentShieldData.length,
                showQuickJumper: true,
                showSizeChanger: true,
                showTotal: (total) => {
                  return `共 ${total} 条数据`;
                },
                pageSizeOptions: pageSizeOptionsDefault,
                defaultPageSize: 30,
              }}
              loading={loading}
              dataSource={currentShieldData}
              columns={columns}
            />
          </div>
        ) : (
          <BlankBusinessPlaceholder text='屏蔽规则' />
        )}
      </div>
    </PageLayout>
  );
}
Example #14
Source File: index.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
Shield: React.FC = () => {
  const { t } = useTranslation();
  const history = useHistory();
  const [query, setQuery] = useState<string>('');
  const { curBusiItem } = useSelector<RootState, CommonStoreState>((state) => state.common);
  const [bgid, setBgid] = useState(undefined);
  const [clusters, setClusters] = useState<string[]>([]);
  const [currentShieldDataAll, setCurrentShieldDataAll] = useState<Array<subscribeItem>>([]);
  const [currentShieldData, setCurrentShieldData] = useState<Array<subscribeItem>>([]);
  const [loading, setLoading] = useState<boolean>(false);

  const columns: ColumnsType = [
    {
      title: t('集群'),
      dataIndex: 'cluster',
      render: (data) => {
        return <div>{data}</div>;
      },
    },
    {
      title: t('告警规则'),
      dataIndex: 'rule_name',
      render: (data) => {
        return <div>{data}</div>;
      },
    },
    {
      title: t('订阅标签'),
      dataIndex: 'tags',
      render: (text: any) => {
        return (
          <>
            {text
              ? text.map((tag, index) => {
                  return tag ? <div key={index}>{`${tag.key} ${tag.func} ${tag.func === 'in' ? tag.value.split(' ').join(', ') : tag.value}`}</div> : null;
                })
              : ''}
          </>
        );
      },
    },
    {
      title: t('告警接收组'),
      dataIndex: 'user_groups',
      render: (text: string, record: subscribeItem) => {
        return (
          <>
            {record.user_groups?.map((item) => (
              <ColorTag text={item.name} key={item.id}></ColorTag>
            ))}
          </>
        );
      },
    },

    {
      title: t('编辑人'),
      ellipsis: true,
      dataIndex: 'update_by',
    },
    {
      title: t('操作'),
      width: '128px',
      dataIndex: 'operation',
      render: (text: undefined, record: subscribeItem) => {
        return (
          <>
            <div className='table-operator-area'>
              <div
                className='table-operator-area-normal'
                style={{
                  cursor: 'pointer',
                  display: 'inline-block',
                }}
                onClick={() => {
                  curBusiItem?.id && history.push(`/alert-subscribes/edit/${record.id}`);
                }}
              >
                {t('编辑')}
              </div>
              <div
                className='table-operator-area-normal'
                style={{
                  cursor: 'pointer',
                  display: 'inline-block',
                }}
                onClick={() => {
                  curBusiItem?.id && history.push(`/alert-subscribes/edit/${record.id}?mode=clone`);
                }}
              >
                {t('克隆')}
              </div>
              <div
                className='table-operator-area-warning'
                style={{
                  cursor: 'pointer',
                  display: 'inline-block',
                }}
                onClick={() => {
                  confirm({
                    title: t('确定删除该订阅规则?'),
                    icon: <ExclamationCircleOutlined />,
                    onOk: () => {
                      dismiss(record.id);
                    },

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

  useEffect(() => {
    getList();
  }, [curBusiItem]);

  useEffect(() => {
    filterData();
  }, [query, clusters, currentShieldDataAll]);

  const dismiss = (id: number) => {
    deleteSubscribes({ ids: [id] }, curBusiItem.id).then((res) => {
      refreshList();
      if (res.err) {
        message.success(res.err);
      } else {
        message.success(t('删除成功'));
      }
    });
  };

  const filterData = () => {
    const data = JSON.parse(JSON.stringify(currentShieldDataAll));
    const res = data.filter((item: subscribeItem) => {
      const tagFind = item?.tags?.find((tag) => {
        return tag.key.indexOf(query) > -1 || tag.value.indexOf(query) > -1 || tag.func.indexOf(query) > -1;
      });
      const groupFind = item?.user_groups?.find((item) => {
        return item?.name?.indexOf(query) > -1;
      });
      return (item?.rule_name?.indexOf(query) > -1 || !!tagFind || !!groupFind) && ((clusters && clusters?.indexOf(item.cluster) > -1) || clusters?.length === 0);
    });
    setCurrentShieldData(res || []);
  };

  const getList = async () => {
    if (curBusiItem.id) {
      setLoading(true);
      const { success, dat } = await getSubscribeList({ id: curBusiItem.id });
      if (success) {
        setCurrentShieldDataAll(dat || []);
        setLoading(false);
      }
    }
  };

  const refreshList = () => {
    getList();
  };

  const onSearchQuery = (e) => {
    let val = e.target.value;
    setQuery(val);
  };

  const clusterChange = (data) => {
    setClusters(data);
  };

  const busiChange = (data) => {
    setBgid(data);
  };

  return (
    <PageLayout title={t('订阅规则')} icon={<CopyOutlined />} hideCluster>
      <div className='shield-content'>
        <LeftTree
          busiGroup={{
            // showNotGroupItem: true,
            onChange: busiChange,
          }}
        ></LeftTree>
        {curBusiItem?.id ? (
          <div className='shield-index'>
            <div className='header'>
              <div className='header-left'>
                <RefreshIcon
                  className='strategy-table-search-left-refresh'
                  onClick={() => {
                    refreshList();
                  }}
                />
                <ColumnSelect onClusterChange={(e) => setClusters(e)} />
                <Input onPressEnter={onSearchQuery} className={'searchInput'} prefix={<SearchOutlined />} placeholder={t('搜索规则、标签、接收组')} />
              </div>
              <div className='header-right'>
                <Button
                  type='primary'
                  className='add'
                  ghost
                  onClick={() => {
                    history.push('/alert-subscribes/add');
                  }}
                >
                  {t('新增订阅规则')}
                </Button>
              </div>
            </div>
            <Table
              rowKey='id'
              pagination={{
                total: currentShieldData.length,
                showQuickJumper: true,
                showSizeChanger: true,
                showTotal: (total) => {
                  return `共 ${total} 条数据`;
                },
                pageSizeOptions: pageSizeOptionsDefault,
                defaultPageSize: 30,
              }}
              loading={loading}
              dataSource={currentShieldData}
              columns={columns}
            />
          </div>
        ) : (
          <BlankBusinessPlaceholder text='订阅规则' />
        )}
      </div>
    </PageLayout>
  );
}
Example #15
Source File: ConditionalStyle.tsx    From datart with Apache License 2.0 4 votes vote down vote up
ConditionalStyle: FC<ItemLayoutProps<ChartStyleConfig>> = memo(
  ({
    ancestors,
    translate: t = title => title,
    data,
    onChange,
    dataConfigs,
    context,
  }) => {
    const [myData] = useState(() => CloneValueDeep(data));
    const [visible, setVisible] = useState<boolean>(false);
    const [dataSource, setDataSource] = useState<ConditionalStyleFormValues[]>(
      myData.value || [],
    );

    const [currentItem, setCurrentItem] = useState<ConditionalStyleFormValues>(
      {} as ConditionalStyleFormValues,
    );
    const onEditItem = (values: ConditionalStyleFormValues) => {
      setCurrentItem(CloneValueDeep(values));
      openConditionalStyle();
    };
    const onRemoveItem = (values: ConditionalStyleFormValues) => {
      const result: ConditionalStyleFormValues[] = dataSource.filter(
        item => item.uid !== values.uid,
      );

      setDataSource(result);
      onChange?.(ancestors, {
        ...myData,
        value: result,
      });
    };

    const tableColumnsSettings: ColumnsType<ConditionalStyleFormValues> = [
      {
        title: t('conditionalStyleTable.header.range.title'),
        dataIndex: 'range',
        width: 100,
        render: (_, { range }) => (
          <Tag>{t(`conditionalStyleTable.header.range.${range}`)}</Tag>
        ),
      },
      {
        title: t('conditionalStyleTable.header.operator'),
        dataIndex: 'operator',
      },
      {
        title: t('conditionalStyleTable.header.value'),
        dataIndex: 'value',
        render: (_, { value }) => <>{JSON.stringify(value)}</>,
      },
      {
        title: t('conditionalStyleTable.header.color.title'),
        dataIndex: 'value',
        render: (_, { color }) => (
          <>
            <Tag color={color.background}>
              {t('conditionalStyleTable.header.color.background')}
            </Tag>
            <Tag color={color.textColor}>
              {t('conditionalStyleTable.header.color.text')}
            </Tag>
          </>
        ),
      },
      {
        title: t('conditionalStyleTable.header.action'),
        dataIndex: 'action',
        width: 140,
        render: (_, record) => {
          return [
            <Button type="link" key="edit" onClick={() => onEditItem(record)}>
              {t('conditionalStyleTable.btn.edit')}
            </Button>,
            <Popconfirm
              key="remove"
              placement="topRight"
              title={t('conditionalStyleTable.btn.confirm')}
              onConfirm={() => onRemoveItem(record)}
            >
              <Button type="link" danger>
                {t('conditionalStyleTable.btn.remove')}
              </Button>
            </Popconfirm>,
          ];
        },
      },
    ];

    const openConditionalStyle = () => {
      setVisible(true);
    };
    const closeConditionalStyleModal = () => {
      setVisible(false);
      setCurrentItem({} as ConditionalStyleFormValues);
    };
    const submitConditionalStyleModal = (
      values: ConditionalStyleFormValues,
    ) => {
      let result: ConditionalStyleFormValues[] = [];

      if (values.uid) {
        result = dataSource.map(item => {
          if (item.uid === values.uid) {
            return values;
          }
          return item;
        });
      } else {
        result = [...dataSource, { ...values, uid: uuidv4() }];
      }

      setDataSource(result);
      closeConditionalStyleModal();
      onChange?.(ancestors, {
        ...myData,
        value: result,
      });
    };

    return (
      <StyledConditionalStylePanel direction="vertical">
        <Button type="primary" onClick={openConditionalStyle}>
          {t('conditionalStyleTable.btn.add')}
        </Button>
        <Row gutter={24}>
          <Col span={24}>
            <Table<ConditionalStyleFormValues>
              bordered={true}
              size="small"
              pagination={false}
              rowKey={record => record.uid!}
              columns={tableColumnsSettings}
              dataSource={dataSource}
            />
          </Col>
        </Row>
        <AddModal
          context={context}
          visible={visible}
          translate={t}
          values={currentItem}
          onOk={submitConditionalStyleModal}
          onCancel={closeConditionalStyleModal}
        />
      </StyledConditionalStylePanel>
    );
  },
  itemLayoutComparer,
)
Example #16
Source File: ScorecardConditionalStyle.tsx    From datart with Apache License 2.0 4 votes vote down vote up
ScorecardConditionalStyle: FC<ItemLayoutProps<ChartStyleConfig>> = memo(
  ({
    ancestors,
    translate: t = title => title,
    data,
    onChange,
    dataConfigs,
    context,
  }) => {
    const [myData] = useState(() => CloneValueDeep(data));
    const [allItems] = useState(() => {
      let results: ChartStyleSelectorItem[] = [];
      try {
        results =
          typeof myData?.options?.getItems === 'function'
            ? myData?.options?.getItems.call(
                null,
                dataConfigs?.map(col => AssignDeep(col)),
              ) || []
            : [];
      } catch (error) {
        console.error(`ListTemplatePanel | invoke action error ---> `, error);
      }
      return results;
    });

    const [visible, setVisible] = useState<boolean>(false);
    const [dataSource, setDataSource] = useState<
      ScorecardConditionalStyleFormValues[]
    >(
      myData?.value?.filter(item =>
        allItems.find(ac => ac.key === item.metricKey),
      ) || [],
    );

    const [currentItem, setCurrentItem] =
      useState<ScorecardConditionalStyleFormValues>(
        {} as ScorecardConditionalStyleFormValues,
      );
    const onEditItem = (values: ScorecardConditionalStyleFormValues) => {
      setCurrentItem(CloneValueDeep(values));
      openConditionalStyle();
    };
    const onRemoveItem = (values: ScorecardConditionalStyleFormValues) => {
      const result: ScorecardConditionalStyleFormValues[] = dataSource.filter(
        item => item.uid !== values.uid,
      );

      setDataSource(result);
      onChange?.(ancestors, {
        ...myData,
        value: result,
      });
    };

    const tableColumnsSettings: ColumnsType<ScorecardConditionalStyleFormValues> =
      [
        {
          title: t('viz.palette.data.metrics', true),
          dataIndex: 'metricKey',
          render: key => {
            return allItems.find(v => v.key === key)?.label;
          },
        },
        {
          title: t('conditionalStyleTable.header.operator'),
          dataIndex: 'operator',
        },
        {
          title: t('conditionalStyleTable.header.value'),
          dataIndex: 'value',
          render: (_, { value }) => <>{JSON.stringify(value)}</>,
        },
        {
          title: t('conditionalStyleTable.header.color.title'),
          dataIndex: 'value',
          render: (_, { color }) => (
            <>
              <Tag color={color.textColor}>
                {t('conditionalStyleTable.header.color.text')}
              </Tag>
              <Tag color={color.background}>
                {t('conditionalStyleTable.header.color.background')}
              </Tag>
            </>
          ),
        },
        {
          title: t('conditionalStyleTable.header.action'),
          dataIndex: 'action',
          width: 140,
          render: (_, record) => {
            return [
              <Button type="link" key="edit" onClick={() => onEditItem(record)}>
                {t('conditionalStyleTable.btn.edit')}
              </Button>,
              <Popconfirm
                key="remove"
                placement="topRight"
                title={t('conditionalStyleTable.btn.confirm')}
                onConfirm={() => onRemoveItem(record)}
              >
                <Button type="link" danger>
                  {t('conditionalStyleTable.btn.remove')}
                </Button>
              </Popconfirm>,
            ];
          },
        },
      ];

    const openConditionalStyle = () => {
      setVisible(true);
    };
    const closeConditionalStyleModal = () => {
      setVisible(false);
      setCurrentItem({} as ScorecardConditionalStyleFormValues);
    };
    const submitConditionalStyleModal = (
      values: ScorecardConditionalStyleFormValues,
    ) => {
      let result: ScorecardConditionalStyleFormValues[] = [];

      if (values.uid) {
        result = dataSource.map(item => {
          if (item.uid === values.uid) {
            return values;
          }
          return item;
        });
      } else {
        result = [...dataSource, { ...values, uid: uuidv4() }];
      }

      setDataSource(result);
      closeConditionalStyleModal();
      onChange?.(ancestors, {
        ...myData,
        value: result,
      });
    };

    return (
      <StyledScorecardConditionalStylePanel direction="vertical">
        <Button type="primary" onClick={openConditionalStyle}>
          {t('conditionalStyleTable.btn.add')}
        </Button>
        <Row gutter={24}>
          <Col span={24}>
            <Table<ScorecardConditionalStyleFormValues>
              bordered={true}
              size="small"
              pagination={false}
              rowKey={record => record.uid!}
              columns={tableColumnsSettings}
              dataSource={dataSource}
            />
          </Col>
        </Row>
        <AddModal
          context={context}
          visible={visible}
          translate={t}
          values={currentItem}
          allItems={allItems}
          onOk={submitConditionalStyleModal}
          onCancel={closeConditionalStyleModal}
        />
      </StyledScorecardConditionalStylePanel>
    );
  },
  itemLayoutComparer,
)
Example #17
Source File: Entries.tsx    From subscan-multisig-react with Apache License 2.0 4 votes vote down vote up
// eslint-disable-next-line complexity
export function Entries({
  source,
  isConfirmed,
  isCancelled,
  account,
  loading,
  totalCount,
  currentPage,
  onChangePage,
}: EntriesProps) {
  const { t } = useTranslation();
  const isInjected = useIsInjected();
  const { network } = useApi();

  const renderAction = useCallback(
    // eslint-disable-next-line complexity
    (row: Entry) => {
      if (row.status) {
        return <span>{t(`status.${row.status}`)}</span>;
      }

      const actions: TxActionType[] = [];
      // eslint-disable-next-line react/prop-types
      const pairs = (account.meta?.addressPair ?? []) as AddressPair[];
      const injectedAccounts: string[] = pairs.filter((pair) => isInjected(pair.address)).map((pair) => pair.address);

      if (injectedAccounts.includes(row.depositor)) {
        actions.push('cancel');
      }

      const localAccountInMultisigPairList = intersection(
        injectedAccounts,
        pairs.map((pair) => pair.address)
      );
      const approvedLocalAccounts = intersection(localAccountInMultisigPairList, row.approvals);

      if (approvedLocalAccounts.length !== localAccountInMultisigPairList.length) {
        actions.push('approve');
      }

      if (actions.length === 0) {
        // eslint-disable-next-line react/prop-types
        if (row.approvals && row.approvals.length === account.meta.threshold) {
          actions.push('pending');
        }
      }

      return (
        <Space>
          {actions.map((action) => {
            if (action === 'pending') {
              return (
                <Button key={action} disabled>
                  {t(action)}
                </Button>
              );
            } else if (action === 'approve') {
              return <TxApprove key={action} entry={row} />;
            } else {
              return <TxCancel key={action} entry={row} />;
            }
          })}
        </Space>
      );
    },
    [account.meta?.addressPair, account.meta.threshold, isInjected, t]
  );

  const columns: ColumnsType<Entry> = [
    {
      title: t(isConfirmed || isCancelled ? 'extrinsic_index' : 'call_data'),
      dataIndex: isConfirmed || isCancelled ? 'extrinsicIdx' : 'hexCallData',
      width: 300,
      align: 'left',
      // eslint-disable-next-line complexity
      render(data: string) {
        let extrinsicHeight = '';
        let extrinsicIndex = '';
        if ((isConfirmed || isCancelled) && data.split('-').length > 1) {
          extrinsicHeight = data.split('-')[0];
          extrinsicIndex = data.split('-')[1];
        }

        return !(isConfirmed || isCancelled) ? (
          <>
            <Typography.Text copyable={!isEmpty(data) && { text: data }}>
              {!isEmpty(data)
                ? // ? `${data.substring(0, CALL_DATA_LENGTH)}${data.length > CALL_DATA_LENGTH ? '...' : ''}`
                  toShortString(data, CALL_DATA_LENGTH)
                : '-'}
            </Typography.Text>
          </>
        ) : (
          <SubscanLink extrinsic={{ height: extrinsicHeight, index: extrinsicIndex }}>{data}</SubscanLink>
        );
      },
    },
    {
      title: t('actions'),
      dataIndex: 'callDataJson',
      align: 'left',
      render: renderMethod,
    },
    {
      title: t('progress'),
      dataIndex: 'approvals',
      align: 'left',
      render(approvals: string[]) {
        const cur = (approvals && approvals.length) || 0;

        return cur + '/' + account.meta.threshold;
      },
    },
    {
      title: t('status.index'),
      key: 'status',
      align: 'left',
      render: (_, row) => renderAction(row),
    },
  ];

  const expandedRowRender = (entry: Entry) => {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const progressColumnsNested: ColumnsType<any> = [
      { dataIndex: 'name', width: 100 },
      {
        width: 400,
        dataIndex: 'address',
        render: (address) => (
          <Space size="middle">
            <BaseIdentityIcon theme="polkadot" size={32} value={address} />
            <SubscanLink address={address} copyable />
          </Space>
        ),
      },
      {
        width: 250,
        key: 'status',
        render: (_, pair) => renderMemberStatus(entry, pair, network, !isCancelled && !isConfirmed),
      },
    ];
    // const callDataJson = entry.callData?.toJSON() ?? {};
    const args: Required<ArgObj>[] = ((entry.meta?.args ?? []) as Required<ArgObj>[]).map((arg) => {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const value = (entry.callDataJson?.args as any)[arg?.name ?? ''];

      return { ...arg, value };
    });

    return (
      <div className="record-expand bg-gray-100 py-3 px-5">
        <div className=" text-black-800 text-base leading-none mb-3">{t('progress')}</div>
        <div className="members">
          <Table
            columns={progressColumnsNested}
            dataSource={account.meta.addressPair as { key: string; name: string; address: string }[]}
            pagination={false}
            bordered
            rowKey="address"
            showHeader={false}
            className="mb-4 mx-4"
          />
        </div>
        <div className=" text-black-800 text-base leading-none my-3">{t('parameters')}</div>

        <Args
          args={args}
          className="mb-4 mx-4"
          section={entry.callDataJson?.section}
          method={entry.callDataJson?.method}
        />
      </div>
    );
  };

  return (
    <div className="record-table">
      <Table
        loading={loading}
        dataSource={source}
        columns={columns}
        rowKey={(record) => record.callHash ?? (record.blockHash as string)}
        pagination={
          isConfirmed || isCancelled
            ? {
                total: totalCount,
                pageSize: 10,
                current: currentPage,
                onChange: onChangePage,
              }
            : false
        }
        expandable={{
          expandedRowRender,
          expandIcon: genExpandIcon(),
          expandIconColumnIndex: 4,
        }}
        className="lg:block hidden"
      ></Table>

      <Space direction="vertical" className="lg:hidden block">
        {source.map((data) => {
          const { address, hash, callData, approvals } = data;
          const approvedCount = approvals.length || 0;
          const threshold = (account.meta.threshold as number) || 1;

          return (
            <Collapse key={address} expandIcon={() => <></>} className="wallet-collapse">
              <Panel
                header={
                  <Space direction="vertical" className="w-full mb-4">
                    <Typography.Text className="mr-4" copyable>
                      {hash}
                    </Typography.Text>

                    <div className="flex items-center">
                      <Typography.Text>{renderMethod(callData)}</Typography.Text>

                      <Progress
                        /* eslint-disable-next-line no-magic-numbers */
                        percent={parseInt(String((approvedCount / threshold) * 100), 10)}
                        steps={threshold}
                        className="ml-4"
                      />
                    </div>
                  </Space>
                }
                key={address}
                extra={renderAction(data)}
                className="overflow-hidden mb-4"
              >
                <MemberList
                  data={account}
                  statusRender={(pair) => renderMemberStatus(data, pair, network, !isCancelled && !isConfirmed)}
                />
              </Panel>
            </Collapse>
          );
        })}

        {!source.length && <Empty />}
      </Space>
    </div>
  );
}
Example #18
Source File: access.tsx    From shippo with MIT License 4 votes vote down vote up
Page_permission_access: React.FC = () => {
  const [data, setData] = useState<IPermissionAccess[]>()
  const editAccessDrawerRef = useRef<EditAccessDrawerRef>(null)

  const handleDle = useCallback((id: number) => {
    confirm({
      title: '确认删除?',
      icon: <ExclamationCircleOutlined />,
      content: '此操作不可逆',
      onOk() {
        console.log('OK')
        services.permissionAccess.del({ id }).then((hr) => {
          if (hr.data.success) {
            message.success('成功')
          } else {
            message.success('失败')
          }
        })
      },
      onCancel() {
        console.log('Cancel')
      },
    })
  }, [])

  const [columns, setColumns] = useState<ColumnsType<IPermissionAccess>>([
    {
      title: '访问规则ID',
      dataIndex: 'id',
      key: 'id',
    },
    {
      title: '访问规则表达式',
      dataIndex: 'accessRule',
      key: 'accessRule',
    },
    {
      title: '描述',
      dataIndex: 'remark',
      key: 'remark',
    },
    {
      title: '访问规则类型',
      dataIndex: 'accessType',
      key: 'accessType',
    },
    {
      title: '被引用次数',
      dataIndex: 'permissionAssociationCount',
      key: 'permissionAssociationCount',
    },
    {
      title: '创建时间',
      dataIndex: 'createdAt',
      key: 'createdAt',
    },
    {
      title: '操作',
      key: 'action',
      render: (_, record) => (
        <Space size="middle">
          <Button
            type="link"
            onClick={() => {
              editAccessDrawerRef.current?.open(record)
            }}
          >
            修改
          </Button>
          <Button
            type="link"
            onClick={() => {
              handleDle(record.id)
            }}
          >
            删除
          </Button>
        </Space>
      ),
    },
  ])

  const updateTable = useCallback(async () => {
    const hr = await services.permissionAccess.find_all()
    setData(
      hr.data.resource.map((item) => {
        return { ...item, createdAt: formatTimeStr(item.createdAt) }
      })
    )
  }, [])

  useMount(() => {
    updateTable()
  })

  return (
    <div>
      <EditAccessDrawer ref={editAccessDrawerRef} onClose={() => updateTable()} />
      <Space size="middle">
        <Button type="primary" onClick={() => editAccessDrawerRef.current?.open()}>
          新增访问规则
        </Button>
      </Space>
      <Table
        rowKey="id"
        columns={columns}
        dataSource={data}
        pagination={{ position: ['bottomCenter'] }}
        size="small"
      />
    </div>
  )
}
Example #19
Source File: business.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
Resource: React.FC = () => {
  const { t } = useTranslation();
  const urlQuery = useQuery();
  const id = urlQuery.get('id');
  const [visible, setVisible] = useState<boolean>(false);
  const [action, setAction] = useState<ActionType>();
  const [teamId, setTeamId] = useState<string>(id || '');
  const [memberId, setMemberId] = useState<string>('');
  const [memberList, setMemberList] = useState<{ user_group: any }[]>([]);
  const [memberTotal, setMemberTotal] = useState<number>(0);
  const [allMemberList, setAllMemberList] = useState<User[]>([]);
  const [teamInfo, setTeamInfo] = useState<{ name: string; id: number }>();
  const [teamList, setTeamList] = useState<Team[]>([]);
  const [query, setQuery] = useState<string>('');
  const [memberLoading, setMemberLoading] = useState<boolean>(false);
  const [searchValue, setSearchValue] = useState<string>('');
  const [searchMemberValue, setSearchMemberValue] = useState<string>('');
  const userRef = useRef(null as any);
  let { profile } = useSelector<RootState, accountStoreState>((state) => state.account);
  const dispatch = useDispatch();
  const teamMemberColumns: ColumnsType<any> = [
    {
      title: t('团队名称'),
      dataIndex: ['user_group', 'name'],
      ellipsis: true,
    },
    {
      title: t('团队备注'),
      dataIndex: ['user_group', 'note'],
      ellipsis: true,
      render: (text: string, record) => record['user_group'].note || '-',
    },
    {
      title: t('权限'),
      dataIndex: 'perm_flag',
    },
    {
      title: t('操作'),
      width: '100px',
      render: (text: string, record) => (
        <a
          style={{
            color: memberList.length > 1 ? 'red' : '#00000040',
          }}
          onClick={() => {
            if (memberList.length <= 1) return;

            let params = [
              {
                user_group_id: record['user_group'].id,
                busi_group_id: teamId,
              },
            ];
            confirm({
              title: t('是否删除该团队'),
              onOk: () => {
                deleteBusinessTeamMember(teamId, params).then((_) => {
                  message.success(t('团队删除成功'));
                  handleClose('deleteMember');
                });
              },
              onCancel: () => {},
            });
          }}
        >
          {t('删除')}
        </a>
      ),
    },
  ];

  useEffect(() => {
    teamId && getTeamInfoDetail(teamId);
  }, [teamId]);

  useEffect(() => {
    getTeamList();
  }, []);

  const getList = (action) => {
    getTeamList(undefined, action === 'delete');
  };

  // 获取业务组列表
  const getTeamList = (search?: string, isDelete?: boolean) => {
    let params = {
      query: search === undefined ? searchValue : search,
      limit: PAGE_SIZE,
    };
    getBusinessTeamList(params).then((data) => {
      setTeamList(data.dat || []);
      if ((!teamId || isDelete) && data.dat.length > 0) {
        setTeamId(data.dat[0].id);
      }
    });
  };

  // 获取业务组详情
  const getTeamInfoDetail = (id: string) => {
    setMemberLoading(true);
    getBusinessTeamInfo(id).then((data) => {
      dispatch({
        type: 'common/saveData',
        prop: 'curBusiItem',
        data: data,
      });
      setTeamInfo(data);
      setMemberList(data.user_groups);
      setMemberLoading(false);
    });
  };

  const handleClick = (type: ActionType, id?: string, memberId?: string) => {
    if (memberId) {
      setMemberId(memberId);
    } else {
      setMemberId('');
    }

    setAction(type);
    setVisible(true);
  };

  // 弹窗关闭回调
  const handleClose = (action) => {
    setVisible(false);
    if (['create', 'delete', 'update'].includes(action)) {
      getList(action);
      dispatch({ type: 'common/getBusiGroups' });
    }
    if (teamId && ['update', 'addMember', 'deleteMember'].includes(action)) {
      getTeamInfoDetail(teamId);
    }
  };

  return (
    <PageLayout title={t('业务组管理')} icon={<UserOutlined />} hideCluster>
      <div className='user-manage-content'>
        <div style={{ display: 'flex', height: '100%' }}>
          <div className='left-tree-area'>
            <div className='sub-title'>
              {t('业务组列表')}
              <Button
                style={{
                  height: '30px',
                }}
                size='small'
                type='link'
                onClick={() => {
                  handleClick(ActionType.CreateBusiness);
                }}
              >
                {t('新建业务组')}
              </Button>
            </div>
            <div style={{ display: 'flex', margin: '5px 0px 12px' }}>
              <Input
                prefix={<SearchOutlined />}
                placeholder={t('搜索业务组名称')}
                onPressEnter={(e) => {
                  // @ts-ignore
                  getTeamList(e.target.value);
                }}
                onBlur={(e) => {
                  // @ts-ignore
                  getTeamList(e.target.value);
                }}
              />
            </div>

            <List
              style={{
                marginBottom: '12px',
                flex: 1,
                overflow: 'auto',
              }}
              dataSource={teamList}
              size='small'
              renderItem={(item) => (
                <List.Item key={item.id} className={teamId == item.id ? 'is-active' : ''} onClick={() => setTeamId(item.id)}>
                  {item.name}
                </List.Item>
              )}
            />
          </div>
          {teamList.length > 0 ? (
            <div className='resource-table-content'>
              <Row className='team-info'>
                <Col
                  span='24'
                  style={{
                    color: '#000',
                    fontSize: '14px',
                    fontWeight: 'bold',
                    display: 'inline',
                  }}
                >
                  {teamInfo && teamInfo.name}
                  <EditOutlined
                    title={t('刷新')}
                    style={{
                      marginLeft: '8px',
                      fontSize: '14px',
                    }}
                    onClick={() => handleClick(ActionType.EditBusiness, teamId)}
                  ></EditOutlined>
                  <DeleteOutlined
                    style={{
                      marginLeft: '8px',
                      fontSize: '14px',
                    }}
                    onClick={() => {
                      confirm({
                        title: t('是否删除该业务组'),
                        onOk: () => {
                          deleteBusinessTeam(teamId).then((_) => {
                            message.success(t('业务组删除成功'));
                            handleClose('delete');
                          });
                        },
                        onCancel: () => {},
                      });
                    }}
                  />
                </Col>
                <Col
                  style={{
                    marginTop: '8px',
                    color: '#666',
                  }}
                >
                  {t('备注')}:{t('告警规则,告警事件,监控对象,自愈脚本等都归属业务组,是一个在系统里可以自闭环的组织')}
                </Col>
              </Row>
              <Row justify='space-between' align='middle'>
                <Col span='12'>
                  <Input
                    prefix={<SearchOutlined />}
                    value={searchMemberValue}
                    className={'searchInput'}
                    onChange={(e) => setSearchMemberValue(e.target.value)}
                    placeholder={t('搜索团队名称')}
                  />
                </Col>
                <Button
                  type='primary'
                  ghost
                  onClick={() => {
                    handleClick(ActionType.AddBusinessMember, teamId);
                  }}
                >
                  {t('添加团队')}
                </Button>
              </Row>

              <Table
                rowKey='id'
                columns={teamMemberColumns}
                dataSource={memberList && memberList.length > 0 ? memberList.filter((item) => item.user_group && item.user_group.name.indexOf(searchMemberValue) !== -1) : []}
                loading={memberLoading}
              />
            </div>
          ) : (
            <div className='blank-busi-holder'>
              <p style={{ textAlign: 'left', fontWeight: 'bold' }}>
                <InfoCircleOutlined style={{ color: '#1473ff' }} /> {t('提示信息')}
              </p>
              <p>
                业务组(监控对象、监控大盘、告警规则、自愈脚本都要归属某个业务组)为空,请先&nbsp;
                <a onClick={() => handleClick(ActionType.CreateBusiness)}>创建业务组</a>
              </p>
            </div>
          )}
        </div>
      </div>
      <UserInfoModal
        visible={visible}
        action={action as ActionType}
        userType={'business'}
        onClose={handleClose}
        teamId={teamId}
        onSearch={(val) => {
          setTeamId(val);
        }}
      />
    </PageLayout>
  );
}
Example #20
Source File: index.tsx    From fe-v5 with Apache License 2.0 4 votes vote down vote up
export default function Dashboard() {
  const { t } = useTranslation();
  const [form] = Form.useForm();
  const ref = useRef(null);
  const history = useHistory();
  const [isModalVisible, setIsModalVisible] = useState(false);
  const [modalType, setModalType] = useState<ModalStatus>(ModalStatus.None);
  const [selectRowKeys, setSelectRowKeys] = useState<number[]>([]);
  const [exportData, setExportData] = useState<string>('');
  const [editing, setEditing] = useState(false);
  const [query, setQuery] = useState<string>('');
  const [searchVal, setsearchVal] = useState<string>('');
  const [dashboardList, setDashboardList] = useState<DashboardType[]>();
  const [busiId, setBusiId] = useState<number>();

  const showModal = () => {
    setIsModalVisible(true);
  };

  const handleOk = async () => {
    await form.validateFields();

    if (editing) {
      await edit();
      message.success(t('编辑大盘成功'));
    } else {
      await create();
      message.success(t('新建大盘成功'));
    }

    (ref?.current as any)?.refreshList();
    setIsModalVisible(false);
    setEditing(false);
  };

  useEffect(() => {
    if (busiId) {
      getDashboard(busiId).then((res) => {
        if (searchVal && res.dat) {
          const filters = searchVal.split(' ');
          for (var i = 0; i < filters.length; i++) {
            res.dat = res.dat.filter((item) => item.name.includes(filters[i]) || item.tags.includes(filters[i]));
          }
        }
        setDashboardList(res.dat);
      });
    }
  }, [busiId, searchVal]);

  const create = async () => {
    let { name, tags } = form.getFieldsValue();
    return (
      busiId &&
      createDashboard(busiId, {
        name,
        tags,
      })
    );
  };

  const edit = async () => {
    let { name, tags, id } = form.getFieldsValue();
    return (
      busiId &&
      updateSingleDashboard(busiId, id, {
        name,
        tags,
        pure: true,
      })
    );
  };

  const handleEdit = (record: DashboardType) => {
    const { id, name, tags } = record;
    form.setFieldsValue({
      name,
      tags,
      id,
    });
    setIsModalVisible(true);
    setEditing(true);
  };

  const handleTagClick = (tag) => {
    const queryItem = query.length > 0 ? query.split(' ') : [];
    if (queryItem.includes(tag)) return;
    setQuery((query) => query + ' ' + tag);
    setsearchVal((searchVal) => searchVal + ' ' + tag);
  };

  const layout = {
    labelCol: {
      span: 4,
    },
    wrapperCol: {
      span: 16,
    },
  };
  const dashboardColumn: ColumnsType = [
    {
      title: t('大盘名称'),
      dataIndex: 'name',
      render: (text: string, record: DashboardType) => {
        const { t } = useTranslation();
        return (
          <div className='table-active-text' onClick={() => history.push(`/dashboard/${busiId}/${record.id}`)}>
            {text}
          </div>
        );
      },
    },
    {
      title: t('分类标签'),
      dataIndex: 'tags',
      render: (text: string[]) => (
        <>
          {text.map((tag, index) => {
            return tag ? (
              <Tag
                color='blue'
                key={index}
                style={{
                  cursor: 'pointer',
                }}
                onClick={() => handleTagClick(tag)}
              >
                {tag}
              </Tag>
            ) : null;
          })}
        </>
      ),
    },
    {
      title: t('更新时间'),
      dataIndex: 'update_at',
      render: (text: number) => dayjs(text * 1000).format('YYYY-MM-DD HH:mm:ss'),
    },
    {
      title: t('发布人'),
      dataIndex: 'create_by',
    },
    {
      title: t('操作'),
      width: '240px',
      render: (text: string, record: DashboardType) => (
        <div className='table-operator-area'>
          <div className='table-operator-area-normal' onClick={() => handleEdit(record)}>
            {t('编辑')}
          </div>
          <div
            className='table-operator-area-normal'
            onClick={async () => {
              confirm({
                title: `${t('是否克隆大盘')}${record.name}?`,
                onOk: async () => {
                  await cloneDashboard(busiId as number, record.id);
                  message.success(t('克隆大盘成功'));
                  (ref?.current as any)?.refreshList();
                },

                onCancel() {},
              });
            }}
          >
            {t('克隆')}
          </div>
          <div
            className='table-operator-area-warning'
            onClick={async () => {
              confirm({
                title: `${t('是否删除大盘')}:${record.name}?`,
                onOk: async () => {
                  await removeDashboard(busiId as number, record.id);
                  message.success(t('删除大盘成功'));
                  (ref?.current as any)?.refreshList();
                },

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

  const onSearchQuery = (e) => {
    let val = e.target.value;
    setsearchVal(val);
  };

  const handleImportDashboard = async (data) => {
    const { dat } = await importDashboard(busiId as number, data);
    return dat || {};
  };

  return (
    <PageLayout title={t('监控大盘')} icon={<FundViewOutlined />} hideCluster={true}>
      <div style={{ display: 'flex' }}>
        <LeftTree busiGroup={{ onChange: (id) => setBusiId(id) }}></LeftTree>
        {busiId ? (
          <div className='dashboard' style={{ flex: 1, overflow: 'auto' }}>
            <div className='table-handle'>
              <div className='table-handle-search'>
                <Input
                  onPressEnter={onSearchQuery}
                  className={'searchInput'}
                  value={query}
                  onChange={(e) => setQuery(e.target.value)}
                  prefix={<SearchOutlined />}
                  placeholder={t('大盘名称、分类标签')}
                />
              </div>
              <div className='table-handle-buttons'>
                <Button type='primary' onClick={showModal} ghost>
                  {t('新建大盘')}
                </Button>
                <div className={'table-more-options'}>
                  <Dropdown
                    overlay={
                      <ul className='ant-dropdown-menu'>
                        <li className='ant-dropdown-menu-item' onClick={() => setModalType(ModalStatus.BuiltIn)}>
                          <span>{t('导入监控大盘')}</span>
                        </li>
                        <li
                          className='ant-dropdown-menu-item'
                          onClick={async () => {
                            if (selectRowKeys.length) {
                              let exportData = await exportDashboard(busiId as number, selectRowKeys);
                              setExportData(JSON.stringify(exportData.dat, null, 2));
                              setModalType(ModalStatus.Export);
                            } else {
                              message.warning(t('未选择任何大盘'));
                            }
                          }}
                        >
                          <span>{t('导出监控大盘')}</span>
                        </li>
                        <li
                          className='ant-dropdown-menu-item'
                          onClick={() => {
                            if (selectRowKeys.length) {
                              confirm({
                                title: '是否批量删除大盘?',
                                onOk: async () => {
                                  const reuqests = selectRowKeys.map((id) => {
                                    console.log(id);
                                    return removeDashboard(busiId as number, id);
                                  });
                                  Promise.all(reuqests).then(() => {
                                    message.success(t('批量删除大盘成功'));
                                  });
                                  // TODO: 删除完后立马刷新数据有时候不是实时的,这里暂时间隔0.5s后再刷新列表
                                  setTimeout(() => {
                                    (ref?.current as any)?.refreshList();
                                  }, 500);
                                },
                                onCancel() {},
                              });
                            } else {
                              message.warning(t('未选择任何大盘'));
                            }
                          }}
                        >
                          <span>{t('批量删除大盘')}</span>
                        </li>
                      </ul>
                    }
                    trigger={['click']}
                  >
                    <Button onClick={(e) => e.stopPropagation()}>
                      {t('更多操作')}
                      <DownOutlined
                        style={{
                          marginLeft: 2,
                        }}
                      />
                    </Button>
                  </Dropdown>
                </div>
              </div>
            </div>
            <Table
              dataSource={dashboardList}
              className='dashboard-table'
              columns={dashboardColumn}
              pagination={{
                total: dashboardList?.length,
                showTotal(total: number) {
                  return `共 ${total} 条数据`;
                },
                pageSizeOptions: [30, 50, 100, 300],
                defaultPageSize: 30,
                showSizeChanger: true,
              }}
              rowKey='id'
              rowSelection={{
                selectedRowKeys: selectRowKeys,
                onChange: (selectedRowKeys: number[]) => {
                  setSelectRowKeys(selectedRowKeys);
                },
              }}
            ></Table>
          </div>
        ) : (
          <BlankBusinessPlaceholder text='监控大盘' />
        )}
      </div>
      <Modal
        title={editing ? t('编辑监控大盘') : t('创建新监控大盘')}
        visible={isModalVisible}
        onOk={handleOk}
        onCancel={() => {
          setIsModalVisible(false);
        }}
        destroyOnClose
      >
        <Form {...layout} form={form} preserve={false}>
          <Form.Item
            label={t('大盘名称')}
            name='name'
            wrapperCol={{
              span: 24,
            }}
            rules={[
              {
                required: true,
                message: t('请输入大盘名称'),
              },
            ]}
          >
            <Input />
          </Form.Item>
          <Form.Item
            wrapperCol={{
              span: 24,
            }}
            label={t('分类标签')}
            name='tags'
          >
            <Select
              mode='tags'
              dropdownStyle={{
                display: 'none',
              }}
              placeholder={t('请输入分类标签(请用回车分割)')}
            ></Select>
          </Form.Item>
          <Form.Item name='id' hidden>
            <Input />
          </Form.Item>
        </Form>
      </Modal>
      <ImportAndDownloadModal
        bgid={busiId}
        status={modalType}
        crossCluster={false}
        fetchBuiltinFunc={getBuiltinDashboards}
        submitBuiltinFunc={createBuiltinDashboards}
        onClose={() => {
          setModalType(ModalStatus.None);
        }}
        onSuccess={() => {
          (ref?.current as any)?.refreshList();
        }}
        onSubmit={handleImportDashboard}
        label='大盘'
        title={
          ModalStatus.Export === modalType ? (
            '大盘'
          ) : (
            <Tabs defaultActiveKey={ModalStatus.BuiltIn} onChange={(e: ModalStatus) => setModalType(e)} className='custom-import-alert-title'>
              <TabPane tab=' 导入内置大盘模块' key={ModalStatus.BuiltIn}></TabPane>
              <TabPane tab='导入大盘JSON' key={ModalStatus.Import}></TabPane>
            </Tabs>
          )
        }
        exportData={exportData}
      />
    </PageLayout>
  );
}
Example #21
Source File: ObjectAttrStruct.tsx    From next-basics with GNU General Public License v3.0 4 votes vote down vote up
export function LegacyObjectAttrStructForm(
  props: LegacyObjectAttrStructProps
): React.ReactElement {
  const [value, setValue] = React.useState<Partial<StructValueType>>({
    default: "",
    struct_define: [],
  });

  const [addStructMode, setAddStructMode] = React.useState("new");
  const [addStructModalVisible, setAddStructModalVisible] =
    React.useState(false);

  const [importStructModalVisible, setImportStructModalVisible] =
    React.useState(false);
  const [currentStruct, setCurrentStruct] = React.useState<StructDefine>(
    {} as StructDefine
  );
  const [cmdbObjectList, setCmdbObjectList] = React.useState<
    Partial<CmdbModels.ModelCmdbObject>[]
  >([]);
  const [selectedObjectId, setSelectedObjectId] = React.useState("");
  const [curValueType, setCurValueType] = React.useState("");

  const [selectedObjectAttrKeys, setSelectedObjectAttrKeys] = React.useState(
    []
  );
  const [loadingObject, setLoadingObject] = React.useState(false);

  const memoizeAttrList = React.useMemo(() => {
    if (selectedObjectId.length) {
      return cmdbObjectList
        .filter((object) => object.objectId === selectedObjectId)[0]
        ?.attrList.filter(
          (attr) => !["struct", "structs"].includes(attr.value.type)
        );
    } else {
      return [];
    }
  }, [cmdbObjectList, selectedObjectId]);

  const handleValueChange = (value: Partial<StructValueType>) => {
    setValue(value);
    props.onChange && props.onChange(value);
  };

  const { getFieldDecorator } = props.form;

  const handleDeleteStruct = (struct: StructDefine) => {
    Modal.confirm({
      title: i18n.t(`${NS_FORMS}:${K.NOTICE}`),
      content: (
        <>
          {i18n.t(`${NS_FORMS}:${K.DELETE_STRUCTURE_ITEM_PREFIX}`)}
          <Tag color="red">{struct.name}</Tag>
          {i18n.t(`${NS_FORMS}:${K.DELETE_STRUCTURE_ITEM_POSTFIX}`)}
        </>
      ),

      onOk() {
        const structs = value.struct_define.filter(
          (item) => item.id !== struct.id
        );

        handleValueChange({ ...value, struct_define: structs });
      },
    });
  };

  const loadCmdbObjectList = async () => {
    setCmdbObjectList((await CmdbObjectApi_getObjectAll({})).data);
    setLoadingObject(false);
  };

  const getOptionBtns = (record: any): React.ReactNode => (
    <div className="struct-option-btn-group" style={{ display: "flex" }}>
      {addStructMode === "new" && (
        <Button
          type="link"
          icon={<EditOutlined />}
          onClick={(e) => {
            setCurrentStruct(record);
            setCurValueType(record.type);
            setAddStructModalVisible(true);
          }}
        />
      )}

      <Button
        type="link"
        danger
        icon={<DeleteOutlined />}
        onClick={(e) => {
          handleDeleteStruct(record);
        }}
      />
    </div>
  );

  const DragHandle = SortableHandle(() => (
    <SwapOutlined className={styles.iconRotate} />
  ));

  const SortableItem = SortableElement((props: any) => <tr {...props} />);

  const SortableBody = SortableContainer((props: any) => <tbody {...props} />);

  const onSortEnd = ({ oldIndex, newIndex }: SortEnd) => {
    const dataSource = value?.struct_define;
    if (oldIndex !== newIndex) {
      const tempData = [].concat(dataSource);
      const temp = tempData[oldIndex];
      tempData[oldIndex] = tempData[newIndex];
      tempData[newIndex] = temp;
      handleValueChange({
        ...value,
        struct_define: tempData.filter((el) => !!el),
      });
    }
  };

  const DraggableContainer = (props: any) => (
    <SortableBody
      useDragHandle
      disableAutoscroll
      helperClass={styles["row-dragging"]}
      onSortEnd={onSortEnd}
      {...props}
    />
  );

  const DraggableBodyRow = ({ className, style, ...restProps }: any) => {
    const dataSource = value?.struct_define;
    // function findIndex base on Table rowKey props and should always be a right array index
    const index = dataSource.findIndex(
      (x) => x?.id === restProps["data-row-key"]
    );
    return <SortableItem index={index} {...restProps} />;
  };

  const structColumns = [
    {
      title: i18n.t(`${NS_FORMS}:${K.STRUCTURE_ITEM_ID}`),
      dataIndex: "id",
      key: "id",
    },

    {
      title: i18n.t(`${NS_FORMS}:${K.STRUCTURE_ITEM_NAME}`),
      dataIndex: "name",
      key: "name",
    },

    {
      title: i18n.t(`${NS_FORMS}:${K.TYPE}`),
      dataIndex: "type",
      key: "type",
      render: (text: string, record: any) =>
        valueTypeList.filter((type) => type.key === text)[0].text,
    },

    {
      title: i18n.t(`${NS_FORMS}:${K.HANDEL}`),
      key: "action",
      render: (text: string, record: any) => getOptionBtns(record),
    },

    {
      title: "排序",
      dataIndex: "sort",
      width: 70,
      className: styles["drag-visible"],
      render: () => <DragHandle />,
    },
  ];

  const structWithEnumColumns = [
    {
      title: i18n.t(`${NS_FORMS}:${K.STRUCTURE_ITEM_ID}`),
      dataIndex: "id",
      key: "id",
    },

    {
      title: i18n.t(`${NS_FORMS}:${K.STRUCTURE_ITEM_NAME}`),
      dataIndex: "name",
      key: "name",
    },

    {
      title: i18n.t(`${NS_FORMS}:${K.TYPE}`),
      dataIndex: "type",
      key: "type",
      render: (text: string, record: any) =>
        valueTypeList.filter((type) => type.key === text)[0].text,
    },

    {
      title: i18n.t(`${NS_FORMS}:${K.ENUM_REGEX_JSON}`),
      dataIndex: "regex",
      key: "regex",
      render: (text: any, record: { regex: any[]; type: string }) => {
        if (
          Array.isArray(record.regex) &&
          ["enums", "enum"].includes(record.type)
        ) {
          return record.regex.join(",") || "";
        } else if (record.type === "ip") {
          return IPRegex;
        }
        return record.regex || "";
      },
    },

    {
      title: i18n.t(`${NS_FORMS}:${K.HANDEL}`),
      key: "action",
      render: (text: any, record: any) => getOptionBtns(record),
    },

    {
      title: "排序",
      dataIndex: "sort",
      width: 70,
      className: styles["drag-visible"],
      render: () => <DragHandle />,
    },
  ];

  React.useEffect(() => {
    !isNil(props.value) && setValue(props.value);
  }, [props.value]);

  React.useEffect(() => {
    if (addStructMode !== "new") {
      loadCmdbObjectList();
    }
  }, [addStructMode]);

  const handleModeChange = (e: RadioChangeEvent) => {
    setAddStructMode(e.target.value);
    handleValueChange({ ...value, struct_define: [] });
    e.target.value !== "new" && setLoadingObject(true);
  };

  const handleAddStructConfirm = () => {
    //istanbul ignore next
    props.form.validateFields(async (err, data) => {
      if (err) {
        return;
      }
      const new_struct_define = [...value.struct_define];
      if (isEmpty(currentStruct)) {
        new_struct_define.push({ ...data, isNew: true });
      } else {
        const currentStructId = value.struct_define.findIndex(
          (item) => item.id === currentStruct.id
        );
        if (currentStructId !== -1) {
          new_struct_define[currentStructId] = {
            ...data,
            isNew: new_struct_define[currentStructId].isNew,
          };
        }
      }
      handleValueChange({
        ...value,
        struct_define: new_struct_define,
      });
      setAddStructModalVisible(false);
      setCurValueType("");
      props.form.resetFields();
    });
  };

  const rowSelection = {
    onChange: (
      selectedRowKeys: string[] | number[],
      selectedRows: StructDefine[]
    ) => {
      setSelectedObjectAttrKeys(selectedRowKeys);
    },
    selectedRowKeys: selectedObjectAttrKeys,
  };

  const handleObjectChange = (e: string) => {
    setSelectedObjectId(e);
    setSelectedObjectAttrKeys(
      cmdbObjectList
        .filter((object) => object.objectId === e)[0]
        ?.attrList.filter(
          (attr) => !["struct", "structs"].includes(attr.value.type)
        )
        .map((attr, index) => attr.id)
    );
  };

  return (
    <div>
      {i18n.t(`${NS_FORMS}:${K.STRUCTURE_BODY_DEFINATION}`)}
      <div>
        <Row>
          <Radio.Group value={addStructMode} onChange={handleModeChange}>
            <Radio value="new">
              {i18n.t(`${NS_FORMS}:${K.NEW_DEFINATION}`)}
            </Radio>
            <Radio value="import">{i18n.t(`${NS_FORMS}:${K.IFEM}`)}</Radio>
          </Radio.Group>
        </Row>
        <Row style={{ marginTop: 8 }}>
          {addStructMode === "new" ? (
            <Button
              icon={<PlusOutlined />}
              onClick={() => {
                setCurrentStruct({} as StructDefine);
                setAddStructModalVisible(true);
              }}
            >
              {i18n.t(`${NS_FORMS}:${K.ADD_STRUCTURE_ITEM}`)}
            </Button>
          ) : (
            <Button
              icon={<PlusOutlined />}
              onClick={() => {
                setImportStructModalVisible(true);
              }}
              loading={loadingObject}
            >
              {i18n.t(`${NS_FORMS}:${K.SELECT_MODEL}`)}
            </Button>
          )}
        </Row>
      </div>
      <div style={{ marginTop: 15 }}>
        <Table
          columns={
            (value?.struct_define?.some((item) => regexType.includes(item.type))
              ? structWithEnumColumns
              : structColumns) as ColumnsType<StructDefine>
          }
          dataSource={value?.struct_define}
          pagination={false}
          rowKey="id"
          components={{
            body: {
              wrapper: DraggableContainer,
              row: DraggableBodyRow,
            },
          }}
        />
      </div>
      <Modal
        title={
          isEmpty(currentStruct)
            ? i18n.t(`${NS_FORMS}:${K.TITLE_ADD_STRUCTURE_ITEM}`)
            : i18n.t(`${NS_FORMS}:${K.TITLE_EDIT_STRUCTURE_ITEM}`)
        }
        visible={addStructModalVisible}
        onOk={handleAddStructConfirm}
        onCancel={() => {
          setCurValueType("");
          props.form.resetFields();
          setAddStructModalVisible(false);
        }}
      >
        <Form
          labelCol={{ span: currentLang === "zh" ? 6 : 10 }}
          wrapperCol={{ span: 16 }}
        >
          <Form.Item label={i18n.t(`${NS_FORMS}:${K.STRUCTURE_ITEM_ID}`)}>
            {getFieldDecorator("id", {
              initialValue: isEmpty(currentStruct) ? "" : currentStruct.id,
              rules: [
                {
                  required: true,
                  message: i18n.t(`${NS_FORMS}:${K.INPUT_STRUCTURE_ITEM_ID}`),
                },
                {
                  pattern: /^[a-zA-Z][a-zA-Z_0-9]{0,31}$/gi,
                  message: i18n.t(`${NS_FORMS}:${K.STRUCTURE_ITEM_ID_LIMIT}`),
                },
                {
                  validator: (rule, curValue, cb) => {
                    const structIdArr =
                      value.struct_define?.map((r) => r.id) || [];
                    if (
                      currentStruct.id !== curValue &&
                      structIdArr.includes(curValue)
                    ) {
                      cb(
                        i18n.t(`${NS_FORMS}:${K.DUPLICATE_STRUCTURE_ITEM_ID}`, {
                          id: curValue,
                        })
                      );
                    }
                    cb();
                  },
                },
              ],
            })(<Input autoFocus />)}
          </Form.Item>
          <Form.Item label={i18n.t(`${NS_FORMS}:${K.STRUCTURE_ITEM_NAME}`)}>
            {getFieldDecorator("name", {
              initialValue: isEmpty(currentStruct) ? "" : currentStruct.name,
              rules: [
                {
                  required: true,
                  message: i18n.t(`${NS_FORMS}:${K.INPUT_STRUCTURE_ITEM_NAME}`),
                },
              ],
            })(<Input />)}
          </Form.Item>
          <Form.Item label={i18n.t(`${NS_FORMS}:${K.TYPE}`)}>
            {getFieldDecorator("type", {
              initialValue: isEmpty(currentStruct) ? "" : currentStruct.type,
              rules: [
                {
                  required: true,
                  message: i18n.t(`${NS_FORMS}:${K.ENTER_TYPE}`),
                },
              ],
            })(
              <Select
                onChange={(value) => setCurValueType(value as string)}
                style={{ width: "100%" }}
                disabled={!isEmpty(currentStruct) && !currentStruct?.isNew}
              >
                {valueTypeList
                  .filter(
                    (type) => type.key !== "struct" && type.key !== "structs"
                  )
                  .map((item) => (
                    <Option key={item.key} value={item.key}>
                      {item.text}
                    </Option>
                  ))}
              </Select>
            )}
          </Form.Item>
          {(curValueType === "enum" || curValueType === "enums") && (
            <Form.Item label={i18n.t(`${NS_FORMS}:${K.ENUMERATION_VALUE}`)}>
              {getFieldDecorator("regex", {
                initialValue:
                  isEmpty(currentStruct) || isNil(currentStruct.regex)
                    ? []
                    : currentStruct.regex,
              })(
                <Select
                  mode="tags"
                  style={{ width: "100%" }}
                  placeholder={i18n.t(
                    `${NS_FORMS}:${K.PLEASE_INPUT_ENUMERATED_VALUE}`
                  )}
                />
              )}
            </Form.Item>
          )}
          {(curValueType === "str" ||
            curValueType === "int" ||
            curValueType === "arr" ||
            curValueType === "json") && (
            <Form.Item
              label={
                curValueType === "json"
                  ? "JSON Schema:"
                  : i18n.t(`${NS_FORMS}:${K.REGULAR}`)
              }
            >
              {getFieldDecorator("regex", {
                initialValue: isEmpty(currentStruct) ? "" : currentStruct.regex,
              })(
                <Input
                  placeholder={i18n.t(`${NS_FORMS}:${K.THIS_IS_NOT_MANDATORY}`)}
                />
              )}
            </Form.Item>
          )}
          {curValueType === "ip" && (
            <Form.Item label={i18n.t(`${NS_FORMS}:${K.REGULAR}`)}>
              <Input.TextArea
                value={IPRegex}
                style={{ wordBreak: "break-all" }}
                disabled={true}
                autoSize={{ minRows: 4 }}
                resize={false}
              />
            </Form.Item>
          )}
        </Form>
      </Modal>
      <Modal
        title={i18n.t(`${NS_FORMS}:${K.CITE_MODEL}`)}
        visible={importStructModalVisible}
        onOk={() => {
          handleValueChange({
            ...value,
            struct_define: selectedObjectAttrKeys.map((id) => {
              const selectedRow = memoizeAttrList.filter(
                (attr) => attr.id === id
              )[0];
              return {
                id: selectedRow.id,
                name: selectedRow.name,
                type: selectedRow.value.type,
                regex: selectedRow.value.regex,
              };
            }),
          });

          setSelectedObjectId("");
          setImportStructModalVisible(false);
        }}
        onCancel={() => {
          setSelectedObjectId("");
          setImportStructModalVisible(false);
        }}
        width={800}
      >
        <Select
          showSearch
          placeholder={i18n.t(
            `${NS_FORMS}:${K.SELECT_ONE_CMDB_RESOURCE_MODEL}`
          )}
          filterOption={(input, option) =>
            option.props.children.toLowerCase().indexOf(input.toLowerCase()) >=
            0
          }
          onChange={handleObjectChange}
          value={selectedObjectId}
          style={{ width: "100%" }}
        >
          {cmdbObjectList.map(
            (object: Partial<CmdbModels.ModelCmdbObject>, index) => (
              <Option key={index} value={object.objectId}>
                {object.name}
              </Option>
            )
          )}
        </Select>
        {selectedObjectId.length > 0 && (
          <div style={{ marginTop: 15 }}>
            {i18n.t(`${NS_FORMS}:${K.SELECT_ATTRIBUTE}`)}
            <Table
              columns={objectAttrColumns}
              dataSource={memoizeAttrList}
              rowSelection={rowSelection as any}
              pagination={false}
              rowKey={"id"}
            />
          </div>
        )}
      </Modal>
    </div>
  );
}
Example #22
Source File: DevicesPage.tsx    From iot-center-v2 with MIT License 4 votes vote down vote up
DevicesPage: FunctionComponent<Props> = ({helpCollapsed}) => {
  const [loading, setLoading] = useState(true)
  const [message, setMessage] = useState<Message | undefined>(undefined)
  const [data, setData] = useState(NO_DEVICES)
  const [dataStamp, setDataStamp] = useState(0)
  const [lastEntries, setLastEntries] = useState(NO_ENTRIES)

  useEffect(() => {
    setLoading(true)
    const fetchDevices = async () => {
      try {
        const response = await fetch('/api/devices')
        if (response.status >= 300) {
          const text = await response.text()
          throw new Error(`${response.status} ${text}`)
        }
        const data = (await response.json()) as Array<DeviceInfo>
        setData(data)

        setLastEntries(
          await Promise.all(
            data.map(({deviceId}) => fetchLastEntryTime(deviceId))
          )
        )
      } catch (e) {
        setMessage({
          title: 'Cannot fetch data',
          description: String(e),
          type: 'error',
        })
      } finally {
        setLoading(false)
      }
    }
    fetchDevices()
  }, [dataStamp])

  const removeAuthorization = async (device: DeviceInfo) => {
    try {
      setLoading(true)
      const response = await fetch(`/api/devices/${device.deviceId}`, {
        method: 'DELETE',
      })
      if (response.status >= 300) {
        const text = await response.text()
        throw new Error(`${response.status} ${text}`)
      }
      setLoading(false)
      antdMessage.success(`Device ${device.deviceId} was unregistered`, 2)
    } catch (e) {
      setLoading(false)
      setMessage({
        title: 'Cannot remove device',
        description: String(e),
        type: 'error',
      })
    } finally {
      setDataStamp(dataStamp + 1)
    }
  }

  const addAuthorization = async (deviceId: string, deviceType: string) => {
    try {
      setLoading(true)
      const response = await fetch(`/api/env/${deviceId}`)
      if (response.status >= 300) {
        const text = await response.text()
        throw new Error(`${response.status} ${text}`)
      }
      const {newlyRegistered} = await response.json()

      if (newlyRegistered && deviceType !== '') {
        const setTypeResponse = await fetch(
          `/api/devices/${deviceId}/type/${deviceType}`,
          {
            method: 'POST',
          }
        )
        if (setTypeResponse.status >= 300) {
          const text = await setTypeResponse.text()
          throw new Error(`${setTypeResponse.status} ${text}`)
        }
      }

      setLoading(false)
      if (newlyRegistered) {
        antdMessage.success(`Device '${deviceId}' was registered`, 2)
      } else {
        antdMessage.success(`Device '${deviceId}' is already registered`, 2)
      }
    } catch (e) {
      setLoading(false)
      setMessage({
        title: 'Cannot register device',
        description: String(e),
        type: 'error',
      })
    } finally {
      setDataStamp(dataStamp + 1)
    }
  }

  // define table columns
  const columnDefinitions: ColumnsType<DeviceInfo> = [
    {
      title: 'Device ID',
      dataIndex: 'deviceId',
      defaultSortOrder: 'ascend',
      render: (deviceId: string) => (
        <Link to={`/devices/${deviceId}`}>{deviceId}</Link>
      ),
    },
    {
      title: 'Registration Time',
      dataIndex: 'createdAt',
      responsive: helpCollapsed ? ['lg'] : ['xxl'],
    },
    {
      title: 'Last Entry',
      dataIndex: 'deviceId',
      render: (id: string) => {
        const lastEntry = lastEntries.find(
          ({deviceId}) => deviceId === id
        )?.lastEntry
        if (lastEntry != null && lastEntry !== 0)
          return timeFormatter({
            timeZone: 'UTC',
            format: 'YYYY-MM-DD HH:mm:ss ZZ',
          })(lastEntry)
      },
      responsive: helpCollapsed ? ['xl'] : [],
    },
    {
      title: '',
      key: 'action',
      align: 'right',
      render: (_: string, device: DeviceInfo) => (
        <>
          <Tooltip title="Go to device settings" placement="topRight">
            <Button
              type="text"
              icon={<IconSettings />}
              href={`/devices/${device.deviceId}`}
            />
          </Tooltip>
          <Tooltip title="Go to device dashboard" placement="topRight">
            <Button
              type="text"
              icon={<IconDashboard />}
              href={`/dashboard/${device.deviceId}`}
            />
          </Tooltip>
          <Popconfirm
            icon={<ExclamationCircleFilled style={{color: 'red'}} />}
            title={`Are you sure to remove '${device.deviceId}' ?`}
            onConfirm={() => removeAuthorization(device)}
            okText="Yes"
            okType="danger"
            cancelText="No"
          >
            <Tooltip title="Remove device" placement="topRight" color="red">
              <Button type="text" icon={<IconDelete />} />
            </Tooltip>
          </Popconfirm>
        </>
      ),
    },
  ]

  return (
    <PageContent
      title="Device Registrations"
      spin={loading}
      message={message}
      titleExtra={
        <>
          <Tooltip title="Register a new Device">
            <Button
              onClick={() => {
                let deviceId = ''
                let deviceType = ''
                Modal.confirm({
                  title: 'Register Device',
                  icon: '',
                  content: (
                    <Form
                      name="registerDevice"
                      initialValues={{deviceId, deviceType}}
                    >
                      <Form.Item
                        name="deviceId"
                        rules={[
                          {required: true, message: 'Please input device ID !'},
                        ]}
                      >
                        <Input
                          placeholder="Device ID"
                          onChange={(e) => {
                            deviceId = e.target.value
                          }}
                        />
                        <Input
                          placeholder="Device type"
                          onChange={(e) => {
                            deviceType = e.target.value
                          }}
                        />
                      </Form.Item>
                    </Form>
                  ),
                  onOk: () => {
                    addAuthorization(deviceId, deviceType)
                  },
                  okText: 'Register',
                })
              }}
            >
              Register
            </Button>
          </Tooltip>
          <Tooltip title="Reload Table">
            <Button
              type="primary"
              onClick={() => setDataStamp(dataStamp + 1)}
              style={{marginRight: '8px'}}
            >
              Reload
            </Button>
          </Tooltip>
        </>
      }
    >
      <Table
        dataSource={data}
        columns={columnDefinitions}
        rowKey={deviceTableRowKey}
      />
    </PageContent>
  )
}
Example #23
Source File: DevicePage.tsx    From iot-center-v2 with MIT License 4 votes vote down vote up
DevicePage: FunctionComponent<
  RouteComponentProps<PropsRoute> & Props
> = ({match, location, helpCollapsed, mqttEnabled}) => {
  const deviceId = match.params.deviceId ?? VIRTUAL_DEVICE
  const [loading, setLoading] = useState(true)
  const [message, setMessage] = useState<Message | undefined>()
  const [deviceData, setDeviceData] = useState<DeviceData | undefined>()
  const [dataStamp, setDataStamp] = useState(0)
  const [progress, setProgress] = useState(-1)
  const writeAllowed =
    deviceId === VIRTUAL_DEVICE ||
    new URLSearchParams(location.search).get('write') === 'true'

  const isVirtualDevice = deviceId === VIRTUAL_DEVICE

  // fetch device configuration and data
  useEffect(() => {
    const fetchData = async () => {
      setLoading(true)
      try {
        const deviceConfig = await fetchDeviceConfig(deviceId)
        setDeviceData(await fetchDeviceData(deviceConfig))
      } catch (e) {
        console.error(e)
        setMessage({
          title: 'Cannot load device data',
          description: String(e),
          type: 'error',
        })
      } finally {
        setLoading(false)
      }
    }

    fetchData()
  }, [dataStamp, deviceId])

  async function writeData() {
    const onProgress: ProgressFn = (percent /*, current, total */) => {
      // console.log(
      //   `writeData ${current}/${total} (${Math.trunc(percent * 100) / 100}%)`
      // );
      setProgress(percent)
    }
    try {
      if (!deviceData) return
      const missingDataTimeStamps = mqttEnabled
        ? await fetchDeviceMissingDataTimeStamps(deviceData.config)
        : undefined
      const count = await writeEmulatedData(
        deviceData,
        onProgress,
        missingDataTimeStamps
      )
      if (count) {
        notification.success({
          message: (
            <>
              <b>{count}</b> measurement point{count > 1 ? 's were' : ' was'}{' '}
              written to InfluxDB.
            </>
          ),
        })
        setDataStamp(dataStamp + 1) // reload device data
      } else {
        notification.info({
          message: `No new data were written to InfluxDB, the current measurement is already written.`,
        })
      }
    } catch (e) {
      console.error(e)
      setMessage({
        title: 'Cannot write data',
        description: String(e),
        type: 'error',
      })
    } finally {
      setProgress(-1)
    }
  }

  const writeButtonDisabled = progress !== -1 || loading
  const pageControls = (
    <>
      {writeAllowed ? (
        <Tooltip title="Write Missing Data for the last 7 days" placement="top">
          <Button
            type="primary"
            onClick={writeData}
            disabled={writeButtonDisabled}
            icon={<IconWriteData />}
          />
        </Tooltip>
      ) : undefined}
      <Tooltip title="Reload" placement="topRight">
        <Button
          disabled={loading}
          loading={loading}
          onClick={() => setDataStamp(dataStamp + 1)}
          icon={<IconRefresh />}
        />
      </Tooltip>
      <Tooltip title="Go to device realtime dashboard" placement="topRight">
        <Button
          type={mqttEnabled ? 'default' : 'ghost'}
          icon={<PlayCircleOutlined />}
          href={`/realtime/${deviceId}`}
        ></Button>
      </Tooltip>
      <Tooltip title="Go to device dashboard" placement="topRight">
        <Button
          icon={<IconDashboard />}
          href={`/dashboard/${deviceId}`}
        ></Button>
      </Tooltip>
    </>
  )

  const columnDefinitions: ColumnsType<measurementSummaryRow> = [
    {
      title: 'Field',
      dataIndex: '_field',
    },
    {
      title: 'min',
      dataIndex: 'minValue',
      render: (val: number) => +val.toFixed(2),
      align: 'right',
    },
    {
      title: 'max',
      dataIndex: 'maxValue',
      render: (val: number) => +val.toFixed(2),
      align: 'right',
    },
    {
      title: 'max time',
      dataIndex: 'maxTime',
    },
    {
      title: 'entry count',
      dataIndex: 'count',
      align: 'right',
    },
    {
      title: 'sensor',
      dataIndex: 'sensor',
    },
  ]

  return (
    <PageContent
      title={
        isVirtualDevice ? (
          <>
            {'Virtual Device'}
            <Tooltip title="This page writes temperature measurements for the last 7 days from an emulated device, the temperature is reported every minute.">
              <InfoCircleFilled style={{fontSize: '1em', marginLeft: 5}} />
            </Tooltip>
          </>
        ) : (
          `Device ${deviceId}`
        )
      }
      message={message}
      spin={loading}
      titleExtra={pageControls}
    >
      {deviceId === VIRTUAL_DEVICE ? (
        <>
          <div style={{visibility: progress >= 0 ? 'visible' : 'hidden'}}>
            <Progress
              percent={progress >= 0 ? Math.trunc(progress) : 0}
              strokeColor={COLOR_LINK}
            />
          </div>
        </>
      ) : undefined}
      <GridDescription
        title="Device Configuration"
        column={
          helpCollapsed ? {xxl: 3, xl: 2, md: 1, sm: 1} : {xxl: 2, md: 1, sm: 1}
        }
        descriptions={[
          {
            label: 'Device ID',
            value: deviceData?.config.id,
          },
          {
            label: 'Registration Time',
            value: deviceData?.config.createdAt,
          },
          {
            label: 'InfluxDB URL',
            value: deviceData?.config.influx_url,
          },
          {
            label: 'InfluxDB Organization',
            value: deviceData?.config.influx_org,
          },
          {
            label: 'InfluxDB Bucket',
            value: deviceData?.config.influx_bucket,
          },
          {
            label: 'InfluxDB Token',
            value: deviceData?.config.influx_token ? '***' : 'N/A',
          },
          ...(mqttEnabled
            ? [
                {
                  label: 'Mqtt URL',
                  value: deviceData?.config?.mqtt_url,
                },
                {
                  label: 'Mqtt topic',
                  value: deviceData?.config?.mqtt_topic,
                },
              ]
            : []),
          {
            label: 'Device type',
            value: (
              <InputConfirm
                value={deviceData?.config?.device}
                tooltip={'Device type is used for dynamic dashboard filtering'}
                onValueChange={async (newValue) => {
                  try {
                    await fetchSetDeviceType(deviceId, newValue)
                    setDataStamp(dataStamp + 1)
                  } catch (e) {
                    console.error(e)
                    setMessage({
                      title: 'Cannot load device data',
                      description: String(e),
                      type: 'error',
                    })
                  }
                }}
              />
            ),
          },
        ]}
      />
      <Title>Measurements</Title>
      <Table
        dataSource={deviceData?.measurements}
        columns={columnDefinitions}
        pagination={false}
        rowKey={measurementTableRowKey}
      />
      <div style={{height: 20}} />
      {isVirtualDevice && mqttEnabled ? (
        <RealTimeSettings onBeforeStart={writeData} />
      ) : undefined}
    </PageContent>
  )
}
Example #24
Source File: index.tsx    From antdp with MIT License 4 votes vote down vote up
EditableTable = (
  props: EditableTableProps,
  ref: React.ForwardedRef<RefEditTableProps>,
) => {
  const {
    columns,
    dataSource = [],
    onBeforeSave,
    onSave,
    rowKey = 'id',
    optIsFirst = false,
    optConfig = {},
    isOptDelete = false,
    initValue = {},
    onValuesChange,
    isAdd,
    onErr,
    multiple = false,
    onBeforeAdd,
    isOpt = true,
    addBtnProps = {},
    store,
    ...rest
  } = props;
  const [formsRef] = useStore(store)


  const [editingKey, setEditingKey] = useState<string[]>([]);
  const [newAdd, setNewAdd] = React.useState<string[]>([]);
  /** editingKey 和 newAdd 移出 id */
  const removeKey = (id: string | number) => {
    setEditingKey((arr) => arr.filter((k) => `${k}` !== `${id}`));
    setNewAdd((arr) => arr.filter((k) => `${k}` !== `${id}`));
  };

  /** 获取行 所有编辑字段 */
  const fields: string[] = React.useMemo(() => {
    return columns
      .filter((item) => {
        return item.editable;
      })
      .map((item) => item.dataIndex as string);
  }, [columns]);

  /** 重置 某个表单 */
  const restForm = (key: string | number, obj = {}) => {
    const stores = formsRef.getStore();
    if (stores[`${key}`]) {
      stores[`${key}`].setFieldsValue(obj);
    }
  };

  /** 获取某个表单 */
  const getForm = (id: string | number) => {
    const stores = formsRef.getStore();
    return stores[`${id}`];
  };

  /** 判断是否编辑 */
  const isEditing = (record: any) => editingKey.includes(`${record[rowKey]}`);
  /** 判断是否是新增的 */
  const isAddEdit = (record: any) => newAdd.includes(`${record[rowKey]}`);

  /** 新增  */
  const add = () => {
    // 新增之前的调用方法
    if (onBeforeAdd && !onBeforeAdd()) {
      return;
    }
    if (newAdd.length === 1 && !multiple) {
      message.warn('只能新增一行');
      return;
    }
    if (editingKey.length === 1 && !multiple) {
      message.warn('只能编辑一行');
      return;
    }
    const id = (new Date().getTime() * Math.round(10)).toString();
    const newItem = { ...(initValue || {}), [rowKey]: id };
    const list = dataSource.concat([newItem]);
    setEditingKey((arr) => arr.concat([id]));
    setNewAdd((arr) => arr.concat([id]));
    onSave && onSave(list, newItem);
  };

  /** 编辑 */
  const edit = (record: any) => {
    let obj = { ...record };
    restForm(record[rowKey], obj);
    setEditingKey((arr) => arr.concat([`${record[rowKey]}`]));
  };

  /** 取消编辑  */
  const cancel = (id: string | number) => {
    removeKey(id);
    restForm(id, {});
  };

  /** 删除行 */
  const onDelete = (id: string | number, rowItem: object, index: number) => {
    const list = dataSource.filter((item) => `${item[rowKey]}` !== `${id}`);
    removeKey(id);
    onSave && onSave(list, rowItem, rowItem, index);
  };

  /** 保存 */
  const save = async (key: string | number, record: object, indx: number) => {
    try {
      const row = await getForm(key).validateFields();
      if (onBeforeSave && !onBeforeSave(row, record, indx)) {
        return;
      }
      const newData = [...dataSource];
      const index = newData.findIndex((item) => `${key}` === `${item[rowKey]}`);
      if (index > -1) {
        const item = newData[index];
        newData.splice(index, 1, { ...item, ...row });
      } else {
        newData.push(row);
      }
      onSave && onSave(newData, row, record, indx);
      removeKey(key);
      getForm(key).resetFields(fields);
    } catch (errInfo) {
      onErr && onErr(errInfo as ValidateErrorEntity<any>);
    }
  };
  /** 操作列配置 */
  const operation: ColumnsProps[] =
    (isOpt &&
      Operation({
        optConfig,
        isEditing,
        isAddEdit,
        save,
        isOptDelete,
        cancel,
        onDelete,
        edit,
        newAdd,
        editingKey,
        rowKey,
        multiple,
      })) ||
    [];

  const optColumns = optIsFirst
    ? operation.concat(columns)
    : columns.concat(operation);

  const mergedColumns = optColumns.map((col) => {
    if (!col.editable) {
      return col;
    }
    return {
      ...col,
      onCell: (record: any) => ({
        record,
        multiple,
        rowKey,
        dataIndex: col.dataIndex,
        title: col.title,
        editing: isEditing(record),
        inputNode: col.inputNode,
        rules: col.rules || [],
        itemAttr: col.itemAttr,
        type: col.type,
        attr: col.attr,
        tip: col.tip,
        tipAttr: col.tipAttr,
        isList: col.isList,
        listAttr: col.listAttr,
      }),
    };
  }) as ColumnsType<any>;
  // 表单值更新 表单更新值适用单个 不使用多个
  const onChange = (
    id: string | number,
    form: FormInstance,
    value: any,
    allValue: any,
  ) => {
    if (onValuesChange) {
      const list = dataSource.map((item) => {
        if (`${id}` === `${item[rowKey]}`) {
          return { ...item, ...allValue };
        }
        return { ...item };
      });
      onValuesChange(list, value, allValue, id, form);
    }
  };

  React.useImperativeHandle(
    ref,
    (): RefEditTableProps => ({
      save,
      onDelete,
      edit,
      cancel,
      add,
      isEditing,
      editingKey,
      newAdd,
      forms: formsRef,
    }),
  );

  return (
    <React.Fragment>
      <EditForms.Provider
        value={{
          formsRef,
          onValuesChange: onChange,
          dataSource,
          rowKey,
        }}
      >
        <Table
          size="small"
          bordered
          {...rest}
          components={{
            body: {
              row: Tr,
              cell: Td,
            },
          }}
          rowKey={rowKey}
          dataSource={dataSource}
          columns={mergedColumns}
          rowClassName="editable-row"
          pagination={false}
        />
        {isAdd && (
          <Button
            type="dashed"
            block
            children="添加一行数据"
            {...(addBtnProps || {})}
            style={{ marginTop: 10, ...((addBtnProps || {}).style || {}) }}
            onClick={add}
          />
        )}
      </EditForms.Provider>
    </React.Fragment>
  );
}
Example #25
Source File: RetentionTable.tsx    From posthog-foss with MIT License 4 votes vote down vote up
export function RetentionTable({ dashboardItemId = null }: { dashboardItemId?: number | null }): JSX.Element | null {
    const { insightProps } = useValues(insightLogic)
    const logic = retentionTableLogic(insightProps)
    const {
        results: _results,
        resultsLoading,
        peopleLoading,
        people: _people,
        loadingMore,
        filters: { period, date_to, breakdowns },
        aggregationTargetLabel,
    } = useValues(logic)
    const results = _results as RetentionTablePayload[]
    const people = _people as RetentionTablePeoplePayload

    const { loadPeople, loadMorePeople } = useActions(logic)
    const [modalVisible, setModalVisible] = useState(false)
    const [selectedRow, selectRow] = useState(0)
    const [isLatestPeriod, setIsLatestPeriod] = useState(false)

    useEffect(() => {
        setIsLatestPeriod(periodIsLatest(date_to || null, period || null))
    }, [date_to, period])

    const columns: ColumnsType<Record<string, any>> = [
        {
            title: 'Cohort',
            key: 'cohort',
            render: (row: RetentionTablePayload) =>
                // If we have breakdowns, then use the returned label attribute
                // as the cohort name, otherwise we construct one ourselves
                // based on the returned date. It might be nice to just unify to
                // have label computed as such from the API.
                breakdowns?.length
                    ? row.label
                    : period === 'Hour'
                    ? dayjs(row.date).format('MMM D, h A')
                    : dayjs.utc(row.date).format('MMM D'),
            align: 'center',
        },
        {
            title: 'Cohort Size',
            key: 'users',
            render: (row) => row.values[0]['count'],
            align: 'center',
        },
    ]

    if (!resultsLoading && results) {
        if (results.length === 0) {
            return null
        }
        const maxIntervalsCount = Math.max(...results.map((result) => result.values.length))
        columns.push(
            ...Array.from(Array(maxIntervalsCount).keys()).map((index: number) => ({
                key: `period::${index}`,
                title: `${period} ${index}`,
                render: (row: RetentionTablePayload) => {
                    if (index >= row.values.length) {
                        return ''
                    }
                    return renderPercentage(
                        row.values[index]['count'],
                        row.values[0]['count'],
                        isLatestPeriod && index === row.values.length - 1,
                        index === 0
                    )
                },
            }))
        )
    }

    function dismissModal(): void {
        setModalVisible(false)
    }

    function loadMore(): void {
        loadMorePeople()
    }

    return (
        <>
            <Table
                data-attr="retention-table"
                size="small"
                className="retention-table"
                pagination={false}
                rowClassName={dashboardItemId ? '' : 'cursor-pointer'}
                dataSource={results}
                columns={columns}
                rowKey="date"
                loading={resultsLoading}
                onRow={(_, rowIndex: number | undefined) => ({
                    onClick: () => {
                        if (!dashboardItemId && rowIndex !== undefined) {
                            loadPeople(rowIndex)
                            setModalVisible(true)
                            selectRow(rowIndex)
                        }
                    },
                })}
            />
            {results && (
                <RetentionModal
                    results={results}
                    actors={people}
                    selectedRow={selectedRow}
                    visible={modalVisible}
                    dismissModal={dismissModal}
                    actorsLoading={peopleLoading}
                    loadMore={loadMore}
                    loadingMore={loadingMore}
                    aggregationTargetLabel={aggregationTargetLabel}
                />
            )}
        </>
    )
}
Example #26
Source File: FunnelStepTable.tsx    From posthog-foss with MIT License 4 votes vote down vote up
export function FunnelStepTable(): JSX.Element | null {
    const { insightProps, isViewedOnDashboard } = useValues(insightLogic)
    const logic = funnelLogic(insightProps)
    const {
        stepsWithCount,
        flattenedSteps,
        steps,
        visibleStepsWithConversionMetrics,
        hiddenLegendKeys,
        barGraphLayout,
        flattenedStepsByBreakdown,
        flattenedBreakdowns,
        aggregationTargetLabel,
        isModalActive,
        filters,
    } = useValues(logic)
    const { openPersonsModalForStep, toggleVisibilityByBreakdown, setHiddenById } = useActions(logic)
    const { cohorts } = useValues(cohortsModel)
    const showLabels = false // #7653 - replaces (visibleStepsWithConversionMetrics?.[0]?.nested_breakdown?.length ?? 0) < 6
    const isOnlySeries = flattenedBreakdowns.length === 1

    function getColumns(): ColumnsType<FlattenedFunnelStep> | ColumnsType<FlattenedFunnelStepByBreakdown> {
        if (barGraphLayout === FunnelLayout.vertical) {
            const _columns: ColumnsType<FlattenedFunnelStepByBreakdown> = []

            if (!isViewedOnDashboard) {
                _columns.push({
                    render: function RenderCheckbox({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
                        const checked = !!flattenedBreakdowns?.every(
                            (b) =>
                                !hiddenLegendKeys[
                                    getVisibilityIndex(visibleStepsWithConversionMetrics?.[0], b.breakdown_value)
                                ]
                        )
                        const color = getSeriesColor(breakdown?.breakdownIndex, isOnlySeries)

                        return renderGraphAndHeader(
                            rowIndex,
                            0,
                            <PHCheckbox
                                color={color}
                                checked={
                                    !hiddenLegendKeys[
                                        getVisibilityIndex(
                                            visibleStepsWithConversionMetrics?.[0],
                                            breakdown.breakdown_value
                                        )
                                    ]
                                } // assume visible status from first step's visibility
                                onChange={() => toggleVisibilityByBreakdown(breakdown.breakdown_value)}
                            />,
                            <PHCheckbox
                                color={isOnlySeries ? 'var(--primary)' : undefined}
                                checked={checked}
                                indeterminate={flattenedBreakdowns?.some(
                                    (b) =>
                                        !hiddenLegendKeys[
                                            getVisibilityIndex(
                                                visibleStepsWithConversionMetrics?.[0],
                                                b.breakdown_value
                                            )
                                        ]
                                )}
                                onChange={() => {
                                    // either toggle all data on or off
                                    setHiddenById(
                                        Object.fromEntries(
                                            visibleStepsWithConversionMetrics.flatMap((s) =>
                                                flattenedBreakdowns.map((b) => [
                                                    getVisibilityIndex(s, b.breakdown_value),
                                                    checked,
                                                ])
                                            )
                                        )
                                    )
                                }}
                            />,
                            showLabels,
                            undefined,
                            isViewedOnDashboard
                        )
                    },
                    fixed: 'left',
                    width: 20,
                    align: 'center',
                })

                _columns.push({
                    render: function RenderLabel({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
                        const color = getSeriesColor(breakdown?.breakdownIndex, isOnlySeries)

                        return renderGraphAndHeader(
                            rowIndex,
                            1,
                            <InsightLabel
                                seriesColor={color}
                                fallbackName={formatBreakdownLabel(
                                    cohorts,
                                    isOnlySeries ? `Unique ${aggregationTargetLabel.plural}` : breakdown.breakdown_value
                                )}
                                hasMultipleSeries={steps.length > 1}
                                breakdownValue={breakdown.breakdown_value}
                                hideBreakdown={false}
                                iconSize={IconSize.Small}
                                iconStyle={{ marginRight: 12 }}
                                allowWrap
                                hideIcon
                                pillMaxWidth={165}
                            />,
                            renderColumnTitle('Breakdown'),
                            showLabels,
                            undefined,
                            isViewedOnDashboard
                        )
                    },
                    fixed: 'left',
                    width: 150,
                    className: 'funnel-table-cell breakdown-label-column',
                })

                _columns.push({
                    render: function RenderCompletionRate({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
                        return renderGraphAndHeader(
                            rowIndex,
                            2,
                            <span>{formatDisplayPercentage(breakdown?.conversionRates?.total ?? 0)}%</span>,
                            renderSubColumnTitle('Rate'),
                            showLabels,
                            undefined,
                            isViewedOnDashboard
                        )
                    },
                    fixed: 'left',
                    width: 120,
                    align: 'right',
                    className: 'funnel-table-cell dividing-column',
                })
            }

            // Add columns per step
            visibleStepsWithConversionMetrics.forEach((step, stepIndex) => {
                _columns.push({
                    render: function RenderCompleted({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
                        const breakdownStep = breakdown.steps?.[step.order]
                        return renderGraphAndHeader(
                            rowIndex,
                            step.order === 0 ? 3 : (stepIndex - 1) * 5 + 5,
                            breakdownStep?.count != undefined ? (
                                <ValueInspectorButton
                                    onClick={() => openPersonsModalForStep({ step: breakdownStep, converted: true })}
                                    disabled={!isModalActive}
                                >
                                    {breakdown.steps?.[step.order].count}
                                </ValueInspectorButton>
                            ) : (
                                EmptyValue
                            ),
                            renderSubColumnTitle(
                                <>
                                    <UserOutlined
                                        title={`Unique ${aggregationTargetLabel.plural} ${
                                            filters.aggregation_group_type_index != undefined ? 'that' : 'who'
                                        } completed this step`}
                                    />{' '}
                                    Completed
                                </>
                            ),
                            showLabels,
                            step,
                            isViewedOnDashboard
                        )
                    },
                    width: 80,
                    align: 'right',
                })

                _columns.push({
                    render: function RenderConversion({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
                        return renderGraphAndHeader(
                            rowIndex,
                            step.order === 0 ? 4 : (stepIndex - 1) * 5 + 6,
                            breakdown.steps?.[step.order]?.conversionRates.fromBasisStep != undefined ? (
                                <>
                                    {getSignificanceFromBreakdownStep(breakdown, step.order)?.fromBasisStep ? (
                                        <Tooltip title="Significantly different from other breakdown values">
                                            <span className="table-text-highlight">
                                                <FlagOutlined style={{ marginRight: 2 }} />{' '}
                                                {formatDisplayPercentage(
                                                    breakdown.steps?.[step.order]?.conversionRates.fromBasisStep
                                                )}
                                                %
                                            </span>
                                        </Tooltip>
                                    ) : (
                                        <span>
                                            {formatDisplayPercentage(
                                                breakdown.steps?.[step.order]?.conversionRates.fromBasisStep
                                            )}
                                            %
                                        </span>
                                    )}
                                </>
                            ) : (
                                EmptyValue
                            ),
                            renderSubColumnTitle('Rate'),
                            showLabels,
                            step,
                            isViewedOnDashboard
                        )
                    },
                    width: 80,
                    align: 'right',
                    className: step.order === 0 ? 'funnel-table-cell dividing-column' : undefined,
                })

                if (step.order !== 0) {
                    _columns.push({
                        render: function RenderDropoff({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
                            const breakdownStep = breakdown.steps?.[step.order]
                            return renderGraphAndHeader(
                                rowIndex,
                                (stepIndex - 1) * 5 + 7,
                                breakdownStep?.droppedOffFromPrevious != undefined ? (
                                    <ValueInspectorButton
                                        onClick={() =>
                                            openPersonsModalForStep({
                                                step: breakdownStep,
                                                converted: false,
                                            })
                                        }
                                        disabled={!isModalActive}
                                    >
                                        {breakdown.steps?.[step.order]?.droppedOffFromPrevious}
                                    </ValueInspectorButton>
                                ) : (
                                    EmptyValue
                                ),
                                renderSubColumnTitle(
                                    <>
                                        <UserDeleteOutlined
                                            title={`Unique ${aggregationTargetLabel.plural} who dropped off on this step`}
                                        />{' '}
                                        Dropped
                                    </>
                                ),
                                showLabels,
                                step,
                                isViewedOnDashboard
                            )
                        },
                        width: 80,
                        align: 'right',
                    })

                    _columns.push({
                        render: function RenderDropoffRate({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
                            return renderGraphAndHeader(
                                rowIndex,
                                (stepIndex - 1) * 5 + 8,
                                breakdown.steps?.[step.order]?.conversionRates.fromPrevious != undefined ? (
                                    <>
                                        {!getSignificanceFromBreakdownStep(breakdown, step.order)?.fromBasisStep &&
                                        getSignificanceFromBreakdownStep(breakdown, step.order)?.fromPrevious ? (
                                            <Tooltip title="Significantly different from other breakdown values">
                                                <span className="table-text-highlight">
                                                    <FlagOutlined style={{ marginRight: 2 }} />{' '}
                                                    {formatDisplayPercentage(
                                                        1 - breakdown.steps?.[step.order]?.conversionRates.fromPrevious
                                                    )}
                                                    %
                                                </span>
                                            </Tooltip>
                                        ) : (
                                            <span>
                                                {formatDisplayPercentage(
                                                    1 - breakdown.steps?.[step.order]?.conversionRates.fromPrevious
                                                )}
                                                %
                                            </span>
                                        )}
                                    </>
                                ) : (
                                    EmptyValue
                                ),
                                renderSubColumnTitle('Rate'),
                                showLabels,
                                step,
                                isViewedOnDashboard
                            )
                        },
                        width: 80,
                        align: 'right',
                    })

                    _columns.push({
                        render: function RenderAverageTime({}, breakdown: FlattenedFunnelStepByBreakdown, rowIndex) {
                            return renderGraphAndHeader(
                                rowIndex,
                                (stepIndex - 1) * 5 + 9,
                                breakdown.steps?.[step.order]?.average_conversion_time != undefined ? (
                                    <span>
                                        {colonDelimitedDuration(
                                            breakdown.steps?.[step.order]?.average_conversion_time,
                                            3
                                        )}
                                    </span>
                                ) : (
                                    EmptyValue
                                ),
                                renderSubColumnTitle('Avg. time'),
                                showLabels,
                                step,
                                isViewedOnDashboard
                            )
                        },
                        width: 80,
                        align: 'right',
                        className: 'funnel-table-cell dividing-column',
                    })
                }
            })

            return _columns
        }

        // If steps are horizontal, render table with flattened steps

        const _columns: ColumnsType<FlattenedFunnelStep> = []
        _columns.push({
            title: '',
            render: function RenderSeriesGlyph({}, step: FlattenedFunnelStep): JSX.Element | null {
                if (step.breakdownIndex === undefined) {
                    // Not a breakdown value; show a step-order glyph
                    return <SeriesGlyph variant="funnel-step-glyph">{humanizeOrder(step.order)}</SeriesGlyph>
                }
                return null
            },
            fixed: 'left',
            width: 20,
            align: 'center',
        })

        if (!!steps[0]?.breakdown) {
            _columns.push({
                title: '',
                render: function RenderCheckbox({}, step: FlattenedFunnelStep): JSX.Element | null {
                    const color = getSeriesColor(step?.breakdownIndex, flattenedBreakdowns.length === 1)

                    // Breakdown parent
                    if (step.breakdownIndex === undefined && (step.nestedRowKeys ?? []).length > 0) {
                        return (
                            <PHCheckbox
                                checked={!!step.nestedRowKeys?.every((rowKey) => !hiddenLegendKeys[rowKey])}
                                indeterminate={step.nestedRowKeys?.some((rowKey) => !hiddenLegendKeys[rowKey])}
                                onChange={() => {
                                    // either toggle all data on or off
                                    const currentState = !!step.nestedRowKeys?.every(
                                        (rowKey) => !hiddenLegendKeys[rowKey]
                                    )
                                    setHiddenById(
                                        Object.fromEntries(
                                            step.nestedRowKeys?.map((rowKey) => [rowKey, currentState]) ?? []
                                        )
                                    )
                                }}
                            />
                        )
                    }
                    // Breakdown child

                    return (
                        <PHCheckbox
                            color={color}
                            checked={!hiddenLegendKeys[step.rowKey]}
                            onChange={() => {
                                setHiddenById({
                                    [getVisibilityIndex(step, step.breakdown_value)]: !hiddenLegendKeys[step.rowKey],
                                })
                            }}
                        />
                    )
                },
                fixed: 'left',
                width: 20,
                align: 'center',
            })
        }

        _columns.push({
            title: 'Step',
            render: function RenderLabel({}, step: FlattenedFunnelStep): JSX.Element {
                const color = getStepColor(step, !!step.breakdown)

                return (
                    <InsightLabel
                        seriesColor={color}
                        fallbackName={
                            !step.isBreakdownParent && isBreakdownChildType(step.breakdown)
                                ? formatBreakdownLabel(cohorts, step.breakdown)
                                : step.name
                        }
                        action={
                            !step.isBreakdownParent && isBreakdownChildType(step.breakdown)
                                ? undefined
                                : getActionFilterFromFunnelStep(step)
                        }
                        hasMultipleSeries={step.isBreakdownParent && steps.length > 1}
                        breakdownValue={
                            step.breakdown === ''
                                ? 'None'
                                : isBreakdownChildType(step.breakdown)
                                ? step.breakdown_value
                                : undefined
                        }
                        pillMaxWidth={165}
                        hideBreakdown={step.isBreakdownParent}
                        iconSize={IconSize.Small}
                        iconStyle={{ marginRight: 12 }}
                        hideIcon
                        allowWrap
                        showEventName={step.isBreakdownParent}
                    />
                )
            },
            fixed: 'left',
            width: 120,
        })

        _columns.push({
            title: 'Completed',
            render: function RenderCompleted({}, step: FlattenedFunnelStep): JSX.Element {
                return (
                    <ValueInspectorButton
                        onClick={() => openPersonsModalForStep({ step, converted: true })}
                        disabled={!isModalActive}
                    >
                        {step.count}
                    </ValueInspectorButton>
                )
            },
            width: 80,
            align: 'center',
        })

        _columns.push({
            title: 'Conversion',
            render: function RenderConversion({}, step: FlattenedFunnelStep): JSX.Element | null {
                return step.order === 0 ? (
                    EmptyValue
                ) : (
                    <span>{formatDisplayPercentage(step.conversionRates.total)}%</span>
                )
            },
            width: 80,
            align: 'center',
        })

        _columns.push({
            title: 'Dropped off',
            render: function RenderDropoff({}, step: FlattenedFunnelStep): JSX.Element | null {
                return step.order === 0 ? (
                    EmptyValue
                ) : (
                    <ValueInspectorButton
                        onClick={() => openPersonsModalForStep({ step, converted: false })}
                        disabled={!isModalActive}
                    >
                        {step.droppedOffFromPrevious}
                    </ValueInspectorButton>
                )
            },
            width: 80,
            align: 'center',
        })

        _columns.push({
            title: 'From previous step',
            render: function RenderDropoffFromPrevious({}, step: FlattenedFunnelStep): JSX.Element | null {
                return step.order === 0 ? (
                    EmptyValue
                ) : (
                    <span>{formatDisplayPercentage(1 - step.conversionRates.fromPrevious)}%</span>
                )
            },
            width: 80,
            align: 'center',
        })

        _columns.push({
            title: 'Average time',
            render: function RenderAverageTime({}, step: FlattenedFunnelStep): JSX.Element {
                return step.average_conversion_time ? (
                    <span>{humanFriendlyDuration(step.average_conversion_time, 2)}</span>
                ) : (
                    EmptyValue
                )
            },
            width: 100,
            align: 'center',
        })

        return _columns
    }

    // If the bars are vertical, use table as legend #5733
    const columns = getColumns()
    const tableData: TableProps<any /* TODO: Type this */> =
        barGraphLayout === FunnelLayout.vertical
            ? {
                  dataSource: flattenedStepsByBreakdown.slice(
                      0,
                      isViewedOnDashboard ? 2 : flattenedStepsByBreakdown.length
                  ),
                  columns,
                  showHeader: false,
                  rowClassName: (record, index) => {
                      return clsx(
                          `funnel-steps-table-row-${index}`,
                          index === 2 && 'funnel-table-cell',
                          record.significant && 'table-cell-highlight'
                      )
                  },
              }
            : {
                  dataSource: flattenedSteps,
                  columns,
              }

    return stepsWithCount.length > 1 ? (
        <Table
            {...tableData}
            scroll={isViewedOnDashboard ? undefined : { x: 'max-content' }}
            size="small"
            rowKey="rowKey"
            pagination={{ pageSize: 100, hideOnSinglePage: true }}
            style={{ height: '100%' }}
            data-attr={barGraphLayout === FunnelLayout.vertical ? 'funnel-bar-graph' : 'funnel-steps-table'}
            className="funnel-steps-table"
        />
    ) : null
}
Example #27
Source File: VolumeTable.tsx    From posthog-foss with MIT License 4 votes vote down vote up
export function VolumeTable({
    type,
    data,
}: {
    type: EventTableType
    data: Array<EventDefinition | PropertyDefinition>
}): JSX.Element {
    const [searchTerm, setSearchTerm] = useState(false as string | false)
    const [dataWithWarnings, setDataWithWarnings] = useState([] as VolumeTableRecord[])
    const { user } = useValues(userLogic)
    const { openDrawer } = useActions(definitionDrawerLogic)

    const hasTaxonomyFeatures = user?.organization?.available_features?.includes(AvailableFeature.INGESTION_TAXONOMY)

    const columns: ColumnsType<VolumeTableRecord> = [
        {
            title: `${capitalizeFirstLetter(type)} name`,
            render: function Render(_, record): JSX.Element {
                return (
                    <span>
                        <div style={{ display: 'flex', alignItems: 'baseline', paddingBottom: 4 }}>
                            <span className="ph-no-capture" style={{ paddingRight: 8 }}>
                                <PropertyKeyInfo
                                    style={hasTaxonomyFeatures ? { fontWeight: 'bold' } : {}}
                                    value={record.eventOrProp.name}
                                />
                            </span>
                            {hasTaxonomyFeatures ? (
                                <ObjectTags tags={record.eventOrProp.tags || []} staticOnly />
                            ) : null}
                        </div>
                        {hasTaxonomyFeatures &&
                            (isPosthogEvent(record.eventOrProp.name) ? null : (
                                <VolumeTableRecordDescription
                                    id={record.eventOrProp.id}
                                    description={record.eventOrProp.description}
                                    type={type}
                                />
                            ))}
                        {record.warnings?.map((warning) => (
                            <Tooltip
                                key={warning}
                                color="orange"
                                title={
                                    <>
                                        <b>Warning!</b> {warning}
                                    </>
                                }
                            >
                                <WarningOutlined style={{ color: 'var(--warning)', marginLeft: 6 }} />
                            </Tooltip>
                        ))}
                    </span>
                )
            },
            sorter: (a, b) => ('' + a.eventOrProp.name).localeCompare(b.eventOrProp.name || ''),
            filters: [
                { text: 'Has warnings', value: 'warnings' },
                { text: 'No warnings', value: 'noWarnings' },
            ],
            onFilter: (value, record) => (value === 'warnings' ? !!record.warnings.length : !record.warnings.length),
        },
        type === 'event' && hasTaxonomyFeatures
            ? {
                  title: 'Owner',
                  render: function Render(_, record): JSX.Element {
                      const owner = record.eventOrProp?.owner
                      return isPosthogEvent(record.eventOrProp.name) ? <>-</> : <Owner user={owner} />
                  },
              }
            : {},
        type === 'event'
            ? {
                  title: function VolumeTitle() {
                      return (
                          <Tooltip
                              placement="right"
                              title="Total number of events over the last 30 days. Can be delayed by up to an hour."
                          >
                              30 day volume (delayed by up to an hour)
                              <InfoCircleOutlined className="info-indicator" />
                          </Tooltip>
                      )
                  },
                  render: function RenderVolume(_, record) {
                      return <span className="ph-no-capture">{compactNumber(record.eventOrProp.volume_30_day)}</span>
                  },
                  sorter: (a, b) =>
                      a.eventOrProp.volume_30_day == b.eventOrProp.volume_30_day
                          ? (a.eventOrProp.volume_30_day || -1) - (b.eventOrProp.volume_30_day || -1)
                          : (a.eventOrProp.volume_30_day || -1) - (b.eventOrProp.volume_30_day || -1),
              }
            : {},
        {
            title: function QueriesTitle() {
                return (
                    <Tooltip
                        placement="right"
                        title={`Number of queries in PostHog that included a filter on this ${type}`}
                    >
                        30 day queries (delayed by up to an hour)
                        <InfoCircleOutlined className="info-indicator" />
                    </Tooltip>
                )
            },
            render: function Render(_, item) {
                return <span className="ph-no-capture">{compactNumber(item.eventOrProp.query_usage_30_day)}</span>
            },
            sorter: (a, b) =>
                a.eventOrProp.query_usage_30_day == b.eventOrProp.query_usage_30_day
                    ? (a.eventOrProp.query_usage_30_day || -1) - (b.eventOrProp.query_usage_30_day || -1)
                    : (a.eventOrProp.query_usage_30_day || -1) - (b.eventOrProp.query_usage_30_day || -1),
        },
        hasTaxonomyFeatures
            ? {
                  render: function Render(_, item) {
                      return (
                          <>
                              {isPosthogEvent(item.eventOrProp.name) ? null : (
                                  <Button
                                      type="link"
                                      icon={<ArrowRightOutlined style={{ color: '#5375FF' }} />}
                                      onClick={() => openDrawer(type, item.eventOrProp.id)}
                                  />
                              )}
                          </>
                      )
                  },
              }
            : {},
    ]

    useEffect(() => {
        setDataWithWarnings(
            data.map((eventOrProp: EventOrPropType): VolumeTableRecord => {
                const record = { eventOrProp } as VolumeTableRecord
                record.warnings = []
                if (eventOrProp.name?.endsWith(' ')) {
                    record.warnings.push(`This ${type} ends with a space.`)
                }
                if (eventOrProp.name?.startsWith(' ')) {
                    record.warnings.push(`This ${type} starts with a space.`)
                }
                return record
            }) || []
        )
    }, [data])

    return (
        <>
            <Input.Search
                allowClear
                enterButton
                style={{ marginTop: '1.5rem', maxWidth: 400, width: 'initial', flexGrow: 1 }}
                onChange={(e) => {
                    setSearchTerm(e.target.value)
                }}
                placeholder={`Filter ${type === 'property' ? 'properties' : 'events'}....`}
            />
            <br />
            <br />
            <Table
                dataSource={searchTerm ? search(dataWithWarnings, searchTerm) : dataWithWarnings}
                columns={columns}
                rowKey={(item) => item.eventOrProp.name}
                size="small"
                style={{ marginBottom: '4rem' }}
                pagination={{ pageSize: 100, hideOnSinglePage: true }}
                onRow={(record) =>
                    hasTaxonomyFeatures && !isPosthogEvent(record.eventOrProp.name)
                        ? { onClick: () => openDrawer(type, record.eventOrProp.id), style: { cursor: 'pointer' } }
                        : {}
                }
            />
        </>
    )
}
Example #28
Source File: index.tsx    From shippo with MIT License 4 votes vote down vote up
Users = () => {
  const [data, setData] = useState<IUserExtRoleName[]>()
  const [total, setTotal] = useState(0)
  const [current, setCurrent] = useState(1)

  const [isModalVisible, setIsModalVisible] = useState(false)

  const editUserDrawerRef = useRef<EditUserDrawerRef>(null)

  const [qq, setQQ] = useState('')

  const handleUserCreate = useCallback(async (qq: string) => {
    if (!checkQQ(qq)) {
      return message.info('QQ号格式错误')
    }

    try {
      const hr = await services.admin.user__create({ email: qq + '@qq.com' })
      if (hr.data.success) {
        message.success('成功')
      }
    } catch (error) {
      console.log(error)
      message.error('失败')
    }
  }, [])

  const columns: ColumnsType<IUserExtRoleName> = [
    {
      title: 'UID',
      dataIndex: 'id',
      key: 'id',
    },
    {
      title: '手机号',
      dataIndex: 'phone',
      key: 'phone',
    },
    {
      title: '邮箱',
      dataIndex: 'email',
      key: 'email',
    },
    {
      title: '昵称',
      dataIndex: 'nickname',
      key: 'nickname',
    },
    {
      title: '头像',
      dataIndex: 'avatar',
      key: 'avatar',
      render: (value) => {
        return <Avatar shape="square" size="small" icon={<UserOutlined />} />
      },
    },
    {
      title: '经验',
      dataIndex: 'exp',
      key: 'exp',
    },
    {
      title: '硬币',
      dataIndex: 'coin',
      key: 'coin',
    },
    {
      title: '角色名称',
      dataIndex: 'roleName',
      key: 'roleName',
    },
    {
      title: '注册时间',
      dataIndex: 'createdAt',
      key: 'createdAt',
    },
    {
      title: '操作',
      key: 'action',
      render: (_, record) => {
        return (
          <Space size="middle">
            <Button type="link" onClick={() => editUserDrawerRef.current?.open(record)}>
              编辑用户
            </Button>
          </Space>
        )
      },
    },
  ]

  const updateTable = useCallback(() => {
    services.user
      .find_all({
        pageSize: 20,
        current,
      })
      .then((hr) => {
        setData(
          hr.data.resource.items.map((item) => {
            return { ...item, createdAt: formatTimeStr(item.createdAt) }
          })
        )
        setTotal(hr.data.resource.total)
      })
  }, [current])

  useEffect(() => {
    updateTable()
  }, [updateTable])

  return (
    <div>
      <EditUserDrawer ref={editUserDrawerRef} onClose={() => updateTable()} />
      <Space size="middle">
        <Button
          type="primary"
          shape="round"
          icon={<PlusOutlined />}
          onClick={() => setIsModalVisible(true)}
        >
          新增邮箱用户
        </Button>
      </Space>
      <Table
        rowKey="id"
        columns={columns}
        dataSource={data}
        pagination={{
          position: ['bottomCenter'],
          pageSize: 20,
          total,
          current,
          showSizeChanger: false,
          size: 'default',
          onChange: (page: number, pageSize: number) => {
            setCurrent(page)
          },
        }}
        size="small"
      />

      <Modal
        title="新增邮箱用户"
        visible={isModalVisible}
        onOk={() => {
          setIsModalVisible(false)
          handleUserCreate(qq)
        }}
        onCancel={() => setIsModalVisible(false)}
      >
        <Alert message="只需要输入QQ号即可,不需要后戳。(@qq.com)" type="warning" />
        <Input placeholder="QQ号" value={qq} onChange={(event) => setQQ(event.target.value)} />
      </Modal>
    </div>
  )
}
Example #29
Source File: role.tsx    From shippo with MIT License 4 votes vote down vote up
Page_permission_role: React.FC = () => {
  const [data, setData] = useState<IRole[]>()
  const editRoleDrawerRef = useRef<EditRoleDrawerRef>(null)
  const editRolePolicyDrawerRef = useRef<EditRolePolicyDrawerRef>(null)

  const handleDle = useCallback((id: number) => {
    confirm({
      title: '确认删除?',
      icon: <ExclamationCircleOutlined />,
      content: '此操作不可逆',
      onOk() {
        console.log('OK')
        services.role.del({ id }).then((hr) => {
          if (hr.data.success) {
            message.success('成功')
          } else {
            message.success('失败')
          }
        })
      },
      onCancel() {
        console.log('Cancel')
      },
    })
  }, [])

  const [columns, setColumns] = useState<ColumnsType<IRole>>([
    {
      title: '角色ID',
      dataIndex: 'id',
      key: 'id',
    },
    {
      title: '角色名称',
      dataIndex: 'roleName',
      key: 'roleName',
    },
    {
      title: '描述',
      dataIndex: 'remark',
      key: 'remark',
    },
    {
      title: '创建时间',
      dataIndex: 'createdAt',
      key: 'createdAt',
    },
    {
      title: '操作',
      key: 'action',
      render: (_, record) => {
        return (
          <Space size="middle">
            <Button
              type="link"
              onClick={() => {
                editRoleDrawerRef.current?.open(record)
              }}
            >
              修改
            </Button>
            <Button
              type="link"
              onClick={() => {
                handleDle(record.id)
              }}
            >
              删除
            </Button>
            <Button
              type="link"
              onClick={() => {
                editRolePolicyDrawerRef.current?.open(record)
              }}
            >
              权限策略配置
            </Button>
          </Space>
        )
      },
    },
  ])

  const updateTable = useCallback(async () => {
    const hr = await services.role.find_all()
    setData(
      hr.data.resource.map((item) => {
        return { ...item, createdAt: formatTimeStr(item.createdAt) }
      })
    )
  }, [])

  useMount(() => {
    updateTable()
  })

  return (
    <div>
      <EditRoleDrawer ref={editRoleDrawerRef} onClose={() => updateTable()} />
      <EditRolePolicyDrawer ref={editRolePolicyDrawerRef} />
      <Space size="middle">
        <Button type="primary" onClick={() => editRoleDrawerRef.current?.open()}>
          新增角色
        </Button>
      </Space>
      <Table
        rowKey="id"
        columns={columns}
        dataSource={data}
        pagination={{ position: ['bottomCenter'] }}
        size="small"
      />
    </div>
  )
}