@ant-design/icons#CopyOutlined TypeScript Examples
The following examples show how to use
@ant-design/icons#CopyOutlined.
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: CopyToClipboard.tsx From posthog-foss with MIT License | 6 votes |
export function CopyToClipboardInput({
value,
placeholder,
description,
isValueSensitive = false,
...props
}: InputProps): JSX.Element {
return (
<Input
className={isValueSensitive ? 'ph-no-capture' : ''}
type="text"
value={value}
placeholder={placeholder || 'nothing to show here'}
disabled={!value}
suffix={
value ? (
<Tooltip title="Copy to Clipboard">
<CopyOutlined
onClick={() => {
copyToClipboard(value, description)
}}
/>
</Tooltip>
) : null
}
{...props}
/>
)
}
Example #2
Source File: Export.tsx From fe-v5 with Apache License 2.0 | 6 votes |
function Export(props: ModalWrapProps & IProps) {
const { visible, destroy, data } = props;
const { t } = useTranslation();
let str = data;
try {
str = JSON.stringify(JSON.parse(data), null, 4);
} catch (e) {
console.log(e);
}
return (
<Modal
title='导出配置'
visible={visible}
onCancel={() => {
destroy();
}}
footer={null}
>
<>
<div style={{ marginBottom: 10 }}>
<a
onClick={() => {
download([data], 'download.json');
}}
>
Download.json
</a>
<a style={{ float: 'right' }} onClick={() => copyToClipBoard(data, t)}>
<CopyOutlined />
复制JSON内容到剪贴板
</a>
</div>
<Input.TextArea value={str} rows={10} />
</>
</Modal>
);
}
Example #3
Source File: Export.tsx From fe-v5 with Apache License 2.0 | 6 votes |
function Export(props: IProps & ModalWrapProps) {
const { visible, destroy } = props;
const [data, setData] = useState(props.data);
return (
<Modal
title='导出大盘'
visible={visible}
onCancel={() => {
destroy();
}}
footer={null}
>
<p>
<a
onClick={() => {
download([data], 'download.json');
}}
>
Download.json
</a>
<a style={{ float: 'right' }} onClick={() => copyToClipBoard(data, (val) => val)}>
<CopyOutlined />
复制JSON内容到剪贴板
</a>
</p>
<Input.TextArea
value={data}
onChange={(e) => {
setData(e.target.value);
}}
rows={15}
/>
</Modal>
);
}
Example #4
Source File: index.tsx From metaplex with Apache License 2.0 | 6 votes |
SetupVariables: FC<Variables> = ({
storeAddress,
storeOwnerAddress,
}) => {
const ref = useRef<HTMLDivElement>(null);
const copySettings = useCallback(() => {
const text = ref.current?.innerText;
if (text) {
navigator.clipboard.writeText(text);
}
}, []);
if (!storeAddress && !storeOwnerAddress) {
return null;
}
return (
<Card
title="Store configuration"
extra={
<Button
type="dashed"
onClick={copySettings}
icon={<CopyOutlined />}
></Button>
}
>
<div ref={ref}>
{storeOwnerAddress && (
<p>REACT_APP_STORE_OWNER_ADDRESS_ADDRESS={storeOwnerAddress}</p>
)}
</div>
</Card>
);
}
Example #5
Source File: index.tsx From electron-playground with MIT License | 6 votes |
CodeBlock: React.FunctionComponent<ICodeBlockProps> = props => {
const { value, language, src } = props
const onCopy = () => message.info('已复制到剪贴板')
const [code, setCode] = React.useState<string>(value || '')
const [codePath, setCodePath] = React.useState<string>('')
React.useEffect(() => {
if (!src) {
return
}
const codePath = getSrcRelativePath(src)
setCode('loading ......')
setCodePath(codePath)
fs.readFile(codePath, (err, content) => {
setCode(content.toString())
})
}, [src])
const openFile = (path: string) => {
shell.openPath(path)
}
return (
<div className={style.container}>
{codePath && <p>
代码路径: <a onClick={openFile.bind(null, codePath)}>{codePath}</a>
</p>}
<SyntaxHighlighter language={language || 'javascript'} style={githubGist} >{code}</SyntaxHighlighter>
<CopyToClipboard text={code} onCopy={onCopy}>
<CopyOutlined className={style.copy} />
</CopyToClipboard>
</div>
)
}
Example #6
Source File: config-toolbar.ts From XFlow with MIT License | 6 votes |
registerIcon = () => {
IconStore.set('SaveOutlined', SaveOutlined)
IconStore.set('UndoOutlined', UndoOutlined)
IconStore.set('RedoOutlined', RedoOutlined)
IconStore.set('VerticalAlignTopOutlined', VerticalAlignTopOutlined)
IconStore.set('VerticalAlignBottomOutlined', VerticalAlignBottomOutlined)
IconStore.set('GatewayOutlined', GatewayOutlined)
IconStore.set('GroupOutlined', GroupOutlined)
IconStore.set('UngroupOutlined', UngroupOutlined)
IconStore.set('CopyOutlined', CopyOutlined)
IconStore.set('SnippetsOutlined', SnippetsOutlined)
}
Example #7
Source File: GeneralInput.tsx From next-basics with GNU General Public License v3.0 | 6 votes |
InputGroup = forwardRef<Input, InputGroupProps>(function InputGroup(
props,
ref
): React.ReactElement {
const { t } = useTranslation(NS_FORMS);
const { formElement, value, inputBoxStyle, copyButton, ...inputProps } =
props;
const handleCopy = (text: string, success: boolean) => {
if (success) {
message.success(t(K.COPY_SUCCESS));
}
};
const getCopyButton = (): React.ReactElement => {
return (
<Clipboard text={value} onCopy={handleCopy}>
<Button icon={<CopyOutlined />} />
</Clipboard>
);
};
const input = (
<Input value={value} style={inputBoxStyle} ref={ref} {...inputProps} />
);
return copyButton ? (
<Input.Group compact style={{ display: "flex" }}>
{input}
{getCopyButton()}
</Input.Group>
) : (
input
);
})
Example #8
Source File: CopyPaste.tsx From datart with Apache License 2.0 | 6 votes |
CopyBtn: FC<{
fn: (ids?: string[]) => void;
title: string;
}> = ({ fn, title }) => {
const selectedIds = useSelector(selectSelectedIds);
const onCopy = () => {
fn(selectedIds);
};
return (
<Tooltip title={title}>
<ToolbarButton
disabled={!selectedIds.length}
onClick={onCopy}
icon={<CopyOutlined />}
/>
</Tooltip>
);
}
Example #9
Source File: node.tsx From imove with MIT License | 5 votes |
nodeMenuConfig = [
{
key: 'copy',
title: '复制',
icon: <CopyOutlined />,
handler: shortcuts.copy.handler,
},
{
key: 'delete',
title: '删除',
icon: <DeleteOutlined />,
handler: shortcuts.delete.handler,
},
{
key: 'rename',
title: '编辑文本',
icon: <EditOutlined />,
showDividerBehind: true,
handler() {
// TODO
},
},
{
key: 'bringToTop',
title: '置于顶层',
icon: <XIcon type={'icon-bring-to-top'} />,
handler: shortcuts.bringToTop.handler,
},
{
key: 'bringToBack',
title: '置于底层',
icon: <XIcon type={'icon-bring-to-bottom'} />,
showDividerBehind: true,
handler: shortcuts.bringToBack.handler,
},
{
key: 'editCode',
title: '编辑代码',
icon: <FormOutlined />,
disabled(flowChart: Graph) {
return getSelectedNodes(flowChart).length !== 1;
},
handler(flowChart: Graph) {
flowChart.trigger('graph:editCode');
},
},
{
key: 'executeCode',
title: '执行代码',
icon: <CodeOutlined />,
disabled(flowChart: Graph) {
return getSelectedNodes(flowChart).length !== 1;
},
handler(flowChart: Graph) {
flowChart.trigger('graph:runCode');
},
},
]
Example #10
Source File: index.tsx From fe-v5 with Apache License 2.0 | 5 votes |
HostCopyTitle = (props: Props) => {
const { t, i18n } = useTranslation();
const handleCopyBtnClick: HandleCopyBtnClick = async (dataIndex, copyType) => {
const { data } = props;
let tobeCopy = [];
if (copyType === 'all') {
tobeCopy = _.map(data, (item) => item[dataIndex]);
}
if (_.isEmpty(tobeCopy)) {
message.warning(t('host.copy.empty'));
return;
}
const tobeCopyStr = _.join(tobeCopy, '\n');
const copySucceeded = clipboard(tobeCopyStr);
if (copySucceeded) {
if (i18n.language === 'zh') {
message.success(`复制成功${tobeCopy.length}条记录`);
} else if (i18n.language === 'en') {
message.success(`Successful copy ${tobeCopy.length} items`);
}
} else {
Modal.warning({
title: t('host.copy.error'),
content: <Input.TextArea defaultValue={tobeCopyStr} />,
});
}
}
const { dataIndex } = props;
return (
<span>
Host
<CopyOutlined
className="pointer"
style={{ paddingLeft: 5 }}
onClick={() => handleCopyBtnClick(dataIndex, 'all')}
/>
</span>
);
}
Example #11
Source File: index.tsx From metaplex with Apache License 2.0 | 5 votes |
Settings = ({
additionalSettings,
}: {
additionalSettings?: JSX.Element;
}) => {
const { publicKey } = useWallet();
return (
<>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: '15px 0',
}}
>
<Identicon
address={publicKey?.toBase58()}
style={{
width: 48,
}}
/>
{publicKey && (
<>
<Tooltip title="Address copied">
<div
style={{
fontWeight: 600,
letterSpacing: '-0.02em',
color: '#FFFFFF',
}}
onClick={() =>
navigator.clipboard.writeText(publicKey?.toBase58() || '')
}
>
<CopyOutlined />
{shortenAddress(publicKey?.toBase58())}
</div>
</Tooltip>
</>
)}
<br />
<span
style={{
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
width: 'calc(100% + 32px)',
marginBottom: 10,
}}
></span>
{additionalSettings}
</div>
</>
);
}
Example #12
Source File: index.tsx From nanolooker with MIT License | 5 votes |
Copy = ({ text }: { text: string }) => {
const { t } = useTranslation();
const { theme } = React.useContext(PreferencesContext);
const [isCopied, setIsCopied] = React.useState<boolean>(false);
return (
<Tooltip title={isCopied ? `${t("common.copied")}!` : t("common.copy")}>
<CopyToClipboard
text={text}
onCopy={() => {
setIsCopied(true);
clearTimeout(copiedTimeout);
copiedTimeout = window.setTimeout(() => {
setIsCopied(false);
}, 2000);
}}
>
{isCopied ? (
theme === Theme.DARK ? (
<CheckCircleFilled
style={{ fontSize: "24px", color: Colors.PENDING_DARK as string }}
/>
) : (
<CheckCircleFilled
style={{ fontSize: "24px", color: Colors.PENDING as string }}
/>
)
) : (
<Button
shape="circle"
size="small"
disabled={isCopied}
style={{
borderColor: isCopied ? (Colors.RECEIVE as string) : undefined,
}}
>
{theme === Theme.DARK ? <CopyFilled /> : <CopyOutlined />}
</Button>
)}
</CopyToClipboard>
</Tooltip>
);
}
Example #13
Source File: components.tsx From jitsu with MIT License | 5 votes |
export function CodeSnippet(props: ICodeSnippetProps) {
const toolBarPos = props.toolbarPosition ? props.toolbarPosition : "bottom"
const copy = () => {
copyToClipboard(props.children, true)
message.info("Code copied to clipboard")
}
const toolbar = (
<Row className={cn("code-snippet-toolbar", "code-snippet-toolbar-" + toolBarPos)}>
<Col span={16}>{props.extra}</Col>
<Col span={8}>
<Align horizontal="right">
{toolBarPos === "bottom" ? (
<ActionLink onClick={copy}>Copy To Clipboard</ActionLink>
) : (
<a onClick={copy}>
<CopyOutlined />
</a>
)}
</Align>
</Col>
</Row>
)
const classes = [
"code-snippet-wrapper-" + toolBarPos,
"code-snippet-wrapper",
props.size === "large" ? "code-snippet-large" : "code-snippet-small",
]
if (props.className) {
classes.push(props.className)
}
return (
<div className={classes.join(" ")}>
{toolBarPos === "top" ? toolbar : null}
<SyntaxHighlighterAsync language={props.language}>{props.children}</SyntaxHighlighterAsync>
{toolBarPos === "bottom" ? toolbar : null}
</div>
)
}
Example #14
Source File: ErrorCard.tsx From jitsu with MIT License | 5 votes |
ErrorCard: FC<ErrorCardProps> = ({
title,
icon,
error,
description,
descriptionWithContacts,
stackTrace,
className,
onReload,
}) => {
if (description === undefined && error !== undefined) {
description = error.message
}
if (stackTrace === undefined && error !== undefined) {
stackTrace = error.stack
}
return (
<Card bordered={false} className={cn(className, "max-h-full")}>
<Card.Meta
avatar={icon || <ExclamationCircleOutlined className={styles.icon} />}
title={title || "An Error Occured"}
description={
<>
<Fragment key="description">
{description !== undefined ? (
description
) : (
<span>
{descriptionWithContacts !== undefined ? (
<>
{descriptionWithContacts}
{descriptionWithContacts && <br />}
</>
) : (
<>
{"The application component crashed because of an internal error."}
<br />
</>
)}
{"Please, try to reload the page first and if the problem is still present contact us at"}{" "}
<Typography.Paragraph copyable={{ tooltips: false }} className="inline">
{"[email protected]"}
</Typography.Paragraph>{" "}
{"and our engineers will fix the problem asap."}
</span>
)}
</Fragment>
{stackTrace && (
<Collapse key="stack-trace" bordered={false} className={`mt-2 ${styles.stackTraceCard}`}>
<Collapse.Panel key={1} header="Error Stack Trace">
<div className="overflow-y-auto">
<Typography.Paragraph
copyable={{
text: stackTrace,
icon: [<CopyOutlined />, <CheckOutlined />],
}}
className={`flex flex-row ${styles.errorStackContainer}`}
>
<pre className="text-xs">{stackTrace}</pre>
</Typography.Paragraph>
</div>
</Collapse.Panel>
</Collapse>
)}
{onReload && (
<div key="reload-button" className="flex justify-center items-center mt-2">
<Button type="default" onClick={onReload} icon={<ReloadOutlined />}>{`Reload`}</Button>
</div>
)}
</>
}
/>
</Card>
)
}
Example #15
Source File: ExampleFluxQuery.tsx From iot-center-v2 with MIT License | 5 votes |
ExampleFluxQuery: React.FC<TExampleFluxQueryProps> = (props) => {
const {clientId, fields, start} = props
const [bucket, setBucket] = useState('<your-bucket>')
useEffect(() => {
const asyncEffect = async () => {
try {
const config = await fetchDeviceConfig(clientId)
await new Promise((r) => setTimeout(r, 10000))
setBucket(config.influx_bucket)
} catch (e) {
// TODO: escalation
console.error(e)
}
}
asyncEffect()
}, [clientId])
const fieldsFilterString = fields
.map((field) => `r["_field"] == "${field}"`)
.join(' or ')
const fieldsFilter =
fields.length > 0
? flux`\n |> filter(fn: (r) => ${fluxExpression(fieldsFilterString)})`
: ''
const fluxQuery = flux`\
from(bucket: ${bucket})
|> range(start: ${fluxDuration(start)})
|> filter(fn: (r) => r._measurement == "environment")
|> filter(fn: (r) => r.clientId == ${clientId})\
${fluxExpression(fieldsFilter)}\
`.toString()
const [copiedMsg, setCopiedMsg] = useState<string>()
useEffect(() => {
if (copiedMsg) {
setTimeout(() => {
setCopiedMsg(undefined)
}, 2000)
}
}, [copiedMsg])
return (
<>
<Col style={{position: 'relative'}}>
<Tooltip title={copiedMsg} visible={!!copiedMsg}>
<Button
icon={<CopyOutlined />}
style={{position: 'absolute', right: 0, top: 0}}
size="small"
type="dashed"
onClick={() => {
navigator.clipboard.writeText(fluxQuery)
setCopiedMsg('Query copied into your clipboard')
}}
/>
</Tooltip>
<code style={{whiteSpace: 'pre-wrap'}}>{fluxQuery}</code>
</Col>
</>
)
}
Example #16
Source File: CodeSnippet.tsx From posthog-foss with MIT License | 5 votes |
export function CodeSnippet({
children,
language = Language.Text,
wrap = false,
style = {},
actions,
copyDescription = 'code snippet',
hideCopyButton = false,
}: CodeSnippetProps): JSX.Element {
return (
<div className="code-container" style={style}>
<div className="action-icon-container">
{actions &&
actions.map(({ Icon, callback, popconfirmProps, title }, index) =>
!popconfirmProps ? (
<Icon
key={`snippet-action-${index}`}
className="action-icon"
onClick={callback}
title={title}
/>
) : (
<Popconfirm key={`snippet-action-${index}`} {...popconfirmProps} onConfirm={callback}>
<Icon className="action-icon" title={title} />
</Popconfirm>
)
)}
{!hideCopyButton && (
<CopyOutlined
className="action-icon"
onClick={() => {
children && copyToClipboard(children, copyDescription)
}}
title="Copy"
/>
)}
</div>
<SyntaxHighlighter
style={okaidia}
language={language}
customStyle={{ borderRadius: 2 }}
wrapLines={wrap}
lineProps={{ style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }}
>
{children}
</SyntaxHighlighter>
</div>
)
}
Example #17
Source File: index.tsx From electron-playground with MIT License | 5 votes |
iconMap.set('copy', { icon: <CopyOutlined />, text: '复制' })
Example #18
Source File: index.tsx From fe-v5 with Apache License 2.0 | 4 votes |
export default function ImportAndDownloadModal(props: Props) {
const { t } = useTranslation();
const exportTextRef = useRef(null as any);
const { status, title, exportData, description, onClose, onSubmit, crossCluster = true, onSuccess, label, fetchBuiltinFunc, submitBuiltinFunc, bgid } = props;
const [form] = Form.useForm();
const { clusters: clusterList } = useSelector<RootState, CommonStoreState>((state) => state.common);
const [allList, setAllList] = useState<{ name: string }[]>([]);
const [buildinList, setBuildinList] = useState<{ name: string }[]>([]);
const [importResult, setImportResult] = useState<{ name: string; isTrue: boolean; msg: string }[]>();
const columns = [
{
title: label,
dataIndex: 'name',
},
{
title: '导入结果',
dataIndex: 'isTrue',
render: (data) => {
return data ? <CheckCircleOutlined style={{ color: '#389e0d', fontSize: '18px' }} /> : <CloseCircleOutlined style={{ color: '#d4380d', fontSize: '18px' }} />;
},
},
{
title: '错误消息',
dataIndex: 'msg',
},
];
const builtinColumn = [
{
title: `${label}名称`,
dataIndex: 'name',
},
{
title: '操作',
dataIndex: 'id',
render(id, record) {
return (
<Button
type='link'
onClick={() => {
submitBuiltinFunc &&
submitBuiltinFunc(record.name, form.getFieldValue('cluster'), bgid!).then(({ dat }) => {
setImportResult(
Object.keys(dat).map((key) => {
return {
name: key,
key: key,
isTrue: !dat[key],
msg: dat[key],
};
}),
);
});
}}
>
导入
</Button>
);
},
},
];
const handleClose = () => {
onClose();
importResult && importResult.some((item) => item.isTrue) && onSuccess && onSuccess();
};
useEffect(() => {
if (status === ModalStatus.BuiltIn || status == ModalStatus.Import) {
fetchBuiltinFunc &&
fetchBuiltinFunc().then((res) => {
let arr = res.dat.map((name) => ({ name }));
setBuildinList(arr);
setAllList(arr);
});
}
setImportResult(undefined);
}, [status]);
const handleExportTxt = () => {
download([exportData], 'download.json');
};
const computeTitle = isValidElement(title) ? title : status === ModalStatus.Export ? t('导出') + title : t('导入') + title;
return (
<Modal
title={computeTitle}
destroyOnClose={true}
wrapClassName={isValidElement(title) ? 'import-modal-wrapper' : undefined}
footer={
status === ModalStatus.Import && (
<>
<Button key='delete' onClick={handleClose}>
{t('取消')}
</Button>
{importResult ? (
<Button type='primary' onClick={handleClose}>
{t('关闭')}
</Button>
) : (
<Button
key='submit'
type='primary'
onClick={async () => {
await form.validateFields();
const data = form.getFieldsValue();
try {
const importData = JSON.parse(data.import);
if (!Array.isArray(importData)) {
message.error(title + 'JSON需要时数组');
return;
}
const requstBody = importData.map((item) => {
return {
...item,
cluster: crossCluster ? data.cluster : undefined,
};
});
const dat = await onSubmit(requstBody);
const dataSource = Object.keys(dat).map((key) => {
return {
name: key,
key: key,
isTrue: !dat[key],
msg: dat[key],
};
});
setImportResult(dataSource);
// 每个业务各自处理onSubmit
} catch (error) {
message.error(t('数据有误:') + error);
}
}}
>
{t('确定')}
</Button>
)}
</>
)
}
onCancel={handleClose}
afterClose={() => setImportResult(undefined)}
visible={status !== 'hide'}
width={600}
>
<div
style={{
color: '#999',
}}
>
{description && <p>{description}</p>}
{status === ModalStatus.Export && (
<p>
<a onClick={handleExportTxt}>Download.json</a>
<a style={{ float: 'right' }} onClick={() => copyToClipBoard(exportData, t)}>
<CopyOutlined />
复制JSON内容到剪贴板
</a>
</p>
)}
</div>
{(() => {
switch (status) {
case ModalStatus.Export:
return (
<div contentEditable='true' suppressContentEditableWarning={true} ref={exportTextRef} className='export-dialog code-area'>
<pre>{exportData}</pre>
</div>
);
case ModalStatus.BuiltIn:
return (
<>
<Form form={form} preserve={false} layout='vertical'>
{crossCluster && (
<Form.Item
label={t('生效集群:')}
name='cluster'
initialValue={clusterList[0] || 'Default'}
rules={[
{
required: true,
message: t('生效集群不能为空'),
},
]}
>
<Select suffixIcon={<CaretDownOutlined />}>
{clusterList?.map((item) => (
<Option value={item} key={item}>
{item}
</Option>
))}
</Select>
</Form.Item>
)}
</Form>
<Input
placeholder={`请输入要查询的${label}名称`}
prefix={<SearchOutlined />}
style={{ marginBottom: '8px' }}
allowClear
onChange={(e) => {
let str = e.target.value;
let filterArr: { name: string }[] = [];
allList.forEach((el) => {
if (el.name.toLowerCase().indexOf(str.toLowerCase()) != -1) filterArr.push(el);
});
setBuildinList(filterArr);
}}
/>
<Table className='samll_table' dataSource={buildinList} columns={builtinColumn} pagination={buildinList.length < 5 ? false : { pageSize: 5 }} size='small' />
{importResult && (
<>
<Divider />
<Table className='samll_table' dataSource={importResult} columns={columns} size='small' pagination={importResult.length < 5 ? false : { pageSize: 5 }} />
</>
)}
</>
);
case ModalStatus.Import:
return (
<>
<Form form={form} preserve={false} layout='vertical'>
{crossCluster ? (
<Form.Item
label={t('生效集群:')}
name='cluster'
initialValue={clusterList[0] || 'Default'}
rules={[
{
required: true,
message: t('生效集群不能为空'),
},
]}
>
<Select suffixIcon={<CaretDownOutlined />}>
{clusterList?.map((item) => (
<Option value={item} key={item}>
{item}
</Option>
))}
</Select>
</Form.Item>
) : null}
<Form.Item
label={(!isValidElement(title) ? title : label) + t('JSON:')}
name='import'
rules={[
{
required: true,
message: t('请输入') + title,
validateTrigger: 'trigger',
},
]}
>
<TextArea className='code-area' placeholder={t('请输入') + (!isValidElement(title) ? title : label)} rows={4}></TextArea>
</Form.Item>
</Form>
{importResult && (
<>
<Divider />
<Table className='samll_table' dataSource={importResult} columns={columns} pagination={false} size='small' />
</>
)}
</>
);
}
})()}
</Modal>
);
}
Example #19
Source File: PreviewHeader.tsx From datart with Apache License 2.0 | 4 votes |
PreviewHeader: FC<{
shareLink?: { password: string; token: string; usePassword: boolean };
chartName?: string;
chartDescription?: string;
onRun?;
onGotoEdit?;
onGenerateShareLink?;
}> = memo(
({
shareLink,
chartName,
chartDescription,
onRun,
onGotoEdit,
onGenerateShareLink,
}) => {
const t = useI18NPrefix(`viz.action`);
const [expireDate, setExpireDate] = useState<string>();
const [enablePassword, setEnablePassword] = useState(false);
const [showShareLinkModal, setShowShareLinkModal] = useState(false);
const moreActionMenu = () => {
const menus: any = [];
if (onGenerateShareLink) {
menus.push(
<Menu.Item
key="1"
icon={<UserOutlined />}
onClick={() => setShowShareLinkModal(true)}
>
{t('share.shareLink')}
</Menu.Item>,
);
}
return <Menu>{menus}</Menu>;
};
const handleCopyToClipboard = value => {
const ta = document.createElement('textarea');
ta.innerText = value;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
};
const getFullShareLinkPath = shareLink => {
const encodeToken = encodeURIComponent(shareLink?.token);
return `http://172.16.1.150:8080/share?token=${encodeToken}${
shareLink?.usePassword ? '&usePassword=1' : ''
}`;
};
return (
<>
<StyledPreviewHeader>
<h1>{chartName}</h1>
<Space>
<Button key="run" icon={<CaretRightFilled />} onClick={onRun}>
{t('run')}
</Button>
<Dropdown.Button
key="edit"
onClick={onGotoEdit}
overlay={moreActionMenu()}
>
{t('edit')}
</Dropdown.Button>
</Space>
<StyledShareLinkModal
title={t('share.shareLink')}
visible={showShareLinkModal}
onOk={() => {
setShowShareLinkModal(false);
}}
onCancel={() => setShowShareLinkModal(false)}
destroyOnClose
>
<Form
preserve={false}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
autoComplete="off"
>
<FormItemEx label={t('share.expireDate')}>
<DatePicker
showTime
onChange={(date, dateString) => {
setExpireDate(dateString);
}}
/>
</FormItemEx>
<FormItemEx label={t('share.enablePassword')}>
<Checkbox
checked={enablePassword}
onChange={e => {
setEnablePassword(e.target.checked);
}}
/>
</FormItemEx>
<FormItemEx label={t('share.generateLink')}>
<Button
htmlType="button"
onClick={() =>
onGenerateShareLink(expireDate, enablePassword)
}
>
{t('share.generateLink')}
</Button>
</FormItemEx>
<FormItemEx label={t('share.link')} rules={[{ required: true }]}>
<Input
value={getFullShareLinkPath(shareLink)}
addonAfter={
<CopyOutlined
onClick={() =>
handleCopyToClipboard(getFullShareLinkPath(shareLink))
}
/>
}
/>
</FormItemEx>
{shareLink?.usePassword && (
<FormItemEx label={t('share.password')}>
<Input
value={shareLink?.password}
addonAfter={
<CopyOutlined
onClick={() =>
handleCopyToClipboard(shareLink?.password)
}
/>
}
/>
</FormItemEx>
)}
</Form>
</StyledShareLinkModal>
</StyledPreviewHeader>
{chartDescription && <Description>{chartDescription}</Description>}
</>
);
},
)
Example #20
Source File: SubscanLink.tsx From subscan-multisig-react with Apache License 2.0 | 4 votes |
// eslint-disable-next-line complexity
export function SubscanLink({ address, extrinsic, children, copyable, block, ...other }: SubscanLinkProps) {
const { network, rpc, networkConfig } = useApi();
const [mainColor, linkColor] = useMemo(() => {
return [getThemeColor(network), getLinkColor(network)];
}, [network]);
const { isCustomNetwork } = useMemo(() => {
return {
isCustomNetwork: isCustomRpc(rpc),
};
}, [rpc]);
const openLink = (url: string) => {
if (!url) {
return;
}
window.open(url, '_blank');
};
if (address) {
return (
<Text
copyable={
copyable && {
tooltips: false,
text: address,
icon: (
<CopyOutlined
className="rounded-full opacity-60 cursor-pointer p-1"
style={{
color: mainColor,
backgroundColor: mainColor + '40',
}}
onClick={(e) => e.preventDefault()}
/>
),
}
}
className="w-full"
style={{
wordBreak: 'break-all',
color: !isCustomNetwork || networkConfig?.explorerHostName ? linkColor : '#302B3C',
height: '20px',
cursor: !isCustomNetwork || networkConfig?.explorerHostName ? 'pointer' : 'default',
}}
>
<span
onClick={() =>
openLink(
!isCustomNetwork
? `https://${network}.subscan.io/account/${address}`
: networkConfig?.explorerHostName
? `https://${networkConfig?.explorerHostName}.subscan.io/account/${address}`
: ''
)
}
>
{address}
</span>
</Text>
);
}
if (extrinsic) {
const { height, index } = extrinsic;
return (
<Text
{...other}
onClick={() => {
openLink(
!isCustomNetwork
? `https://${network}.subscan.io/extrinsic/${height}-${index}`
: networkConfig?.explorerHostName
? `https://${networkConfig?.explorerHostName}.subscan.io/extrinsic/${height}-${index}`
: ''
);
}}
style={{
color: !isCustomNetwork || networkConfig?.explorerHostName ? linkColor : '#302B3C',
height: '20px',
cursor: !isCustomNetwork || networkConfig?.explorerHostName ? 'pointer' : 'default',
}}
>
{children}
</Text>
);
}
if (block) {
return (
<Text
{...other}
onClick={() => {
openLink(
!isCustomNetwork
? `https://${network}.subscan.io/block/${block}`
: networkConfig?.explorerHostName
? `https://${networkConfig?.explorerHostName}.subscan.io/block/${block}`
: ''
);
}}
style={{
color: !isCustomNetwork || networkConfig?.explorerHostName ? linkColor : '#302B3C',
height: '20px',
cursor: !isCustomNetwork || networkConfig?.explorerHostName ? 'pointer' : 'default',
}}
>
{block}
</Text>
);
}
return null;
}
Example #21
Source File: InputCallDataModal.tsx From subscan-multisig-react with Apache License 2.0 | 4 votes |
InputCallDataModal = (props: InputCallDataModalProps) => {
const { t } = useTranslation();
const { network } = useApi();
const mainColor = useMemo(() => {
return getThemeColor(network);
}, [network]);
const [selectedAddress, setSelectedAddress] = useState('');
const [callData, setCallData] = useState('');
useEffect(() => {
if (props.availableAccounts && props.availableAccounts?.length > 0) {
setSelectedAddress(props.availableAccounts[0].address);
}
}, [props.availableAccounts]);
const onSubmit = () => {
if (!selectedAddress) {
message.warn(t('missing selected account'));
return;
}
if (!callData.trim()) {
message.warn(t('missing call data'));
return;
}
props.onConfirm(selectedAddress, callData.trim());
};
return (
<Modal
title={null}
footer={null}
visible={props.visible}
destroyOnClose
onCancel={props.onCancel}
closable={false}
width={620}
bodyStyle={{
paddingLeft: '20px',
paddingRight: '20px',
paddingBottom: '30px',
}}
>
<div className="overflow-auto hide-scrollbar" style={{ maxHeight: '500px' }}>
<div className="flex items-center justify-center relative">
<div
className="font-bold capitalize text-black-800"
style={{ fontSize: '16px', textTransform: 'capitalize' }}
>
{t('Transaction information')}
</div>
<CloseOutlined
className="absolute cursor-pointer right-0"
style={{ color: '#666666' }}
onClick={props.onCancel}
/>
</div>
<div className="mt-6 font-bold text-black-800">{t('Approve Account')}*</div>
<div className="mt-2">
{props.availableAccounts && props.availableAccounts.length > 0 && (
<Select
style={{
width: '100%',
}}
placeholder={t('Select approve account')}
defaultValue={props.availableAccounts[0].address}
onChange={setSelectedAddress}
>
{props.availableAccounts.map((acc) => (
<Select.Option value={acc.address} key={acc.address}>
{acc.meta.name} - {acc.address}
</Select.Option>
))}
</Select>
)}
</div>
<div className="mt-6 font-bold text-black-800">{t('call_hash')}</div>
<div className="mt-2">
<Typography.Text
copyable={{
tooltips: false,
text: props.callHash,
icon: (
<CopyOutlined
className="rounded-full opacity-60 cursor-pointer p-1"
style={{
color: mainColor,
backgroundColor: mainColor + '40',
}}
onClick={(e) => e.preventDefault()}
/>
),
}}
>
{props.callHash}
</Typography.Text>
</div>
<div className="mt-6 font-bold text-black-800">{t('call_data')}*</div>
<div className="mt-2">
<Input.TextArea
value={callData}
onChange={(e) => {
setCallData(e.target.value);
}}
/>
</div>
<Row gutter={16} className={classNames('mt-10')}>
<Col span={24}>
<Button
block
style={{
color: mainColor,
}}
onClick={onSubmit}
>
{t('confirm')}
</Button>
</Col>
</Row>
</div>
</Modal>
);
}
Example #22
Source File: createModalForm.tsx From dashboard with Apache License 2.0 | 4 votes |
CreateModalForm: React.FC<CreateModalFormProps> = (props) => {
const [departmentSelectionVisible, setDepartmentSelectionVisible] = useState(false);
const [selectedDepartments, setSelectedDepartments] = useState<DepartmentOption[]>([]);
const [deletedTagIDs, setDeletedTagIDs] = useState<string[]>([]);
const formRef = useRef<FormInstance>();
return (
<>
<Modal
width={568}
className={'dialog from-item-label-100w'}
visible={props.visible}
onOk={() => {
formRef.current?.submit();
}}
onCancel={() => {
props.setVisible(false);
}}
>
<ProForm
submitter={{
render: false,
}}
// 创建标签组modal
initialValues={props.initialValues}
formRef={formRef}
layout={'horizontal'}
onFinish={async (values) => {
const params: GroupChatTagGroupItem = {
...props.initialValues,
...values,
};
if (props.type === 'edit' && deletedTagIDs.length > 0) {
params.delete_tag_ids = deletedTagIDs;
}
await props.onFinish(params);
}}
>
<h2 className="dialog-title"> {props.type==='create'?'新建客户群标签':'修改客户群标签'} </h2>
<ProFormText
name="name"
label="标签组名称"
width={'md'}
placeholder="请输入标签组名称"
rules={[
{
required: true,
message: '标签组名称必填',
},
]}
/>
<ProFormList
label={'标签名称'}
name="tags"
actionRender={(
field: FormListFieldData,
action: FormListOperation,
) => {
const currentKey = field.name;
const lastKey = formRef.current?.getFieldValue('tags').length - 1;
return [
<Tooltip key={'moveUp'} title="上移">
<UpCircleOutlined
className={'ant-pro-form-list-action-icon'}
onClick={() => {
console.log(field, currentKey);
if (currentKey - 1 >= 0) {
action.move(currentKey, currentKey - 1);
} else {
action.move(currentKey, lastKey);
}
}}
/>
</Tooltip>,
<Tooltip key={'moveDown'} title="下移">
<DownCircleOutlined
className={'ant-pro-form-list-action-icon'}
onClick={() => {
console.log(field, currentKey);
if (currentKey + 1 <= lastKey) {
action.move(currentKey, currentKey + 1);
} else {
action.move(currentKey, 0);
}
}}
/>
</Tooltip>,
<Tooltip key={'copy'} title="复制">
<CopyOutlined
className={'ant-pro-form-list-action-icon'}
onClick={() => {
action.add(formRef.current?.getFieldValue('tags')[currentKey]);
}}
/>
</Tooltip>,
<Tooltip key={'remove'} title="删除">
<DeleteOutlined
className={'ant-pro-form-list-action-icon'}
onClick={() => {
if (formRef.current?.getFieldValue('tags')[currentKey]?.id) {
setDeletedTagIDs([
...deletedTagIDs,
formRef.current?.getFieldValue('tags')[currentKey].id,
]);
}
action.remove(currentKey);
}}
/>
</Tooltip>,
];
}}
creatorButtonProps={{
type: 'default',
style: {width: '128px'},
position: 'bottom',
creatorButtonText: '添加标签',
}}
creatorRecord={{
name: '',
}}
rules={[
{
// @ts-ignore
required: true,
message: '标签名称必填',
},
]}
>
<ProFormText
name="name"
width={'sm'}
fieldProps={{
allowClear: false,
style: {
// width: '230px',
},
}}
placeholder="请输入标签名称"
rules={[
{
required: true,
message: '标签名称必填',
},
]}
/>
</ProFormList>
</ProForm>
</Modal>
<DepartmentSelectionModal
visible={departmentSelectionVisible}
setVisible={setDepartmentSelectionVisible}
defaultCheckedDepartments={selectedDepartments}
onFinish={(values) => {
setSelectedDepartments(values);
}}
allDepartments={props.allDepartments}
/>
</>
);
}
Example #23
Source File: createModalForm.tsx From dashboard with Apache License 2.0 | 4 votes |
CreateModalForm: React.FC<CreateModalFormProps> = (props) => {
const {allDepartments, initialValues} = props;
const departmentMap = _.keyBy<DepartmentOption>(allDepartments, "ext_id");
const [departmentSelectionVisible, setDepartmentSelectionVisible] = useState(false);
const [selectedDepartments, setSelectedDepartments] = useState<DepartmentOption[]>([]);
const [deletedTagExtIDs, setDeletedTagExtIDs] = useState<string[]>([]);
const formRef = useRef<FormInstance>();
const minOrder = props.minOrder ? props.minOrder : 10000;
const maxOrder = props.maxOrder ? props.maxOrder : 100000;
const itemDataToFormData = (values: CustomerTagGroupItem) => {
const params: any = {...values}
params.is_global = (values.department_list === undefined || values.department_list === [] || values.department_list?.includes(0)) ? True : False;
return params;
}
useEffect(() => {
if (initialValues?.department_list?.includes(0)) {
setSelectedDepartments([])
} else {
setSelectedDepartments(initialValues?.department_list?.map((ext_id) => departmentMap[ext_id]) || [])
}
formRef?.current?.setFieldsValue(itemDataToFormData(initialValues || {}));
}, [initialValues])
return (
<>
<Modal
{...props}
width={568}
className={'dialog from-item-label-100w'}
visible={props.visible}
onOk={() => {
formRef.current?.submit();
}}
onCancel={() => {
props.setVisible(false);
}}
>
<ProForm
submitter={{
render: false,
}}
initialValues={itemDataToFormData(initialValues || {})}
formRef={formRef}
layout={'horizontal'}
onFinish={async (values) => {
const params: CustomerTagGroupItem = {
...props.initialValues,
...values,
department_list: selectedDepartments.map((item) => item.ext_id),
};
if (values.is_global === True) {
params.department_list = [0];
}
if (props.type === 'create') {
if (values.order_type === 'max') {
params.order = maxOrder + 1;
}
if (values.order_type === 'min') {
params.order = minOrder - 1 >= 0 ? minOrder - 1 : 0;
}
}
if (props.type === 'edit' && deletedTagExtIDs.length > 0) {
params.remove_ext_tag_ids = deletedTagExtIDs;
}
await props.onFinish(params);
setDeletedTagExtIDs([]);
}}
>
<h3 className="dialog-title" style={{fontSize: 18}}>
{' '}
{props.type === 'edit' ? '修改标签组' : '新建标签组'}{' '}
</h3>
<ProFormText
name="name"
label="标签组名称"
width={'md'}
placeholder="请输入标签组名称"
rules={[
{
required: true,
message: '标签组名称必填',
},
]}
/>
<ProFormRadio.Group
name="is_global"
label="可见范围"
options={[
{
label: '全部员工',
value: True,
},
{
label: '部门可用',
value: False,
},
]}
/>
<ProFormDependency name={['is_global']}>
{({is_global}) => {
// 部门可用
if (is_global === Disable) {
return (
<>
<Row>
<ProForm.Item label={'选择可用部门'}>
<Button
icon={<PlusOutlined/>}
onClick={() => setDepartmentSelectionVisible(true)}
>
添加部门
</Button>
</ProForm.Item>
</Row>
<Row>
<Space direction={'horizontal'} wrap={true} style={{marginBottom: 6}}>
{selectedDepartments?.length > 0 && selectedDepartments.map((item, index) => {
if (!item?.id) {
return <div key={index}></div>
}
return (
<div key={item.id} className={'department-item'}>
<Badge
count={
<CloseCircleOutlined
onClick={() => {
setSelectedDepartments(
selectedDepartments.filter(
(department) => department.id !== item.id,
),
);
}}
style={{color: 'rgb(199,199,199)'}}
/>
}
>
<span className={'container'}>
<FolderFilled
style={{
color: '#47a7ff',
fontSize: 20,
marginRight: 6,
verticalAlign: -6,
}}
/>
{item.name}
</span>
</Badge>
</div>
)
})}
</Space>
</Row>
</>
);
}
// 全局可用
return <></>;
}}
</ProFormDependency>
{props.type === 'create' && (
<ProFormRadio.Group
name="order_type"
label="默认排序"
initialValue={'max'}
options={[
{
label: '排最前面',
value: 'max',
},
{
label: '排最后面',
value: 'min',
},
]}
/>
)}
<ProFormList
label={'标签名称'}
name="tags"
actionRender={(field: FormListFieldData, action: FormListOperation) => {
const currentKey = field.name;
const lastKey = formRef.current?.getFieldValue('tags').length - 1;
return [
<Tooltip key={'moveUp'} title="上移">
<UpCircleOutlined
className={'ant-pro-form-list-action-icon'}
onClick={() => {
if (currentKey - 1 >= 0) {
action.move(currentKey, currentKey - 1);
} else {
action.move(currentKey, lastKey);
}
}}
/>
</Tooltip>,
<Tooltip key={'moveDown'} title="下移">
<DownCircleOutlined
className={'ant-pro-form-list-action-icon'}
onClick={() => {
if (currentKey + 1 <= lastKey) {
action.move(currentKey, currentKey + 1);
} else {
action.move(currentKey, 0);
}
}}
/>
</Tooltip>,
<Tooltip key={'copy'} title="复制">
<CopyOutlined
className={'ant-pro-form-list-action-icon'}
onClick={() => {
action.add(formRef.current?.getFieldValue('tags')[currentKey]);
}}
/>
</Tooltip>,
<Tooltip key={'remove'} title="删除">
<DeleteOutlined
className={'ant-pro-form-list-action-icon'}
onClick={() => {
if (formRef.current?.getFieldValue('tags')[currentKey]?.ext_id) {
setDeletedTagExtIDs([
...deletedTagExtIDs,
formRef.current?.getFieldValue('tags')[currentKey].ext_id,
]);
}
action.remove(currentKey);
}}
/>
</Tooltip>,
];
}}
creatorButtonProps={{
type: 'default',
style: {width: '128px'},
position: 'bottom',
creatorButtonText: '添加标签',
}}
creatorRecord={{
name: '',
}}
rules={[
{
// @ts-ignore
required: true,
message: '标签名称必填',
},
]}
>
<ProFormText
name="name"
width={'sm'}
fieldProps={{
allowClear: false,
style: {
// width: '230px',
},
}}
placeholder="请输入标签名称"
rules={[
{
required: true,
message: '标签名称必填',
},
]}
/>
</ProFormList>
</ProForm>
</Modal>
<DepartmentSelectionModal
visible={departmentSelectionVisible}
setVisible={setDepartmentSelectionVisible}
defaultCheckedDepartments={selectedDepartments}
onFinish={(values) => {
setSelectedDepartments(values);
}}
allDepartments={props.allDepartments}
/>
</>
);
}
Example #24
Source File: index.tsx From Aragorn with MIT License | 4 votes |
FileManage = () => {
const {
state: {
uploaderProfiles,
configuration: { defaultUploaderProfileId }
}
} = useAppContext();
const [windowHeight, setWindowHeight] = useState(window.innerHeight);
useEffect(() => {
function handleResize() {
setWindowHeight(window.innerHeight);
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
const { id } = useParams<{ id: string }>();
const [hasFileManageFeature, setHasFileManageFeature] = useState(false);
const [uploaderProfile, setUploaderProfile] = useState({} as UploaderProfile);
useEffect(() => {
const currentId = id || defaultUploaderProfileId;
setCurrentProfile(currentId as string);
}, []);
useEffect(() => {
if (uploaderProfile?.id) {
getList();
}
}, [uploaderProfile]);
const [list, setList] = useState([] as ListFile[]);
const [listLoading, setListLoading] = useState(false);
const getList = (directoryPath?: string) => {
setListLoading(true);
ipcRenderer.send('file-list-get', uploaderProfile.id, directoryPath);
};
const [dirPath, setDirPath] = useState([] as string[]);
useEffect(() => {
function handleListGetReply(_, res?: FileListResponse) {
setListLoading(false);
if (res === undefined) {
setHasFileManageFeature(false);
setList([]);
message.info(`${uploaderProfile.uploaderName}暂不支持文件管理功能`);
return;
}
setHasFileManageFeature(true);
if (res.success) {
setList(res.data);
} else {
message.error(`文件列表获取失败 ${res.desc || ''}`);
}
}
function handleFileDeleteReply(_, res?: DeleteFileResponse) {
if (res === undefined) {
return;
}
if (res.success) {
message.success({ content: '文件删除成功', key: 'file-manage-delete' });
getList(dirPath.join('/'));
} else {
message.error({ content: `文件删除失败 ${res.desc || ''}`, key: 'file-manage-delete' });
}
}
function handleFileUploadReply() {
getList(dirPath.join('/'));
}
function handleDirectoryCreateReply(_, res?: CreateDirectoryResponse) {
if (res === undefined) {
return;
}
if (res.success) {
message.success('目录创建成功');
setModalVisible(false);
getList(dirPath.join('/'));
} else {
message.error(`目录创建失败 ${res.desc || ''}`);
}
}
function handleExportReplay(_, res) {
setExportLoading(false);
if (res) {
shell.showItemInFolder(res);
setRowKeys([]);
setSelectRows([]);
}
}
ipcRenderer.on('file-list-get-reply', handleListGetReply);
ipcRenderer.on('file-delete-reply', handleFileDeleteReply);
ipcRenderer.on('file-upload-reply', handleFileUploadReply);
ipcRenderer.on('directory-create-reply', handleDirectoryCreateReply);
ipcRenderer.on('export-reply', handleExportReplay);
return () => {
ipcRenderer.removeListener('file-list-get-reply', handleListGetReply);
ipcRenderer.removeListener('file-delete-reply', handleFileDeleteReply);
ipcRenderer.removeListener('file-upload-reply', handleFileUploadReply);
ipcRenderer.removeListener('directory-create-reply', handleDirectoryCreateReply);
ipcRenderer.removeListener('export-reply', handleExportReplay);
};
}, [uploaderProfile, dirPath]);
const handleNameClick = (record: ListFile) => {
if (record.type === 'directory') {
const newPath = [...dirPath, formatFileName(record.name)];
setDirPath(newPath);
getList(newPath.join('/'));
} else {
clipboard.writeText(record.url as string);
message.success('链接已复制到粘贴板');
}
};
const handlePathClick = (index: number) => {
if (index === -1) {
setDirPath([]);
getList();
} else {
const newPath = dirPath.slice(0, index + 1);
setDirPath(newPath);
getList(newPath.join('/'));
}
};
const setCurrentProfile = (uploaderProfileId: string) => {
setDirPath([]);
const uploaderProfile = uploaderProfiles.find(item => item.id === uploaderProfileId);
setUploaderProfile(uploaderProfile as UploaderProfile);
};
const formatFileName = (name: string) => {
if (dirPath.length > 0) {
const pathPrefix = dirPath.join('/') + '/';
return name.split(pathPrefix).pop() || '';
} else {
return name;
}
};
const [selectRowKeys, setRowKeys] = useState([] as string[]);
const [selectRows, setSelectRows] = useState([] as ListFile[]);
const handleTableRowChange = (selectedRowKeys, selectedRows: ListFile[]) => {
setRowKeys(selectedRowKeys);
setSelectRows(selectedRows);
};
const handleRefresh = () => {
getList(dirPath.join('/'));
};
const handleBatchDelete = () => {
Modal.confirm({
title: '确认删除',
onOk: () => {
const names = selectRows.map(item => [...dirPath, formatFileName(item.name)].join('/'));
message.info({ content: '正在删除,请稍后...', key: 'file-manage-delete' });
ipcRenderer.send('file-delete', uploaderProfile.id, names);
}
});
};
const handleDelete = (record: ListFile) => {
let name = record.name;
Modal.confirm({
title: '确认删除',
content: name,
onOk: () => {
let name = record.name;
if (record.type === 'directory') {
name = `${[...dirPath, record.name].join('/')}/`;
} else {
name = [...dirPath, formatFileName(record.name)].join('/');
}
message.info({ content: '正在删除,请稍后...', key: 'file-manage-delete' });
ipcRenderer.send('file-delete', uploaderProfile.id, [name]);
}
});
};
const uploadRef = useRef<HTMLInputElement>(null);
const handleFileUpload = (event: React.FormEvent<HTMLInputElement>) => {
const fileList = event.currentTarget.files || [];
const filesPath = Array.from(fileList).map(file => file.path);
const pathPrefix = dirPath.join('/');
ipcRenderer.send('file-upload', uploaderProfile.id, filesPath, pathPrefix);
event.currentTarget.value = '';
};
const [modalVisible, setModalVisible] = useState(false);
const [form] = Form.useForm();
const handleCreateDirectory = () => {
form.validateFields().then(values => {
ipcRenderer.send('directory-create', uploaderProfile.id, values?.directoryPath || '');
});
};
const handleDownload = (record: ListFile) => {
ipcRenderer.send('file-download', record.name, record.url);
};
const [exportLoading, setExportLoading] = useState(false);
const handleExport = () => {
const data = selectRows.map(item => {
const fileNameArr = item.name.split('.');
fileNameArr.pop();
return {
name: fileNameArr.join('.'),
url: item.url
};
});
setExportLoading(true);
ipcRenderer.send('export', data);
};
const columns: ColumnsType<ListFile> = [
{
title: '文件名',
dataIndex: 'name',
ellipsis: true,
render: (val: string, record: ListFile) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
{record.type === 'directory' ? (
<FolderFilled style={{ fontSize: 16 }} />
) : (
<FileOutlined style={{ fontSize: 16 }} />
)}
{record.type === 'directory' ? (
<a
title={val}
onClick={() => handleNameClick(record)}
className="table-filename"
style={{ marginLeft: 10, overflow: 'hidden', textOverflow: 'ellipsis' }}
>
{formatFileName(val)}
</a>
) : (
<Popover
placement="topLeft"
content={() =>
/(jpg|png|gif|jpeg)$/.test(val) ? (
<Image
style={{ maxWidth: 500 }}
src={record.url}
fallback="data:image/png;base64,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"
/>
) : (
val
)
}
trigger="hover"
>
<a
title={val}
onClick={() => handleNameClick(record)}
className="table-filename"
style={{ marginLeft: 10, overflow: 'hidden', textOverflow: 'ellipsis' }}
>
{formatFileName(val)}
</a>
</Popover>
)}
</div>
)
},
{
title: '文件大小',
dataIndex: 'size',
ellipsis: true,
width: 120,
render: val => (val ? filesize(val) : '-')
},
{
title: '更新时间',
dataIndex: 'lastModified',
ellipsis: true,
width: 200,
render: val => (val ? dayjs(val).format('YYYY-MM-DD HH:mm:ss') : '-')
},
{
title: '操作',
width: 120,
render: (_, record) => (
<Space>
{record.type !== 'directory' && (
<>
<DownloadOutlined onClick={() => handleDownload(record)} />
<CopyOutlined onClick={() => handleNameClick(record)} />
</>
)}
<DeleteOutlined onClick={() => handleDelete(record)} />
</Space>
)
}
];
return (
<div className="storage-page">
<header>
<span>文件管理</span>
<Divider />
</header>
<Space style={{ marginBottom: 10 }}>
<Select style={{ minWidth: 120 }} value={uploaderProfile?.id} onChange={setCurrentProfile}>
{uploaderProfiles.map(item => (
<Select.Option key={item.name} value={item.id}>
{item.name}
</Select.Option>
))}
</Select>
<Button
title="上传"
icon={<UploadOutlined />}
disabled={!hasFileManageFeature}
type="primary"
onClick={() => {
uploadRef.current?.click();
}}
/>
<Button title="刷新" icon={<ReloadOutlined />} disabled={!hasFileManageFeature} onClick={handleRefresh} />
<Button
title="创建文件夹"
icon={<FolderAddOutlined />}
disabled={!hasFileManageFeature}
onClick={() => {
setModalVisible(true);
}}
/>
<Button
title="导出"
icon={<ExportOutlined />}
disabled={selectRows.length === 0}
onClick={handleExport}
loading={exportLoading}
/>
<Button title="删除" icon={<DeleteOutlined />} disabled={selectRows.length === 0} onClick={handleBatchDelete} />
</Space>
<Breadcrumb style={{ marginBottom: 10 }}>
<Breadcrumb.Item>
<a onClick={() => handlePathClick(-1)}>全部文件</a>
</Breadcrumb.Item>
{dirPath.map((item, index) => (
<Breadcrumb.Item key={item}>
<a onClick={() => handlePathClick(index)}>{item}</a>
</Breadcrumb.Item>
))}
</Breadcrumb>
<div className="table-wrapper">
<Table
size="small"
rowKey="name"
scroll={{ y: windowHeight - 270 }}
dataSource={list}
columns={columns}
pagination={{
size: 'small',
defaultPageSize: 100,
pageSizeOptions: ['50', '100', '200'],
hideOnSinglePage: true
}}
loading={listLoading}
rowSelection={{
onChange: handleTableRowChange,
selectedRowKeys: selectRowKeys,
getCheckboxProps: record => ({ disabled: record?.type === 'directory' })
}}
/>
</div>
<input ref={uploadRef} type="file" multiple hidden onChange={handleFileUpload} />
<Modal
title="创建目录"
visible={modalVisible}
onCancel={() => setModalVisible(false)}
onOk={handleCreateDirectory}
destroyOnClose={true}
>
<Form form={form} preserve={false}>
<Form.Item
label="目录名称"
name="directoryPath"
rules={[{ required: true }, { pattern: domainPathRegExp, message: '目录名不能以 / 开头或结尾' }]}
>
<Input autoFocus />
</Form.Item>
</Form>
</Modal>
</div>
);
}
Example #25
Source File: index.tsx From Aragorn with MIT License | 4 votes |
Dashboard = () => {
const {
state: {
uploaderProfiles,
configuration: { defaultUploaderProfileId },
uploadedFiles
}
} = useAppContext();
const history = useHistory();
const [selectRowKeys, setRowKeys] = useState([]);
const [selectRows, setSelectRows] = useState([] as UploadedFileInfo[]);
const handleProfileAdd = () => {
history.push('/uploader');
};
const handleProfileClick = id => {
if (id === defaultUploaderProfileId) {
history.push(`/profile/${id}`);
} else {
ipcRenderer.send('set-default-uploader-profile', id);
}
};
const handleCopy = url => {
clipboard.writeText(url);
message.success('已复制到粘贴板');
};
const handleOpen = path => {
shell.showItemInFolder(path);
};
const handleTableRowChange = (selectedRowKeys, selectedRows) => {
setRowKeys(selectedRowKeys);
setSelectRows(selectedRows);
};
const handleClear = () => {
const ids = selectRows.map(item => item.id);
ipcRenderer.send('clear-upload-history', ids);
setRowKeys([]);
};
const handleReUpload = () => {
const data = selectRows.map(item => {
return { id: item.uploaderProfileId, path: item.path };
});
ipcRenderer.send('file-reupload', data);
setRowKeys([]);
};
const columns: ColumnsType<UploadedFileInfo> = [
{
title: '文件名',
dataIndex: 'name',
ellipsis: true,
render: (val, record) => (
<Popover
placement="topLeft"
content={() =>
/(jpg|png|gif|jpeg)$/.test(val) ? (
<Image
style={{ maxWidth: 500 }}
src={record.url}
fallback="data:image/png;base64,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"
/>
) : (
val
)
}
trigger="hover"
>
<span onClick={() => handleCopy(record.url)} className="row-item">
{val}
</span>
</Popover>
)
},
{
title: '类型',
dataIndex: 'type',
ellipsis: true,
width: 120
},
{
title: '上传器配置',
dataIndex: 'uploaderProfileId',
ellipsis: true,
width: 120,
render: val => (
<a onClick={() => handleProfileClick(val)}>
{uploaderProfiles.find(item => item.id === val)?.name || '未找到'}
</a>
)
},
{
title: '状态',
dataIndex: 'url',
ellipsis: true,
width: 80,
render: val => (
<>
<Badge status={val ? 'success' : 'error'} />
{val ? '成功' : '失败'}
</>
)
},
{
title: '上传时间',
dataIndex: 'date',
width: 180,
ellipsis: true,
render: val => dayjs(val).format('YYYY-MM-DD HH:mm:ss')
},
{
title: '操作',
width: 80,
render: (_, record) => (
<Space>
<FolderOpenOutlined onClick={() => handleOpen(record.path)} />
<CopyOutlined onClick={() => handleCopy(record.url)} />
</Space>
)
}
];
return (
<div className="dashboard-page">
<header>
<span>控制台</span>
<Divider />
</header>
<main>
<div className="profile-wrapper">
<div className="title">上传器配置</div>
<div className="card-wrapper">
{uploaderProfiles.map(item => (
<div
key={item.id}
className={item.id === defaultUploaderProfileId ? 'card card-active' : 'card'}
onClick={() => handleProfileClick(item.id)}
>
<Box className="card-icon" />
<span>{item.name}</span>
</div>
))}
<div className="card" onClick={handleProfileAdd}>
<Plus className="card-icon" />
</div>
</div>
</div>
<div className="history-wrapper">
<div className="title">最近上传</div>
<div className="card-wrapper">
{selectRowKeys.length > 0 && (
<Space style={{ marginBottom: 10 }}>
<Button icon={<DeleteOutlined />} onClick={handleClear}>
清除
</Button>
<Button icon={<UploadOutlined />} onClick={handleReUpload}>
重新上传
</Button>
</Space>
)}
<Table
size="small"
rowKey="id"
dataSource={uploadedFiles}
columns={columns}
rowSelection={{ onChange: handleTableRowChange, selectedRowKeys: selectRowKeys }}
/>
</div>
</div>
</main>
</div>
);
}
Example #26
Source File: AyFormList.tsx From amiya with MIT License | 4 votes |
export default function AyFormList(props: AyFormListProps) {
const { field, getFormItem, formInstant, ayFormProps } = props
// 最少行 & 最大行
const { min = 0, max = Infinity } = field
// 当前行数
const [recordNum, setRecordNum] = useState(0)
useEffect(() => {
setRecordNum(formInstant.getFieldValue(field.key).length)
}, [])
/**
* 复制这行数据到末尾
* @param name 实际上是当前行 index
*/
const handleCopy = (name: number) => {
try {
let value = formInstant.getFieldValue(field.key)
let newValue = [...value, value[name]]
setRecordNum(newValue.length)
formInstant.setFieldsValue({ [field.key || '']: newValue })
} catch {
console.error('复制失败')
}
}
/**
* 删除这行数据
* @param name 实际上是当前行 index
* @param remove 删除方法
*/
const handleRemove = (name: number, remove: (name: number) => void) => {
remove(name)
setRecordNum(Number(recordNum) - 1)
}
/**
* 新增一行
* @param add 新增方法
*/
const handleAdd = (add: (defaultValue?: any, insertIndex?: number | undefined) => void) => {
add(field.creatorRecord || {})
setRecordNum(Number(recordNum) + 1)
}
return (
<Form.List {...field.props} name={field.key || field.label} key={field.key || field.label}>
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => {
let children = field.children || []
if (!Array.isArray(children)) {
children = [children]
}
let content = getFormItem(
children.map((child: AyFormField) => ({
...child,
formItemProps: {
...field.formItemProps,
...restField,
name: [name, child.key]
}
})) as Array<AyFormField | AySearchTableField>,
formInstant,
ayFormProps,
FORM_TYPE_LIST
)
return (
<Space key={`${field.key}-${key}`} className="ay-form-list-item" align="end" {...field.spaceProps}>
{content}
<Space className="ay-form-list-actions">
{recordNum < max && (
<span className="ay-form-list-action" onClick={() => handleCopy(name)}>
<Tooltip title={locale.form.copyToEnd}>
<CopyOutlined />
</Tooltip>
</span>
)}
{recordNum > min && (
<span className="ay-form-list-action" onClick={() => handleRemove(name, remove)}>
<Tooltip title={locale.form.removeRow}>
<DeleteOutlined />
</Tooltip>
</span>
)}
</Space>
</Space>
)
})}
{recordNum < max && (
<Button type="dashed" onClick={() => handleAdd(add)} icon={<PlusOutlined />}>
{locale.form.addItem}
</Button>
)}
</>
)}
</Form.List>
)
}
Example #27
Source File: index.tsx From fe-v5 with Apache License 2.0 | 4 votes |
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 #28
Source File: MITMPage.tsx From yakit with GNU Affero General Public License v3.0 | 4 votes |
MITMPage: React.FC<MITMPageProp> = (props) => {
const [status, setStatus] = useState<"idle" | "hijacked" | "hijacking">("idle");
const [error, setError] = useState("");
const [host, setHost] = useState("127.0.0.1");
const [port, setPort] = useState(8083);
const [downstreamProxy, setDownstreamProxy] = useState<string>();
const [loading, setLoading] = useState(false);
const [caCerts, setCaCerts] = useState<CaCertData>({
CaCerts: new Buffer(""), LocalFile: "",
});
const [enableInitialPlugin, setEnableInitialPlugin] = useState(false);
// 存储修改前和修改后的包!
const [currentPacketInfo, setCurrentPacketInfo] = useState<{
currentPacket: Uint8Array,
currentPacketId: number,
isHttp: boolean
}>({currentPacketId: 0, currentPacket: new Buffer([]), isHttp: true});
const {currentPacket, currentPacketId, isHttp} = currentPacketInfo;
const clearCurrentPacket = () => {
setCurrentPacketInfo({currentPacketId: 0, currentPacket: new Buffer([]), isHttp: true})
}
const [modifiedPacket, setModifiedPacket] = useState<Uint8Array>(new Buffer([]));
// 自动转发 与 劫持响应的自动设置
const [autoForward, setAutoForward, getAutoForward] = useGetState<"manual" | "log" | "passive">("log");
const isManual = autoForward === "manual";
const [hijackAllResponse, setHijackAllResponse] = useState(false); // 劫持所有请求
const [allowHijackCurrentResponse, setAllowHijackCurrentResponse] = useState(false); // 仅劫持一个请求
const [initialed, setInitialed] = useState(false);
const [forResponse, setForResponse] = useState(false);
const [haveSideCar, setHaveSideCar] = useState(true);
const [urlInfo, setUrlInfo] = useState("监听中...")
const [ipInfo, setIpInfo] = useState("")
// 设置初始化启动的插件
const [defaultPlugins, setDefaultPlugins] = useState<string[]>([]);
// yakit log message
const [logs, setLogs] = useState<ExecResultLog[]>([]);
const latestLogs = useLatest<ExecResultLog[]>(logs);
const [_, setLatestStatusHash, getLatestStatusHash] = useGetState("");
const [statusCards, setStatusCards] = useState<StatusCardProps[]>([])
// filter 过滤器
const [mitmFilter, setMITMFilter] = useState<MITMFilterSchema>();
// 内容替代模块
const [replacers, setReplacers] = useState<MITMContentReplacerRule[]>([]);
// mouse
const mouseState = useMouse();
// 操作系统类型
const [system, setSystem] = useState<string>()
useEffect(() => {
ipcRenderer.invoke('fetch-system-name').then((res) => setSystem(res))
}, [])
useEffect(() => {
// 设置 MITM 初始启动插件选项
getValue(CONST_DEFAULT_ENABLE_INITIAL_PLUGIN).then(a => {
setEnableInitialPlugin(!!a)
})
}, [])
// 用于接受后端传回的信息
useEffect(() => {
setInitialed(false)
// 用于前端恢复状态
ipcRenderer.invoke("mitm-have-current-stream").then(data => {
const {haveStream, host, port} = data;
if (haveStream) {
setStatus("hijacking")
setHost(host);
setPort(port);
}
}).finally(() => {
recover()
setTimeout(() => setInitialed(true), 500)
})
// 用于启动 MITM 开始之后,接受开始成功之后的第一个消息,如果收到,则认为说 MITM 启动成功了
ipcRenderer.on("client-mitm-start-success", () => {
setStatus("hijacking")
setTimeout(() => {
setLoading(false)
}, 300)
})
// 用于 MITM 的 Message (YakitLog)
const messages: ExecResultLog[] = [];
const statusMap = new Map<string, StatusCardProps>();
let lastStatusHash = '';
ipcRenderer.on("client-mitm-message", (e, data: ExecResult) => {
let msg = ExtractExecResultMessage(data);
if (msg !== undefined) {
// logHandler.logs.push(msg as ExecResultLog)
// if (logHandler.logs.length > 25) {
// logHandler.logs.shift()
// }
const currentLog = msg as ExecResultLog;
if (currentLog.level === "feature-status-card-data") {
lastStatusHash = `${currentLog.timestamp}-${currentLog.data}`
try {
// 解析 Object
const obj = JSON.parse(currentLog.data)
const {id, data} = obj;
if (!data) {
statusMap.delete(`${id}`)
} else {
statusMap.set(`${id}`, {Data: data, Id: id, Timestamp: currentLog.timestamp})
}
} catch (e) {
}
return
}
messages.push(currentLog)
if (messages.length > 25) {
messages.shift()
}
}
})
// let currentFlow: HTTPFlow[] = []
ipcRenderer.on("client-mitm-history-update", (e: any, data: any) => {
// currentFlow.push(data.historyHTTPFlow as HTTPFlow)
//
// if (currentFlow.length > 30) {
// currentFlow = [...currentFlow.slice(0, 30)]
// }
// setFlows([...currentFlow])
})
ipcRenderer.on("client-mitm-error", (e, msg) => {
if (!msg) {
info("MITM 劫持服务器已关闭")
} else {
failed("MITM 劫持服务器异常或被关闭")
Modal.error({
mask: true, title: "启动 MITM 服务器 ERROR!",
content: <>{msg}</>
})
}
ipcRenderer.invoke("mitm-stop-call")
setError(`${msg}`)
setStatus("idle")
setTimeout(() => {
setLoading(false)
}, 300)
});
ipcRenderer.on("client-mitm-filter", (e, msg) => {
ipcRenderer
.invoke("get-value", DefaultMitmFilter)
.then((res: any) => {
if (res) {
const filter = {
includeSuffix: res.includeSuffix,
excludeMethod: res.excludeMethod,
excludeSuffix: res.excludeSuffix,
includeHostname: res.includeHostname,
excludeHostname: res.excludeHostname,
excludeContentTypes: res.excludeContentTypes,
}
setMITMFilter(filter)
ipcRenderer.invoke("mitm-filter", {
updateFilter: true, ...filter
})
} else {
setMITMFilter({
includeSuffix: msg.includeSuffix,
excludeMethod: msg.excludeMethod,
excludeSuffix: msg.excludeSuffix,
includeHostname: msg.includeHostname,
excludeHostname: msg.excludeHostname,
excludeContentTypes: msg.excludeContentTypes,
})
}
})
})
const updateLogs = () => {
if (latestLogs.current.length !== messages.length) {
setLogs([...messages])
return
}
if (latestLogs.current.length > 0 && messages.length > 0) {
if (latestLogs.current[0].data !== messages[0].data) {
setLogs([...messages])
return
}
}
if (getLatestStatusHash() !== lastStatusHash) {
setLatestStatusHash(lastStatusHash)
const tmpCurrent: StatusCardProps[] = [];
statusMap.forEach((value, key) => {
tmpCurrent.push(value)
})
setStatusCards(tmpCurrent.sort((a, b) => a.Id.localeCompare(b.Id)))
}
}
updateLogs()
let id = setInterval(() => {
updateLogs()
}, 1000)
return () => {
clearInterval(id);
ipcRenderer.removeAllListeners("client-mitm-error")
// ipcRenderer.invoke("mitm-close-stream")
}
}, [])
useEffect(() => {
if (hijackAllResponse && currentPacketId > 0) {
allowHijackedResponseByRequest(currentPacketId)
}
}, [hijackAllResponse, currentPacketId])
useEffect(() => {
ipcRenderer.on("client-mitm-hijacked", forwardHandler);
return () => {
ipcRenderer.removeAllListeners("client-mitm-hijacked")
}
}, [autoForward])
useEffect(() => {
ipcRenderer.invoke("mitm-auto-forward", !isManual).finally(() => {
console.info(`设置服务端自动转发:${!isManual}`)
})
}, [autoForward])
useEffect(() => {
ipcRenderer.on("client-mitm-content-replacer-update", (e, data: MITMResponse) => {
setReplacers(data?.replacers || [])
return
});
return () => {
ipcRenderer.removeAllListeners("client-mitm-content-replacer-update")
}
}, [])
useEffect(() => {
if (currentPacketId <= 0 && status === "hijacked") {
recover()
const id = setInterval(() => {
recover()
}, 500)
return () => {
clearInterval(id)
}
}
}, [currentPacketId])
useEffect(() => {
ipcRenderer.invoke("DownloadMITMCert", {}).then((data: CaCertData) => {
setCaCerts(data)
})
}, [])
const addr = `http://${host}:${port}`;
// 自动转发劫持,进行的操作
const forwardHandler = useMemoizedFn((e: any, msg: MITMResponse) => {
setMITMFilter({
includeSuffix: msg.includeSuffix,
excludeMethod: msg.excludeMethod,
excludeSuffix: msg.excludeSuffix,
includeHostname: msg.includeHostname,
excludeHostname: msg.excludeHostname,
excludeContentTypes: msg.excludeContentTypes,
})
// passive 模式是 mitm 插件模式
// 在这个模式下,应该直接转发,不应该操作数据包
// if (passiveMode) {
// if (msg.forResponse) {
// forwardResponse(msg.responseId || 0)
// } else {
// forwardRequest(msg.id || 0)
// }
// return
// }
if (msg.forResponse) {
if (!msg.response || !msg.responseId) {
failed("BUG: MITM 错误,未能获取到正确的 Response 或 Response ID")
return
}
if (!isManual) {
forwardResponse(msg.responseId || 0)
if (!!currentPacket) {
clearCurrentPacket()
}
} else {
setForResponse(true)
setStatus("hijacked")
setCurrentPacketInfo({
currentPacket: msg.response,
currentPacketId: msg.responseId,
isHttp: msg.isHttps
})
// setCurrentPacket(new Buffer(msg.response).toString("utf8"))
// setCurrentPacketId(msg.responseId || 0);
}
} else {
if (msg.request) {
if (!isManual) {
forwardRequest(msg.id)
if (!!currentPacket) {
clearCurrentPacket()
}
// setCurrentPacket(String.fromCharCode.apply(null, msg.request))
} else {
setStatus("hijacked")
setForResponse(false)
// setCurrentPacket(msg.request)
// setCurrentPacketId(msg.id)
setCurrentPacketInfo({currentPacket: msg.request, currentPacketId: msg.id, isHttp: msg.isHttps})
setUrlInfo(msg.url)
ipcRenderer.invoke("fetch-url-ip", msg.url.split('://')[1].split('/')[0]).then((res) => {
setIpInfo(res)
})
}
}
}
})
// 这个 Forward 主要用来转发修改后的内容,同时可以转发请求和响应
const forward = useMemoizedFn(() => {
// ID 不存在
if (!currentPacketId) {
return
}
setLoading(true);
setStatus("hijacking");
setAllowHijackCurrentResponse(false)
setForResponse(false)
if (forResponse) {
ipcRenderer.invoke("mitm-forward-modified-response", modifiedPacket, currentPacketId).finally(() => {
clearCurrentPacket()
setTimeout(() => setLoading(false))
})
} else {
ipcRenderer.invoke("mitm-forward-modified-request", modifiedPacket, currentPacketId).finally(() => {
clearCurrentPacket()
setTimeout(() => setLoading(false))
})
}
})
const recover = useMemoizedFn(() => {
ipcRenderer.invoke("mitm-recover").then(() => {
// success("恢复 MITM 会话成功")
})
})
const start = useMemoizedFn(() => {
setLoading(true)
setError("")
ipcRenderer.invoke("mitm-start-call", host, port, downstreamProxy).catch((e: any) => {
notification["error"]({message: `启动中间人劫持失败:${e}`})
})
})
const stop = useMemoizedFn(() => {
setLoading(true)
ipcRenderer.invoke("mitm-stop-call").then(() => {
setStatus("idle")
}).catch((e: any) => {
notification["error"]({message: `停止中间人劫持失败:${e}`})
}).finally(() => setTimeout(() => {
setLoading(false)
}, 300))
})
const hijacking = useMemoizedFn(() => {
// setCurrentPacket(new Buffer([]));
clearCurrentPacket()
setLoading(true);
setStatus("hijacking");
})
function getCurrentId() {
return currentPacketId
}
const downloadCert = useMemoizedFn(() => {
return <Tooltip title={'请先下载 SSL/TLS 证书'}>
<Button
type={"link"}
style={{padding: '4px 6px'}}
onClick={() => {
const text = `wget -e use_proxy=yes -e http_proxy=${addr} http://download-mitm-cert.yaklang.io -O yakit-mitm-cert.pem`
showModal({
title: "下载 SSL/TLS 证书以劫持 HTTPS",
width: "50%",
content: <Space direction={"vertical"} style={{width: "100%"}}>
<AutoCard
title={"证书配置"}
extra={<Button
type={"link"}
onClick={() => {
saveABSFileToOpen("yakit证书.crt.pem", caCerts.CaCerts)
// openABSFileLocated(caCerts.LocalFile)
}}
>
下载到本地并打开
</Button>} size={"small"} bodyStyle={{padding: 0}}>
<div style={{height: 360}}>
<YakEditor bytes={true}
valueBytes={caCerts.CaCerts}
/>
</div>
</AutoCard>
<Alert message={<Space>
在设置代理后访问:<CopyableField text={"http://download-mitm-cert.yaklang.io"}/> 可自动下载证书
</Space>}/>
</Space>
})
}}
>HTTPS 证书配置</Button>
</Tooltip>
})
const contentReplacer = useMemoizedFn(() => {
return <Button
type={"link"} style={{padding: `4px 6px`}}
onClick={() => {
let m = showDrawer({
placement: "top", height: "50%",
content: (
<MITMContentReplacer
rules={replacers}
onSaved={rules => {
setReplacers(rules)
m.destroy()
}}/>
),
maskClosable: false,
})
}}
>
匹配/标记/替换
</Button>
})
const setFilter = useMemoizedFn(() => {
return <Button type={"link"} style={{padding: '4px 6px'}}
onClick={() => {
let m = showDrawer({
placement: "top", height: "50%",
content: <>
<MITMFilters
filter={mitmFilter}
onFinished={(filter) => {
setMITMFilter({...filter})
m.destroy()
}}/>
</>
});
}}
>过滤器</Button>
})
const handleAutoForward = useMemoizedFn((e: "manual" | "log" | "passive") => {
if (!isManual) {
info("切换为劫持自动放行模式(仅记录)")
setHijackAllResponse(false)
} else {
info("切换为手动放行模式(可修改劫持)")
}
setAutoForward(e)
if (currentPacket && currentPacketId) {
forward()
}
})
const execFuzzer = useMemoizedFn((value: string) => {
ipcRenderer.invoke("send-to-tab", {
type: "fuzzer",
data: {isHttps: currentPacketInfo.isHttp, request: value}
})
})
const execPlugin = useMemoizedFn((value: string) => {
ipcRenderer.invoke("send-to-packet-hack", {
request: currentPacketInfo.currentPacket,
ishttps: currentPacketInfo.isHttp
})
})
const shiftAutoForwardHotkey = useHotkeys('ctrl+t', () => {
handleAutoForward(isManual ? "manual" : "log")
}, [autoForward])
if (!initialed) {
return <div style={{textAlign: "center", paddingTop: 120}}>
<Spin spinning={true} tip={"正在初始化 MITM"}/>
</div>
}
return <div style={{height: "100%", width: "100%"}}>
{(() => {
switch (status) {
case "idle":
return <Spin spinning={loading}>
<Form
style={{marginTop: 40}}
onSubmitCapture={e => {
e.preventDefault()
start()
if (enableInitialPlugin) {
enableMITMPluginMode(defaultPlugins).then(() => {
info("被动扫描插件模式已启动")
})
}
}}
layout={"horizontal"} labelCol={{span: 7}}
wrapperCol={{span: 13}}
>
<Item label={"劫持代理监听主机"}>
<Input value={host} onChange={e => setHost(e.target.value)}/>
</Item>
<Item label={"劫持代理监听端口"}>
<InputNumber value={port} onChange={e => setPort(e)}/>
</Item>
{/*<SwitchItem label={"启动 MITM 插件"} size={"small"} setValue={e => {*/}
{/* setEnableInitialPlugin(e)*/}
{/* if (e) {*/}
{/* saveValue(CONST_DEFAULT_ENABLE_INITIAL_PLUGIN, "true")*/}
{/* } else {*/}
{/* saveValue(CONST_DEFAULT_ENABLE_INITIAL_PLUGIN, "")*/}
{/* }*/}
{/*}} value={enableInitialPlugin}/>*/}
<Item label={"选择插件"} colon={true}>
<div style={{height: 200, maxWidth: 420}}>
<SimplePluginList
disabled={!enableInitialPlugin}
bordered={true}
initialSelected={defaultPlugins}
onSelected={(list: string[]) => {
setDefaultPlugins(list)
}} pluginTypes={"mitm,port-scan"}
verbose={<div>MITM 与 端口扫描插件</div>}/>
</div>
</Item>
<Item label={"下游代理"} help={"为经过该 MITM 代理的请求再设置一个代理,通常用于访问中国大陆无法访问的网站或访问特殊网络/内网,也可用于接入被动扫描"}>
<Input value={downstreamProxy} onChange={e => setDownstreamProxy(e.target.value)}/>
</Item>
<Item label={"内容规则"} help={"使用规则进行匹配、替换、标记、染色,同时配置生效位置"}>
<Space>
<Button
onClick={() => {
let m = showDrawer({
placement: "top", height: "50%",
content: (
<MITMContentReplacerViewer/>
),
maskClosable: false,
})
}}
>已有规则</Button>
<Button type={"link"} onClick={() => {
const m = showModal({
title: "从 JSON 中导入",
width: "60%",
content: (
<>
<MITMContentReplacerImport onClosed={() => {
m.destroy()
}}/>
</>
)
})
}}>从 JSON 导入</Button>
<Button type={"link"} onClick={() => {
showModal({
title: "导出配置 JSON",
width: "50%",
content: (
<>
<MITMContentReplacerExport/>
</>
)
})
}}>导出为 JSON</Button>
</Space>
</Item>
<Item label={" "} colon={false}>
<Space>
<Button type={"primary"} htmlType={"submit"}>
劫持启动
</Button>
<Divider type={"vertical"}/>
<Checkbox
checked={enableInitialPlugin}
onChange={node => {
const e = node.target.checked;
setEnableInitialPlugin(e)
if (e) {
saveValue(CONST_DEFAULT_ENABLE_INITIAL_PLUGIN, "true")
} else {
saveValue(CONST_DEFAULT_ENABLE_INITIAL_PLUGIN, "")
}
}}
>
插件自动加载
</Checkbox>
</Space>
</Item>
</Form>
</Spin>
case "hijacking":
case "hijacked":
return <div id={"mitm-hijacking-container"} ref={shiftAutoForwardHotkey as Ref<any>} tabIndex={-1}
style={{marginLeft: 12, marginRight: 12, height: "100%"}}>
<Row gutter={14} style={{height: "100%"}}>
<Col span={haveSideCar ? 24 : 24}
style={{display: "flex", flexDirection: "column", height: "100%"}}>
<PageHeader
className="mitm-header-title"
title={'劫持 HTTP Request'} subTitle={`http://${host}:${port}`}
style={{marginRight: 0, paddingRight: 0, paddingTop: 0, paddingBottom: 8}}
extra={
<Space>
<ChromeLauncherButton host={host} port={port}/>
{contentReplacer()}
{setFilter()}
{downloadCert()}
<Button danger={true} type={"link"}
onClick={() => {
stop()
setUrlInfo("监听中...")
setIpInfo("")
}} icon={<PoweroffOutlined/>}
/>
</Space>}>
<Row>
<Col span={12}>
<div style={{width: "100%", textAlign: "left"}}>
<Space>
<Button
type={"primary"}
disabled={status === "hijacking"}
onClick={() => {
forward()
}}>提交数据</Button>
<Button
disabled={status === "hijacking"}
danger={true}
onClick={() => {
hijacking()
if (forResponse) {
dropResponse(currentPacketId).finally(() => {
setTimeout(() => {
setLoading(false)
}, 300)
})
} else {
dropRequest(currentPacketId).finally(() => {
setTimeout(() => setLoading(false), 300)
})
}
setUrlInfo("监听中...")
setIpInfo("")
}}>丢弃请求</Button>
{
(!forResponse && !!currentPacket) && // 劫持到的请求有内容
status === "hijacked" && // 劫持到的状态是 hijacked
!hijackAllResponse && // 如果已经设置了劫持所有请求,就不展示了
<Button
disabled={allowHijackCurrentResponse}
type={allowHijackCurrentResponse ? "primary" : "default"}
onClick={() => {
if (!allowHijackCurrentResponse) {
allowHijackedResponseByRequest(currentPacketId)
setAllowHijackCurrentResponse(true)
} else {
setAllowHijackCurrentResponse(false)
}
}}>
劫持响应 {
allowHijackCurrentResponse &&
<CheckOutlined/>
}
</Button>}
</Space>
</div>
</Col>
<Col span={12}>
<div style={{width: "100%", textAlign: "right"}}>
<Space>
{isManual && <div>
<span style={{marginRight: 4}}>劫持响应:</span>
<Checkbox checked={hijackAllResponse} onClick={e => {
if (!hijackAllResponse) {
info("劫持所有响应内容")
} else {
info("仅劫持请求")
}
setHijackAllResponse(!hijackAllResponse)
}}/>
</div>}
<SelectOne
data={[
{text: "手动劫持", value: "manual"},
{text: "自动放行", value: "log"},
{text: "被动日志", value: "passive"},
]}
value={autoForward}
formItemStyle={{marginBottom: 0}}
setValue={(e) => {
ipcRenderer.invoke("mitm-filter", {updateFilter: true, ...mitmFilter})
handleAutoForward(e)
}}
/>
</Space>
</div>
</Col>
</Row>
<Row>
<Col span={12}>
<div style={{
width: "100%", textAlign: "left", height: '100%',
display: 'flex'
}}>
{!isManual &&
<Text style={{alignSelf: 'center'}}>
{`目标:自动放行中...`}</Text>}
{autoForward === "manual" &&
<>
<Text title={urlInfo} ellipsis={true} style={{
alignSelf: 'center',
maxWidth: 300
}}>{status === 'hijacking' ? '目标:监听中...' : `目标:${urlInfo}`}</Text>
{ipInfo && status !== 'hijacking' &&
<Tag
color='green'
title={ipInfo}
style={{
marginLeft: 5,
alignSelf: "center",
maxWidth: 140,
cursor: "pointer"
}}
>
{`${ipInfo}`}
<CopyToClipboard
text={`${ipInfo}`}
onCopy={(text, ok) => {
if (ok) success("已复制到粘贴板")
}}
>
<CopyOutlined style={{marginLeft: 5}}/>
</CopyToClipboard>
</Tag>
}
</>
}
</div>
</Col>
<Col span={12}>
<div style={{width: "100%", textAlign: "right"}}>
<Button
type={"link"} onClick={() => recover()}
icon={<ReloadOutlined/>}
>恢复请求</Button>
</div>
</Col>
</Row>
</PageHeader>
<div style={{flex: 1, overflowY: 'hidden'}}>
{/*<Spin wrapperClassName={"mitm-loading-spin"} spinning={status === "hijacking"}>*/}
<div style={{height: "100%"}}>
<ResizeBox
isVer={false}
firstNode={(
<MITMPluginList
proxy={`http://${host}:${port}`}
downloadCertNode={downloadCert}
setFilterNode={setFilter}
onExit={() => {
stop()
}}
onSubmitScriptContent={e => {
ipcRenderer.invoke("mitm-exec-script-content", e)
}}
onSubmitYakScriptId={(id: number, params: YakExecutorParam[]) => {
info(`加载 MITM 插件[${id}]`)
ipcRenderer.invoke("mitm-exec-script-by-id", id, params)
}}
/>
// <MITMPluginOperator />
)}
firstMinSize={"330px"}
secondMinSize={"340px"}
firstRatio={"330px"}
secondNode={(
<AutoCard
style={{margin: 0, padding: 0}}
bodyStyle={{margin: 0, padding: 0, overflowY: "hidden"}}
>
{autoForward === "log" && (
<MITMPluginCard
onSubmitScriptContent={(e) => {
ipcRenderer.invoke("mitm-exec-script-content", e)
}}
onSubmitYakScriptId={(
id: number,
params: YakExecutorParam[]
) => {
info(`加载 MITM 插件[${id}]`)
ipcRenderer.invoke("mitm-exec-script-by-id", id, params)
}}
/>
)}
{autoForward === "manual" && (
<HTTPPacketEditor
originValue={currentPacket}
noHeader={true}
bordered={false}
onChange={setModifiedPacket}
noPacketModifier={true}
readOnly={status === "hijacking"}
refreshTrigger={
(forResponse ? `rsp` : `req`) + `${currentPacketId}`
}
actions={[
// {
// id: "send-to-scan-packet", label: "发送到数据包扫描器",
// run: e => {
// // console.info(mouseState)
// scanPacket(mouseState, false, "GET / HTTP/1.1\r\nHost: www.baidu.com", "")
// }, contextMenuGroupId: "Scanners",
// },
...(forResponse
? [
{
id: "trigger-auto-hijacked",
label: "切换为自动劫持模式",
keybindings: [
monaco.KeyMod.Shift |
(system === "Darwin" ? monaco.KeyMod.WinCtrl : monaco.KeyMod.CtrlCmd) |
monaco.KeyCode.KEY_T
],
run: () => {
handleAutoForward(getAutoForward() === "manual" ? "log" : "manual")
},
contextMenuGroupId: "Actions"
},
{
id: "forward-response",
label: "放行该 HTTP Response",
run: function () {
forward()
// hijacking()
// forwardResponse(getCurrentId()).finally(() => {
// setTimeout(() => setLoading(false), 300)
// })
},
contextMenuGroupId: "Actions"
},
{
id: "drop-response",
label: "丢弃该 HTTP Response",
run: function () {
hijacking()
dropResponse(getCurrentId()).finally(
() => {
setTimeout(
() => setLoading(false),
300
)
}
)
},
contextMenuGroupId: "Actions"
}
]
: [
{
id: "trigger-auto-hijacked",
label: "切换为自动劫持模式",
keybindings: [
monaco.KeyMod.Shift |
(system === "Darwin" ? monaco.KeyMod.WinCtrl : monaco.KeyMod.CtrlCmd) |
monaco.KeyCode.KEY_T
],
run: () => {
handleAutoForward(getAutoForward() === "manual" ? "log" : "manual")
},
contextMenuGroupId: "Actions"
},
{
id: "send-to-fuzzer",
label: "发送到 Web Fuzzer",
keybindings: [
monaco.KeyMod.Shift |
(system === "Darwin" ? monaco.KeyMod.WinCtrl : monaco.KeyMod.CtrlCmd) |
monaco.KeyCode.KEY_R
],
run: function (StandaloneEditor: any) {
execFuzzer(StandaloneEditor.getModel().getValue())
},
contextMenuGroupId: "Actions"
},
{
id: "send-to-plugin",
label: "发送到 数据包扫描",
keybindings: [
monaco.KeyMod.Shift |
(system === "Darwin" ? monaco.KeyMod.WinCtrl : monaco.KeyMod.CtrlCmd) |
monaco.KeyCode.KEY_E
],
run: function (StandaloneEditor: any) {
if (!StandaloneEditor.getModel().getValue()) return
execPlugin(StandaloneEditor.getModel().getValue())
},
contextMenuGroupId: "Actions"
},
{
id: "forward-response",
label: "放行该 HTTP Request",
keybindings: [
monaco.KeyMod.Shift |
(system === "Darwin" ? monaco.KeyMod.WinCtrl : monaco.KeyMod.CtrlCmd) |
monaco.KeyCode.KEY_F
],
run: function () {
forward()
// hijacking()
// forwardRequest(getCurrentId()).finally(() => {
// setTimeout(() => setLoading(false), 300)
// })
},
contextMenuGroupId: "Actions"
},
{
id: "drop-response",
label: "丢弃该 HTTP Request",
run: function () {
hijacking()
dropRequest(getCurrentId()).finally(
() => {
setTimeout(
() => setLoading(false),
300
)
}
)
},
contextMenuGroupId: "Actions"
},
{
id: "hijack-current-response",
label: "劫持该 Request 对应的响应",
run: function () {
allowHijackedResponseByRequest(
getCurrentId()
)
},
contextMenuGroupId: "Actions"
}
])
]}
/>
)}
{autoForward === "passive" && (
<MITMPluginLogViewer
messages={logs} status={statusCards}
/>
)}
</AutoCard>
)}
/>
</div>
{/*</Spin>*/}
</div>
</Col>
</Row>
</div>
default:
return <div/>
}
})()}
</div>
}
Example #29
Source File: EditItem.tsx From fe-v5 with Apache License 2.0 | 4 votes |
export default function EditItem(props: Props) {
const { visible, onChange, value, range, id } = props;
const { t } = useTranslation();
const [form] = Form.useForm();
useEffect(() => {
value && form.setFieldsValue(value);
}, [value]);
const handleOk = async () => {
await form.validateFields();
const v: FormType = form.getFieldsValue();
onChange(v);
};
const onCancel = () => {
onChange(undefined);
};
const onFinish = (values) => {
console.log('Received values of form:', values);
};
const handleBlur = (index) => {
const reg = form.getFieldValue(['var', index, 'reg']);
const expression = form.getFieldValue(['var', index, 'definition']);
if ((!reg || new RegExp('^/(.*?)/(g?i?m?y?)$').test(reg)) && expression) {
const formData = form.getFieldsValue();
var newExpression = replaceExpressionVars(expression, formData, index, id);
convertExpressionToQuery(newExpression, range).then((res) => {
const regFilterRes = res.filter((i) => !reg || !stringToRegex(reg) || (stringToRegex(reg) as RegExp).test(i));
if (regFilterRes.length > 0) {
setVaraiableSelected(formData.var[index].name, regFilterRes[0], id);
}
// form.setFields([{ name: ['var', index, 'selected'], value: regFilterRes[0] }]);
});
}
};
return (
<Modal title={t('大盘变量')} width={950} visible={visible} onOk={handleOk} onCancel={onCancel} wrapClassName='variable-modal'>
<Form name='dynamic_form_nest_item' onFinish={onFinish} autoComplete='off' preserve={false} form={form}>
<Row gutter={[6, 6]} className='tag-header'>
<Col span={4}>{t('变量名')}</Col>
<Col span={6}>
{t('变量定义')}
<QuestionCircleOutlined
style={{ marginLeft: 5 }}
onClick={() => window.open('https://grafana.com/docs/grafana/latest/datasources/prometheus/#query-variable', '_blank')}
/>
</Col>
<Col span={6}>{t('筛值正则')}</Col>
<Col span={2}>{t('Multi')}</Col>
<Col span={2}>{t('All Option')}</Col>
<Col span={4}>{t('操作')}</Col>
</Row>
<Form.List name='var'>
{(fields, { add, remove, move }) => (
<>
{fields.map(({ key, name, fieldKey, ...restField }) => (
<Row gutter={[6, 6]} className='tag-content-item' key={key}>
<Col span={4}>
<Form.Item
{...restField}
name={[name, 'name']}
fieldKey={[fieldKey, 'name']}
rules={[
{ required: true, message: t('请输入变量名') },
{ pattern: /^[0-9a-zA-Z_]+$/, message: t('仅支持数字和字符下划线') },
]}
>
<Input />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
{...restField}
name={[name, 'definition']}
fieldKey={[fieldKey, 'definition']}
rules={[
{ required: true, message: t('请输入变量定义') },
{
validator(_, value) {
if (/^\s*label_values.+,\s*\$.+/.test(value)) {
return Promise.reject(new Error('label_values表达式的label不允许使用变量'));
}
return Promise.resolve();
},
},
]}
>
<Input onBlur={(v) => handleBlur(name)} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item {...restField} name={[name, 'reg']} fieldKey={[fieldKey, 'reg']} rules={[{ pattern: new RegExp('^/(.*?)/(g?i?m?y?)$'), message: t('格式不对') }]}>
<Input placeholder='/*.hna/' onBlur={(v) => handleBlur(name)} />
</Form.Item>
</Col>
<Col span={2}>
<Form.Item {...restField} name={[name, 'multi']} fieldKey={[fieldKey, 'multi']} valuePropName='checked'>
<Switch />
</Form.Item>
</Col>
<Col span={2}>
<Form.Item shouldUpdate style={{ margin: 0 }}>
{() => {
return (
form.getFieldValue(['var', name, 'multi']) && (
<Form.Item {...restField} name={[name, 'allOption']} fieldKey={[fieldKey, 'allOption']} valuePropName='checked'>
<Switch />
</Form.Item>
)
);
}}
</Form.Item>
</Col>
{/* <Form.Item {...restField} name={[name, 'selected']} fieldKey={[fieldKey, 'selected']} hidden>
<Input />
</Form.Item> */}
<Col span={4}>
<Button type='link' size='small' onClick={() => move(name, name + 1)} disabled={name === fields.length - 1}>
<ArrowDownOutlined />
</Button>
<Button type='link' size='small' onClick={() => move(name, name - 1)} disabled={name === 0}>
<ArrowUpOutlined />
</Button>
<Button
type='link'
size='small'
onClick={() => {
const v = form.getFieldValue(['var', name]);
add({ ...v, name: 'copy_of_' + v.name });
}}
>
<CopyOutlined />
</Button>
<Button type='link' size='small' onClick={() => remove(name)}>
<DeleteOutlined />
</Button>
</Col>
</Row>
))}
<Form.Item>
<Button type='dashed' onClick={() => add()} block icon={<PlusOutlined />}>
{t('新增变量')}
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form>
</Modal>
);
}