ahooks#useThrottleFn TypeScript Examples
The following examples show how to use
ahooks#useThrottleFn.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: index.tsx From scorpio-h5-design with MIT License | 5 votes |
export default function Code() {
const { selectComponent, pageSchema, setStateByObjectKeys } = useModel('bridge');
const jsonError = useThrottleFn(()=>{
message.error('json格式错误');
}, {
wait: 3000,
});
const options = {
selectOnLineNumbers: true,
strike: true,
};
function editorDidMount(editor:any, monaco:any) {
editor.focus();
}
function onChange(newValue:string, e:any) {
try {
const jsonValue = parseJson(newValue);
pageSchema[0].components[0].generatorSchema = jsonValue;
setStateByObjectKeys({
pageSchema: [...pageSchema],
});
} catch (error) {
// message.error('json格式错误');
console.log('error: ', error);
jsonError.run();
}
}
const code = JSON.stringify(selectComponent.generatorSchema, null, 2);
console.log('code: ', code);
return (
<div className="code-editor">
<LoadableComponent
language="json"
theme="vs-light"
value={code}
options={options}
onChange={onChange}
editorDidMount={editorDidMount}
/>
</div>
);
}
Example #2
Source File: Title.tsx From fe-v5 with Apache License 2.0 | 4 votes |
export default function Title(props: IProps) {
const { curCluster, clusters, setCurCluster, dashboard, setDashboard, refresh, range, setRange, step, setStep, refreshFlag, setRefreshFlag, refreshRef, onAddPanel } = props;
const { id, name } = dashboard;
const history = useHistory();
const [titleEditing, setTitleEditing] = useState(false);
const titleRef = useRef<any>(null);
const handleModifyTitle = async (newName) => {
updateDashboard(id, { ...dashboard, name: newName }).then(() => {
setDashboard({ ...dashboard, name: newName });
setTitleEditing(false);
});
};
const { run } = useThrottleFn(
() => {
if ('start' in range && range.start && range.end) {
const diff = range.end - range.start;
const now = moment().unix();
setRange({
end: now,
start: now - diff,
});
} else if ('unit' in range && range.unit) {
const newRefreshFlag = _.uniqueId('refreshFlag_');
setRange({
...range,
refreshFlag: newRefreshFlag,
});
setRefreshFlag(newRefreshFlag);
}
refresh(false);
},
{ wait: 1000 },
);
return (
<div className='dashboard-detail-header'>
<div className='dashboard-detail-header-left'>
<RollbackOutlined className='back' onClick={() => history.push('/dashboards')} />
{titleEditing ? (
<Input
ref={titleRef}
defaultValue={name}
onPressEnter={(e: any) => {
handleModifyTitle(e.target.value);
}}
/>
) : (
<div className='title'>{dashboard.name}</div>
)}
{!titleEditing ? (
<EditOutlined
className='edit'
onClick={() => {
setTitleEditing(!titleEditing);
}}
/>
) : (
<>
<Button size='small' style={{ marginRight: 5, marginLeft: 5 }} onClick={() => setTitleEditing(false)}>
取消
</Button>
<Button
size='small'
type='primary'
onClick={() => {
handleModifyTitle(titleRef.current.state.value);
}}
>
保存
</Button>
</>
)}
</div>
<div className='dashboard-detail-header-right'>
<Space>
<div>
<Dropdown
trigger={['click']}
overlay={
<Menu>
{_.map([{ type: 'row', name: '分组' }, ...visualizations], (item) => {
return (
<Menu.Item
key={item.type}
onClick={() => {
onAddPanel(item.type);
}}
>
{item.name}
</Menu.Item>
);
})}
</Menu>
}
>
<Button type='primary' icon={<AddPanelIcon />}>
添加图表
</Button>
</Dropdown>
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
集群:
<Dropdown
overlay={
<Menu selectedKeys={[curCluster]}>
{clusters.map((cluster) => (
<Menu.Item
key={cluster}
onClick={(_) => {
setCurCluster(cluster);
localStorage.setItem('curCluster', cluster);
refresh();
}}
>
{cluster}
</Menu.Item>
))}
</Menu>
}
>
<Button>
{curCluster} <DownOutlined />
</Button>
</Dropdown>
</div>
<DateRangePicker value={range} onChange={setRange} />
<Resolution onChange={(v) => setStep(v)} initialValue={step} />
<Refresh onRefresh={run} ref={refreshRef} />
</Space>
</div>
</div>
);
}
Example #3
Source File: detail.tsx From fe-v5 with Apache License 2.0 | 4 votes |
export default function DashboardDetail() {
const refreshRef = useRef<{ closeRefresh: Function }>();
const { t } = useTranslation();
const { id, busiId } = useParams<URLParam>();
const [groupForm] = Form.useForm();
const history = useHistory();
const Ref = useRef<any>(null);
const { clusters } = useSelector<CommonRootState, CommonStoreState>((state) => state.common);
const localCluster = localStorage.getItem('curCluster');
const [curCluster, setCurCluster] = useState<string>(localCluster || clusters[0]);
if (!localCluster && clusters.length > 0) {
setCurCluster(clusters[0]);
localStorage.setItem('curCluster', clusters[0]);
}
const [dashboard, setDashboard] = useState<Dashboard>({
create_by: '',
favorite: 0,
id: 0,
name: '',
tags: '',
update_at: 0,
update_by: '',
});
const [step, setStep] = useState<number | null>(null);
const [titleEditing, setTitleEditing] = useState(false);
const [chartGroup, setChartGroup] = useState<Group[]>([]);
const [variableConfig, setVariableConfig] = useState<VariableType>();
const [variableConfigWithOptions, setVariableConfigWithOptions] = useState<VariableType>();
const [dashboardLinks, setDashboardLinks] = useState<ILink[]>();
const [groupModalVisible, setGroupModalVisible] = useState(false);
const [chartModalVisible, setChartModalVisible] = useState(false);
const [chartModalInitValue, setChartModalInitValue] = useState<Chart | null>();
const [range, setRange] = useState<Range>({
start: 0,
end: 0,
});
const [refreshFlag, setRefreshFlag] = useState(_.uniqueId('refreshFlag_'));
const { run } = useThrottleFn(
() => {
if ('start' in range && range.start && range.end) {
const diff = range.end - range.start;
const now = moment().unix();
setRange({
end: now,
start: now - diff,
});
} else if ('unit' in range && range.unit) {
const newRefreshFlag = _.uniqueId('refreshFlag_');
setRange({
...range,
refreshFlag: newRefreshFlag,
});
setRefreshFlag(newRefreshFlag);
}
init(false);
},
{ wait: 1000 },
);
useEffect(() => {
init();
}, []);
const init = (needUpdateVariable = true) => {
getSingleDashboard(busiId, id).then((res) => {
setDashboard(res.dat);
if (res.dat.configs) {
const configs = JSON.parse(res.dat.configs);
setVariableConfig(configs);
setVariableConfigWithOptions(configs);
setDashboardLinks(configs.links);
}
});
getChartGroup(busiId, id).then((res) => {
let arr = res.dat || [];
setChartGroup(
arr
.sort((a, b) => a - b)
.map((item) => {
item.updateTime = Date.now(); // 前端拓展一个更新时间字段,用来主动刷新ChartGroup
return item;
}),
);
});
};
const handleDateChange = (e) => {
setRange(e);
};
const handleEdit = () => {
setTitleEditing(!titleEditing);
};
const handleModifyTitle = async (e) => {
await updateSingleDashboard(busiId, id, { ...dashboard, name: e.target.value });
// await init();
setDashboard({ ...dashboard, name: e.target.value });
setTitleEditing(false);
};
const handleAddChart = (gid: number) => {
groupId = gid;
editor({
visible: true,
variableConfig: variableConfigWithOptions,
cluster: curCluster,
busiId,
groupId,
id,
initialValues: {
type: 'timeseries',
targets: [
{
refId: 'A',
expr: '',
},
],
},
onOK: () => {
handleChartConfigVisibleChange(true);
},
});
// setChartModalVisible(true);
}; //group是为了让detail组件知道当前需要刷新的是哪个chartGroup,item是为了获取待编辑的信息
const handleUpdateChart = (group: Group, item: Chart) => {
groupId = group.id;
setChartModalInitValue(item);
if (semver.valid(item.configs.version)) {
editor({
visible: true,
variableConfig,
cluster: curCluster,
busiId,
groupId,
id,
initialValues: {
...item.configs,
id: item.id,
},
onOK: () => {
handleChartConfigVisibleChange(true);
},
});
} else {
setChartModalVisible(true);
}
};
const handleDelChart = async (group: Group, item: Chart) => {
groupId = group.id;
await removeChart(busiId, item.id as any);
refreshUpdateTimeByChartGroupId();
};
const handleCloneChart = async (group: Group, item: Chart) => {
groupId = group.id;
const configsClone = _.cloneDeep(item.configs);
configsClone.layout = {
w: configsClone.layout.w,
h: configsClone.layout.h,
};
await createChart(busiId, {
configs: JSON.stringify(configsClone),
weight: 0,
group_id: groupId,
});
refreshUpdateTimeByChartGroupId();
};
const handleShareChart = async (group: Group, item: any) => {
const serielData = {
dataProps: {
...item.configs,
targets: _.map(item.configs.targets, (target) => {
const realExpr = variableConfigWithOptions ? replaceExpressionVars(target.expr, variableConfigWithOptions, variableConfigWithOptions.var.length, id) : target.expr;
return {
...target,
expr: realExpr,
};
}),
step,
range,
},
curCluster: localStorage.getItem('curCluster'),
};
SetTmpChartData([
{
configs: JSON.stringify(serielData),
},
]).then((res) => {
const ids = res.dat;
window.open('/chart/' + ids);
});
};
const handleAddOrUpdateChartGroup = async () => {
await groupForm.validateFields();
let obj = groupForm.getFieldsValue();
if (isAddGroup) {
let weightArr = chartGroup.map((item) => item.weight);
let weight = Math.max(...weightArr) + 1;
await createChartGroup(busiId, { ...obj, weight, dashboard_id: Number(id) });
} else {
let group = chartGroup.find((item) => item.id === groupId);
await updateChartGroup(busiId, [{ dashboard_id: Number(id), ...group, ...obj }]);
}
init();
isAddGroup = true;
setGroupModalVisible(false);
};
const handleUpdateChartGroup = (group: Group) => {
groupId = group.id;
isAddGroup = false;
groupForm.setFieldsValue({ name: group.name });
setGroupModalVisible(true);
};
const handleMoveUpChartGroup = async (group: Group) => {
const { weight } = group;
let lessWeightGroup = chartGroup.find((item) => item.weight === weight - 1);
if (!lessWeightGroup) return;
lessWeightGroup.weight = weight;
group.weight = weight - 1;
await updateChartGroup(busiId, [lessWeightGroup, group]);
init();
};
const handleMoveDownChartGroup = async (group: Group) => {
const { weight } = group;
let lessWeightGroup = chartGroup.find((item) => item.weight === weight + 1);
if (!lessWeightGroup) return;
lessWeightGroup.weight = weight;
group.weight = weight + 1;
await updateChartGroup(busiId, [lessWeightGroup, group]);
init();
};
const handleDelChartGroup = async (id: number) => {
await delChartGroup(busiId, id);
message.success(t('删除分组成功'));
init();
setGroupModalVisible(false);
};
const refreshUpdateTimeByChartGroupId = () => {
let groupIndex = chartGroup.findIndex((item) => item.id === groupId);
if (groupIndex < 0) return;
let newChartGroup = [...chartGroup];
newChartGroup[groupIndex].updateTime = Date.now();
setChartGroup(newChartGroup);
};
const handleChartConfigVisibleChange = (b) => {
setChartModalVisible(false);
setChartModalInitValue(null);
b && refreshUpdateTimeByChartGroupId();
};
const handleVariableChange = (value, b, valueWithOptions) => {
let dashboardConfigs: any = {};
try {
if (dashboard.configs) {
dashboardConfigs = JSON.parse(dashboard.configs);
}
} catch (e) {
console.error(e);
}
dashboardConfigs.var = value.var;
b && updateSingleDashboard(busiId, id, { ...dashboard, configs: JSON.stringify(dashboardConfigs) });
setVariableConfig(dashboardConfigs);
valueWithOptions && setVariableConfigWithOptions(valueWithOptions);
};
const stopAutoRefresh = () => {
refreshRef.current?.closeRefresh();
};
const clusterMenu = (
<Menu selectedKeys={[curCluster]}>
{clusters.map((cluster) => (
<Menu.Item
key={cluster}
onClick={(_) => {
setCurCluster(cluster);
localStorage.setItem('curCluster', cluster);
init();
}}
>
{cluster}
</Menu.Item>
))}
</Menu>
);
return (
<PageLayout
customArea={
<div className='dashboard-detail-header'>
<div className='dashboard-detail-header-left'>
<RollbackOutlined className='back' onClick={() => history.push('/dashboards')} />
{titleEditing ? <Input ref={Ref} defaultValue={dashboard.name} onPressEnter={handleModifyTitle} /> : <div className='title'>{dashboard.name}</div>}
{!titleEditing ? (
<EditOutlined className='edit' onClick={handleEdit} />
) : (
<>
<Button size='small' style={{ marginRight: 5, marginLeft: 5 }} onClick={() => setTitleEditing(false)}>
取消
</Button>
<Button
size='small'
type='primary'
onClick={() => {
handleModifyTitle({ target: { value: Ref.current.state.value } });
}}
>
保存
</Button>
</>
)}
</div>
<div className='dashboard-detail-header-right'>
<Space>
<div style={{ display: 'flex', alignItems: 'center' }}>
集群:
<Dropdown overlay={clusterMenu}>
<Button>
{curCluster} <DownOutlined />
</Button>
</Dropdown>
</div>
<DateRangePicker onChange={handleDateChange} />
<Resolution onChange={(v) => setStep(v)} initialValue={step} />
<Refresh onRefresh={run} ref={refreshRef} />
</Space>
</div>
</div>
}
>
<div className='dashboard-detail-content'>
<div className='dashboard-detail-content-header'>
<div className='variable-area'>
<VariableConfig onChange={handleVariableChange} value={variableConfig} cluster={curCluster} range={range} id={id} onOpenFire={stopAutoRefresh} />
</div>
<DashboardLinks
value={dashboardLinks}
onChange={(v) => {
let dashboardConfigs: any = {};
try {
if (dashboard.configs) {
dashboardConfigs = JSON.parse(dashboard.configs);
}
} catch (e) {
console.error(e);
}
dashboardConfigs.links = v;
updateSingleDashboard(busiId, id, {
...dashboard,
configs: JSON.stringify(dashboardConfigs),
});
setDashboardLinks(v);
}}
/>
</div>
<div className='charts'>
{chartGroup.map((item, i) => (
<ChartGroup
id={id}
cluster={curCluster}
busiId={busiId}
key={i}
step={step}
groupInfo={item}
onAddChart={handleAddChart}
onUpdateChart={handleUpdateChart}
onCloneChart={handleCloneChart}
onShareChart={handleShareChart}
onUpdateChartGroup={handleUpdateChartGroup}
onMoveUpChartGroup={handleMoveUpChartGroup}
onMoveDownChartGroup={handleMoveDownChartGroup}
onDelChart={handleDelChart}
onDelChartGroup={handleDelChartGroup}
range={range}
refreshFlag={refreshFlag}
variableConfig={variableConfigWithOptions!}
moveUpEnable={i > 0}
moveDownEnable={i < chartGroup.length - 1}
/>
))}
<Button
block
icon={<PlusOutlined />}
style={{
paddingRight: 0,
}}
onClick={() => {
groupForm.setFieldsValue({ name: '' });
setGroupModalVisible(true);
}}
>
{t('新增图表分组')}
</Button>
</div>
</div>
<Modal
title={isAddGroup ? t('新建分组') : t('更新分组名称')}
visible={groupModalVisible}
onOk={handleAddOrUpdateChartGroup}
onCancel={() => {
setGroupModalVisible(false);
}}
>
<Form {...layout} form={groupForm}>
<Form.Item
label={t('分组名称')}
name='name'
rules={[
{
required: true,
message: t('请输入名称'),
},
]}
>
<Input />
</Form.Item>
</Form>
</Modal>
{chartModalVisible && (
<ChartConfigModal
id={id}
cluster={curCluster}
busiId={busiId}
initialValue={chartModalInitValue}
groupId={groupId}
show={chartModalVisible}
onVisibleChange={handleChartConfigVisibleChange}
variableConfig={variableConfigWithOptions}
/>
)}
</PageLayout>
);
}
Example #4
Source File: HTTPFlowMiniTable.tsx From yakit with GNU Affero General Public License v3.0 | 4 votes |
HTTPFlowMiniTable: React.FC<HTTPFlowMiniTableProp> = (props) => {
const [tableHeight, setTableHeight] = useState(400);
const [response, setResponse] = useState<QueryGeneralResponse<HTTPFlow>>({
Data: [],
Pagination: genDefaultPagination(),
Total: 0
});
const findHTTPFlowById = (Hash: string) => {
return response.Data.filter(i => i.Hash === Hash).shift()
}
const pipeline = useTablePipeline({
components: antd,
}).input({
dataSource: response.Data,
columns: props.simple ? [
{
code: "Hash", name: "状态", render: (i: any) => {
const flow: HTTPFlow | undefined = findHTTPFlowById(i)
return <div style={{overflow: "hidden"}}>
{flow && <Space size={4}>
<div style={{width: 35, textAlign: "right"}}>{flow.Method}</div>
<Tag style={{
width: 30,
textAlign: "left",
paddingLeft: 3, paddingRight: 3,
}} color={StatusCodeToColor(flow.StatusCode)}>{flow.StatusCode}</Tag>
</Space>}
</div>
},
width: 100, lock: true,
},
{
code: "Hash", name: "URL", render: (i: any) => {
const flow: HTTPFlow | undefined = findHTTPFlowById(i)
return <div style={{overflow: "hidden"}}>
{flow && <Space>
<CopyableField
text={flow.Url} tooltip={false} noCopy={true}
/>
</Space>}
</div>
},
width: 700,
},
{
code: "Hash", name: "操作", render: (i: any) => {
return <>
<Space>
<Button
type={"link"} size={"small"}
onClick={() => {
let m = showDrawer({
width: "80%",
content: onExpandHTTPFlow(
findHTTPFlowById(i),
() => m.destroy()
),
})
}}
>详情</Button>
</Space>
</>
},
width: 80, lock: true,
}
] : [
{
code: "Method", name: "Method",
render: (i: any) => <Tag color={"geekblue"}>{i}</Tag>,
width: 80,
},
{
code: "StatusCode",
name: "StatusCode",
render: (i: any) => <Tag color={StatusCodeToColor(i)}>{i}</Tag>,
width: 80
},
{
code: "GetParamsTotal",
name: "Get 参数",
render: (i: any) => i > 0 ? <Tag color={"orange"}>{i}个</Tag> : "-",
width: 60
},
{
code: "PostParamsTotal",
name: "Post 参数",
render: (i: any) => i > 0 ? <Tag color={"orange"}>{i}个</Tag> : "-",
width: 60, lock: true,
},
{
code: "Url", name: "URL", render: (i: any) => <CopyableField
text={i} tooltip={false} noCopy={true}
/>,
width: 450, features: {sortable: true}
},
{
code: "Hash", name: "操作", render: (i: any) => {
return <>
<Space>
{props.onSendToWebFuzzer && <Button
type={"link"} size={"small"}
onClick={() => {
const req = findHTTPFlowById(i);
if (req) {
ipcRenderer.invoke("send-to-tab", {
type: "fuzzer",
data:{
isHttps: req.IsHTTPS,
request: new Buffer(req.Request).toString()
}
})
}
}}
>发送到Fuzzer</Button>}
<Button
type={"link"} size={"small"}
onClick={() => {
let m = showDrawer({
width: "80%",
content: onExpandHTTPFlow(
findHTTPFlowById(i),
() => m.destroy()
)
})
}}
>详情</Button>
</Space>
</>
},
width: props.onSendToWebFuzzer ? 180 : 80, lock: true,
},
],
}).primaryKey("uuid").use(features.columnResize({
minSize: 60,
})).use(features.columnHover()).use(features.tips())
if (!props.simple) {
pipeline.use(
features.sort({
mode: 'single',
highlightColumnWhenActive: true,
}),
)
}
const update = () => {
ipcRenderer.invoke("QueryHTTPFlows", props.filter).then((data: QueryGeneralResponse<HTTPFlow>) => {
// if ((data.Data || []).length > 0 && (response.Data || []).length > 0) {
// if (data.Data[0].Id === response.Data[0].Id) {
// props.onTotal(data.Total)
// return
// }
// }
setResponse(data)
props.onTotal(data.Total)
})
}
const updateThrottle = useThrottleFn(update, {wait: 1000})
useEffect(() => {
updateThrottle.run()
}, [props.filter])
useEffect(() => {
if (props.simple) {
if (!props.autoUpdate) {
return
}
const id = setInterval(() => {
updateThrottle.run()
}, 1000)
return () => {
clearInterval(id)
}
}
}, [props.simple, props.autoUpdate])
return <div style={{width: "100%", height: "100%", overflow: "auto"}}>
<ReactResizeDetector
onResize={(width, height) => {
if (!width || !height) {
return
}
setTableHeight(height)
}}
handleWidth={true} handleHeight={true} refreshMode={"debounce"} refreshRate={50}
/>
<BaseTable
{...pipeline.getProps()} style={{width: "100%", height: tableHeight, overflow: "auto"}}
/>
</div>
}
Example #5
Source File: HTTPFlowTable.tsx From yakit with GNU Affero General Public License v3.0 | 4 votes |
HTTPFlowTable: React.FC<HTTPFlowTableProp> = (props) => {
const [data, setData, getData] = useGetState<HTTPFlow[]>([])
const [params, setParams] = useState<YakQueryHTTPFlowRequest>(
props.params || {SourceType: "mitm"}
)
const [pagination, setPagination] = useState<PaginationSchema>({
Limit: OFFSET_LIMIT,
Order: "desc",
OrderBy: "created_at",
Page: 1
});
// const [autoReload, setAutoReload, getAutoReload] = useGetState(false);
const autoReloadRef = useRef<boolean>(false);
const autoReload = autoReloadRef.current;
const setAutoReload = (b: boolean) => {
autoReloadRef.current = b
};
const getAutoReload = () => autoReloadRef.current;
const [total, setTotal] = useState<number>(0)
const [loading, setLoading] = useState(false)
const [selected, setSelected, getSelected] = useGetState<HTTPFlow>()
const [_lastSelected, setLastSelected, getLastSelected] = useGetState<HTTPFlow>()
const [compareLeft, setCompareLeft] = useState<CompateData>({content: '', language: 'http'})
const [compareRight, setCompareRight] = useState<CompateData>({content: '', language: 'http'})
const [compareState, setCompareState] = useState(0)
const [tableContentHeight, setTableContentHeight, getTableContentHeight] = useGetState<number>(0);
// 用于记录适合
const [_scrollY, setScrollYRaw, getScrollY] = useGetState(0)
const setScrollY = useThrottleFn(setScrollYRaw, {wait: 300}).run
// 如果这个大于等于 0 ,就 Lock 住,否则忽略
const [_trigger, setLockedScroll, getLockedScroll] = useGetState(-1);
const lockScrollTimeout = (size: number, timeout: number) => {
setLockedScroll(size)
setTimeout(() => setLockedScroll(-1), timeout)
}
const tableRef = useRef(null)
const ref = useHotkeys('ctrl+r, enter', e => {
const selected = getSelected()
if (selected) {
ipcRenderer.invoke("send-to-tab", {
type: "fuzzer",
data: {
isHttps: selected?.IsHTTPS,
request: new Buffer(selected.Request).toString()
}
})
}
})
// 使用上下箭头
useHotkeys("up", () => {
setLastSelected(getSelected())
const data = getData();
if (data.length <= 0) {
return
}
if (!getSelected()) {
setSelected(data[0])
return
}
const expected = parseInt(`${parseInt(`${(getSelected()?.Id as number)}`) + 1}`);
// 如果上点的话,应该是选择更新的内容
for (let i = 0; i < data.length; i++) {
let current = parseInt(`${data[i]?.Id}`);
if (current === expected) {
setSelected(data[i])
return
}
}
setSelected(undefined)
})
useHotkeys("down", () => {
setLastSelected(getSelected())
const data = getData();
if (data.length <= 0) {
return
}
if (!getSelected()) {
setSelected(data[0])
return
}
// 如果上点的话,应该是选择更新的内容
for (let i = 0; i < data.length; i++) {
if (data[i]?.Id == (getSelected()?.Id as number) - 1) {
setSelected(data[i])
return
}
}
setSelected(undefined)
})
// 向主页发送对比数据
useEffect(() => {
if (compareLeft.content) {
const params = {info: compareLeft, type: 1}
setCompareState(compareState === 0 ? 1 : 0)
ipcRenderer.invoke("add-data-compare", params)
}
}, [compareLeft])
useEffect(() => {
if (compareRight.content) {
const params = {info: compareRight, type: 2}
setCompareState(compareState === 0 ? 2 : 0)
ipcRenderer.invoke("add-data-compare", params)
}
}, [compareRight])
const update = useMemoizedFn((
page?: number,
limit?: number,
order?: string,
orderBy?: string,
sourceType?: string,
noLoading?: boolean
) => {
const paginationProps = {
Page: page || 1,
Limit: limit || pagination.Limit,
Order: order || "desc",
OrderBy: orderBy || "id"
}
if (!noLoading) {
setLoading(true)
// setAutoReload(false)
}
// yakQueryHTTPFlow({
// SourceType: sourceType, ...params,
// Pagination: {...paginationProps},
// })
ipcRenderer
.invoke("QueryHTTPFlows", {
SourceType: sourceType,
...params,
Pagination: {...paginationProps}
})
.then((rsp: YakQueryHTTPFlowResponse) => {
setData((rsp?.Data || []))
setPagination(rsp.Pagination)
setTotal(rsp.Total)
})
.catch((e: any) => {
failed(`query HTTP Flow failed: ${e}`)
})
.finally(() => setTimeout(() => setLoading(false), 300))
})
const getNewestId = useMemoizedFn(() => {
let max = 0;
(getData() || []).forEach(e => {
const id = parseInt(`${e.Id}`)
if (id >= max) {
max = id
}
})
return max
})
const getOldestId = useMemoizedFn(() => {
if (getData().length <= 0) {
return 0
}
let min = parseInt(`${getData()[0].Id}`);
(getData() || []).forEach(e => {
const id = parseInt(`${e.Id}`)
if (id <= min) {
min = id
}
})
return min
})
// 第一次启动的时候加载一下
useEffect(() => {
update(1)
}, [])
const scrollTableTo = useMemoizedFn((size: number) => {
if (!tableRef || !tableRef.current) return
const table = tableRef.current as unknown as {
scrollTop: (number) => any,
scrollLeft: (number) => any,
}
table.scrollTop(size)
})
const scrollUpdateTop = useDebounceFn(useMemoizedFn(() => {
const paginationProps = {
Page: 1,
Limit: OFFSET_STEP,
Order: "desc",
OrderBy: "id"
}
const offsetId = getNewestId()
console.info("触顶:", offsetId)
// 查询数据
ipcRenderer
.invoke("QueryHTTPFlows", {
SourceType: "mitm",
...params,
AfterId: offsetId, // 用于计算增量的
Pagination: {...paginationProps}
})
.then((rsp: YakQueryHTTPFlowResponse) => {
const offsetDeltaData = (rsp?.Data || [])
if (offsetDeltaData.length <= 0) {
// 没有增量数据
return
}
setLoading(true)
let offsetData = offsetDeltaData.concat(data);
if (offsetData.length > MAX_ROW_COUNT) {
offsetData = offsetData.splice(0, MAX_ROW_COUNT)
}
setData(offsetData);
scrollTableTo((offsetDeltaData.length + 1) * ROW_HEIGHT)
})
.catch((e: any) => {
failed(`query HTTP Flow failed: ${e}`)
})
.finally(() => setTimeout(() => setLoading(false), 200))
}), {wait: 600, leading: true, trailing: false}).run
const scrollUpdateButt = useDebounceFn(useMemoizedFn((tableClientHeight: number) => {
const paginationProps = {
Page: 1,
Limit: OFFSET_STEP,
Order: "desc",
OrderBy: "id"
}
const offsetId = getOldestId();
console.info("触底:", offsetId)
// 查询数据
ipcRenderer
.invoke("QueryHTTPFlows", {
SourceType: "mitm",
...params,
BeforeId: offsetId, // 用于计算增量的
Pagination: {...paginationProps}
})
.then((rsp: YakQueryHTTPFlowResponse) => {
const offsetDeltaData = (rsp?.Data || [])
if (offsetDeltaData.length <= 0) {
// 没有增量数据
return
}
setLoading(true)
const originDataLength = data.length;
let offsetData = data.concat(offsetDeltaData);
let metMax = false
const originOffsetLength = offsetData.length;
if (originOffsetLength > MAX_ROW_COUNT) {
metMax = true
offsetData = offsetData.splice(originOffsetLength - MAX_ROW_COUNT, MAX_ROW_COUNT)
}
setData(offsetData);
setTimeout(() => {
if (!metMax) {
// 没有丢结果的裁剪问题
scrollTableTo((originDataLength + 1) * ROW_HEIGHT - tableClientHeight)
} else {
// 丢了结果之后的裁剪计算
const a = originOffsetLength - offsetDeltaData.length;
scrollTableTo((originDataLength + 1 + MAX_ROW_COUNT - originOffsetLength) * ROW_HEIGHT - tableClientHeight)
}
}, 50)
})
.catch((e: any) => {
failed(`query HTTP Flow failed: ${e}`)
}).finally(() => setTimeout(() => setLoading(false), 60))
}), {wait: 600, leading: true, trailing: false}).run
const sortFilter = useMemoizedFn((column: string, type: any) => {
const keyRelation: any = {
UpdatedAt: "updated_at",
BodyLength: "body_length",
StatusCode: "status_code"
}
if (column && type) {
update(1, OFFSET_LIMIT, type, keyRelation[column])
} else {
update(1, OFFSET_LIMIT)
}
})
// 这是用来设置选中坐标的,不需要做防抖
useEffect(() => {
if (!getLastSelected() || !getSelected()) {
return
}
const lastSelected = getLastSelected() as HTTPFlow;
const up = parseInt(`${lastSelected?.Id}`) < parseInt(`${selected?.Id}`)
// if (up) {
// console.info("up")
// } else {
// console.info("down")
// }
// console.info(lastSelected.Id, selected?.Id)
const screenRowCount = Math.floor(getTableContentHeight() / ROW_HEIGHT) - 1
if (!autoReload) {
let count = 0;
const data = getData();
for (let i = 0; i < data.length; i++) {
if (data[i].Id != getSelected()?.Id) {
count++
} else {
break
}
}
let minCount = count
if (minCount < 0) {
minCount = 0
}
const viewHeightMin = getScrollY() + tableContentHeight
const viewHeightMax = getScrollY() + tableContentHeight * 2
const minHeight = minCount * ROW_HEIGHT;
const maxHeight = minHeight + tableContentHeight
const maxHeightBottom = minHeight + tableContentHeight + 3 * ROW_HEIGHT
// console.info("top: ", minHeight, "maxHeight: ", maxHeight, "maxHeightBottom: ", maxHeightBottom)
// console.info("viewTop: ", viewHeightMin, "viewButtom: ", viewHeightMax)
if (maxHeight < viewHeightMin) {
// 往下滚动
scrollTableTo(minHeight)
return
}
if (maxHeightBottom > viewHeightMax) {
// 上滚动
const offset = minHeight - (screenRowCount - 2) * ROW_HEIGHT;
// console.info(screenRowCount, minHeight, minHeight - (screenRowCount - 1) * ROW_HEIGHT)
if (offset > 0) {
scrollTableTo(offset)
}
return
}
}
}, [selected])
// 给设置做防抖
useDebounceEffect(() => {
props.onSelected && props.onSelected(selected)
}, [selected], {wait: 400, trailing: true, leading: true})
useEffect(() => {
if (autoReload) {
const id = setInterval(() => {
update(1, undefined, "desc", undefined, undefined, true)
}, 1000)
return () => {
clearInterval(id)
}
}
}, [autoReload])
return (
// <AutoCard bodyStyle={{padding: 0, margin: 0}} bordered={false}>
<div ref={ref as Ref<any>} tabIndex={-1}
style={{width: "100%", height: "100%", overflow: "hidden"}}
>
<ReactResizeDetector
onResize={(width, height) => {
if (!width || !height) {
return
}
setTableContentHeight(height - 38)
}}
handleWidth={true} handleHeight={true} refreshMode={"debounce"} refreshRate={50}/>
{!props.noHeader && (
<PageHeader
title={"HTTP History"}
subTitle={
<Space>
{"所有相关请求都在这里"}
<Button
icon={<ReloadOutlined/>}
type={"link"}
onClick={(e) => {
update(1)
}}
/>
</Space>
}
extra={[
<Space>
<Form.Item label={"选择 HTTP History 类型"} style={{marginBottom: 0}}>
<Select
mode={"multiple"}
value={params.SourceType}
style={{minWidth: 200}}
onChange={(e) => {
setParams({...params, SourceType: e})
setLoading(true)
setTimeout(() => {
update(1, undefined, undefined, undefined, e)
}, 200)
}}
>
<Select.Option value={"mitm"}>mitm: 中间人劫持</Select.Option>
<Select.Option value={"fuzzer"}>
fuzzer: 模糊测试分析
</Select.Option>
</Select>
</Form.Item>
<Popconfirm
title={"确定想要删除所有记录吗?不可恢复"}
onConfirm={(e) => {
ipcRenderer.invoke("delete-http-flows-all")
setLoading(true)
info("正在删除...如自动刷新失败请手动刷新")
setTimeout(() => {
update(1)
if (props.onSelected) props.onSelected(undefined)
}, 400)
}}
>
<Button danger={true}>清除全部历史记录?</Button>
</Popconfirm>
</Space>
]}
/>
)}
<Row style={{margin: "5px 0 5px 5px"}}>
<Col span={12}>
<Space>
<span>HTTP History</span>
<Button
icon={<ReloadOutlined/>}
type={"link"}
size={"small"}
onClick={(e) => {
update(1, undefined, "desc")
}}
/>
{/* <Space>
自动刷新:
<Switch size={"small"} checked={autoReload} onChange={setAutoReload}/>
</Space> */}
<Input.Search
placeholder={"URL关键字"}
enterButton={true}
size={"small"}
style={{width: 170}}
value={params.SearchURL}
onChange={(e) => {
setParams({...params, SearchURL: e.target.value})
}}
onSearch={(v) => {
update(1)
}}
/>
{props.noHeader && (
<Popconfirm
title={"确定想要删除所有记录吗?不可恢复"}
onConfirm={(e) => {
ipcRenderer.invoke("delete-http-flows-all")
setLoading(true)
info("正在删除...如自动刷新失败请手动刷新")
setCompareLeft({content: '', language: 'http'})
setCompareRight({content: '', language: 'http'})
setCompareState(0)
setTimeout(() => {
update(1)
if (props.onSelected) props.onSelected(undefined)
}, 400)
}}
>
<Button danger={true} size={"small"}>
删除历史记录
</Button>
</Popconfirm>
)}
{/*{autoReload && <Tag color={"green"}>自动刷新中...</Tag>}*/}
</Space>
</Col>
<Col span={12} style={{textAlign: "right"}}>
<Tag>{total} Records</Tag>
</Col>
</Row>
<TableResizableColumn
tableRef={tableRef}
virtualized={true}
className={"httpFlowTable"}
loading={loading}
columns={[
{
dataKey: "Id",
width: 80,
headRender: () => "序号",
cellRender: ({rowData, dataKey, ...props}: any) => {
return `${rowData[dataKey] <= 0 ? "..." : rowData[dataKey]}`
}
},
{
dataKey: "Method",
width: 70,
headRender: (params1: any) => {
return (
<div
style={{display: "flex", justifyContent: "space-between"}}
>
方法
<Popover
placement='bottom'
trigger='click'
content={
params &&
setParams && (
<HTTLFlowFilterDropdownForms
label={"搜索方法"}
params={params}
setParams={setParams}
filterName={"Methods"}
autoCompletions={["GET", "POST", "HEAD"]}
submitFilter={() => update(1)}
/>
)
}
>
<Button
style={{
paddingLeft: 4, paddingRight: 4, marginLeft: 4,
color: !!params.Methods ? undefined : "gray",
}}
type={!!params.Methods ? "primary" : "link"} size={"small"}
icon={<SearchOutlined/>}
/>
</Popover>
</div>
)
},
cellRender: ({rowData, dataKey, ...props}: any) => {
// return (
// <Tag color={"geekblue"} style={{marginRight: 20}}>
// {rowData[dataKey]}
// </Tag>
// )
return rowData[dataKey]
}
},
{
dataKey: "StatusCode",
width: 100,
sortable: true,
headRender: () => {
return (
<div
style={{display: "inline-flex"}}
>
状态码
<Popover
placement='bottom'
trigger='click'
content={
params &&
setParams && (
<HTTLFlowFilterDropdownForms
label={"搜索状态码"}
params={params}
setParams={setParams}
filterName={"StatusCode"}
autoCompletions={[
"200",
"300-305",
"400-404",
"500-502",
"200-299",
"300-399",
"400-499"
]}
submitFilter={() => update(1)}
/>
)
}
>
<Button
style={{
paddingLeft: 4, paddingRight: 4, marginLeft: 4,
color: !!params.StatusCode ? undefined : "gray",
}}
type={!!params.StatusCode ? "primary" : "link"} size={"small"}
icon={<SearchOutlined/>}
/>
</Popover>
</div>
)
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return (
<div style={{color: StatusCodeToColor(rowData[dataKey])}}>
{rowData[dataKey] === 0 ? "" : rowData[dataKey]}
</div>
)
}
},
{
dataKey: "Url",
resizable: true,
headRender: () => {
return (
<div
style={{display: "flex", justifyContent: "space-between"}}
>
URL
<Popover
placement='bottom'
trigger='click'
content={
params &&
setParams && (
<HTTLFlowFilterDropdownForms
label={"搜索URL关键字"}
params={params}
setParams={setParams}
filterName={"SearchURL"}
pureString={true}
submitFilter={() => update(1)}
/>
)
}
>
<Button
style={{
paddingLeft: 4, paddingRight: 4, marginLeft: 4,
color: !!params.SearchURL ? undefined : "gray",
}}
type={!!params.SearchURL ? "primary" : "link"} size={"small"}
icon={<SearchOutlined/>}
/>
</Popover>
</div>
)
},
cellRender: ({rowData, dataKey, ...props}: any) => {
if (rowData.IsPlaceholder) {
return <div style={{color: "#888585"}}>{"滚轮上滑刷新..."}</div>
}
return (
<div style={{width: "100%", display: "flex"}}>
<div className='resize-ellipsis' title={rowData.Url}>
{!params.SearchURL ? (
rowData.Url
) : (
rowData.Url
)}
</div>
</div>
)
},
width: 600
},
{
dataKey: "HtmlTitle",
width: 120,
resizable: true,
headRender: () => {
return "Title"
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return rowData[dataKey] ? rowData[dataKey] : ""
}
},
{
dataKey: "Tags",
width: 120,
resizable: true,
headRender: () => {
return "Tags"
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return rowData[dataKey] ? (
`${rowData[dataKey]}`.split("|").filter(i => !i.startsWith("YAKIT_COLOR_")).join(", ")
) : ""
}
},
{
dataKey: "IPAddress",
width: 140, resizable: true,
headRender: () => {
return "IP"
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return rowData[dataKey] ? rowData[dataKey] : ""
}
},
{
dataKey: "BodyLength",
width: 120,
sortable: true,
headRender: () => {
return (
<div style={{display: "inline-block", position: "relative"}}>
响应长度
<Popover
placement='bottom'
trigger='click'
content={
params &&
setParams && (
<HTTLFlowFilterDropdownForms
label={"是否存在Body?"}
params={params}
setParams={setParams}
filterName={"HaveBody"}
pureBool={true}
submitFilter={() => update(1)}
/>
)
}
>
<Button
style={{
paddingLeft: 4, paddingRight: 4, marginLeft: 4,
color: !!params.HaveBody ? undefined : "gray",
}}
type={!!params.HaveBody ? "primary" : "link"} size={"small"}
icon={<SearchOutlined/>}
/>
</Popover>
</div>
)
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return (
<div style={{width: 100}}>
{/* 1M 以上的话,是红色*/}
{rowData.BodyLength !== -1 ?
(<div style={{color: rowData.BodyLength > 1000000 ? "red" : undefined}}>
{rowData.BodySizeVerbose
? rowData.BodySizeVerbose
: rowData.BodyLength}
</div>)
:
(<div></div>)
}
</div>
)
}
},
// {
// dataKey: "UrlLength",
// width: 90,
// headRender: () => {
// return "URL 长度"
// },
// cellRender: ({rowData, dataKey, ...props}: any) => {
// const len = (rowData.Url || "").length
// return len > 0 ? <div>{len}</div> : "-"
// }
// },
{
dataKey: "GetParamsTotal",
width: 65,
align: "center",
headRender: () => {
return (
<div
style={{display: "flex", justifyContent: "space-between"}}
>
参数
<Popover
placement='bottom'
trigger='click'
content={
params &&
setParams && (
<HTTLFlowFilterDropdownForms
label={"过滤是否存在基础参数"}
params={params}
setParams={setParams}
filterName={"HaveCommonParams"}
pureBool={true}
submitFilter={() => update(1)}
/>
)
}
>
<Button
style={{
paddingLeft: 4, paddingRight: 4, marginLeft: 4,
color: !!params.HaveCommonParams ? undefined : "gray",
}}
type={!!params.HaveCommonParams ? "primary" : "link"} size={"small"}
icon={<SearchOutlined/>}
/>
</Popover>
</div>
)
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return (
<Space>
{(rowData.GetParamsTotal > 0 ||
rowData.PostParamsTotal > 0) && <CheckOutlined/>}
</Space>
)
}
},
{
dataKey: "ContentType",
resizable: true, width: 80,
headRender: () => {
return "响应类型"
},
cellRender: ({rowData, dataKey, ...props}: any) => {
let contentTypeFixed = rowData.ContentType.split(";")
.map((el: any) => el.trim())
.filter((i: any) => !i.startsWith("charset"))
.join(",") || "-"
if (contentTypeFixed.includes("/")) {
const contentTypeFixedNew = contentTypeFixed.split("/").pop()
if (!!contentTypeFixedNew) {
contentTypeFixed = contentTypeFixedNew
}
}
return (
<div>
{contentTypeFixed === "null" ? "" : contentTypeFixed}
</div>
)
}
},
{
dataKey: "UpdatedAt",
sortable: true,
width: 110,
headRender: () => {
return "请求时间"
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return <Tooltip
title={rowData[dataKey] === 0 ? "" : formatTimestamp(rowData[dataKey])}
>
{rowData[dataKey] === 0 ? "" : formatTime(rowData[dataKey])}
</Tooltip>
}
},
{
dataKey: "operate",
width: 90,
headRender: () => "操作",
cellRender: ({rowData}: any) => {
if (!rowData.Hash) return <></>
return (
<a
onClick={(e) => {
let m = showDrawer({
width: "80%",
content: onExpandHTTPFlow(
rowData,
() => m.destroy()
)
})
}}
>
详情
</a>
)
}
}
]}
data={autoReload ? data : [TableFirstLinePlaceholder].concat(data)}
autoHeight={tableContentHeight <= 0}
height={tableContentHeight}
sortFilter={sortFilter}
renderRow={(children: ReactNode, rowData: any) => {
if (rowData)
return (
<div
id='http-flow-row'
ref={(node) => {
const color =
rowData.Hash === selected?.Hash ?
"rgba(78, 164, 255, 0.4)" :
rowData.Tags.indexOf("YAKIT_COLOR") > -1 ?
TableRowColor(rowData.Tags.split("|").pop().split('_').pop().toUpperCase()) :
"#ffffff"
if (node) {
if (color) node.style.setProperty("background-color", color, "important")
else node.style.setProperty("background-color", "#ffffff")
}
}}
style={{height: "100%"}}
>
{children}
</div>
)
return children
}}
onRowContextMenu={(rowData: HTTPFlow | any, event: React.MouseEvent) => {
if (rowData) {
setSelected(rowData);
}
showByCursorMenu(
{
content: [
{
title: '发送到 Web Fuzzer',
onClick: () => {
ipcRenderer.invoke("send-to-tab", {
type: "fuzzer",
data: {
isHttps: rowData.IsHTTPS,
request: new Buffer(rowData.Request).toString("utf8")
}
})
}
},
{
title: '发送到 数据包扫描',
onClick: () => {
ipcRenderer
.invoke("GetHTTPFlowByHash", {Hash: rowData.Hash})
.then((i: HTTPFlow) => {
ipcRenderer.invoke("send-to-packet-hack", {
request: i.Request,
ishttps: i.IsHTTPS,
response: i.Response
})
})
.catch((e: any) => {
failed(`Query Response failed: ${e}`)
})
}
},
{
title: '复制 URL',
onClick: () => {
callCopyToClipboard(rowData.Url)
},
},
{
title: '复制为 Yak PoC 模版', onClick: () => {
},
subMenuItems: [
{
title: "数据包 PoC 模版", onClick: () => {
const flow = rowData as HTTPFlow;
if (!flow) return;
generateYakCodeByRequest(flow.IsHTTPS, flow.Request, code => {
callCopyToClipboard(code)
}, RequestToYakCodeTemplate.Ordinary)
}
},
{
title: "批量检测 PoC 模版", onClick: () => {
const flow = rowData as HTTPFlow;
if (!flow) return;
generateYakCodeByRequest(flow.IsHTTPS, flow.Request, code => {
callCopyToClipboard(code)
}, RequestToYakCodeTemplate.Batch)
}
},
]
},
{
title: '标注颜色',
subMenuItems: availableColors.map(i => {
return {
title: i.title,
render: i.render,
onClick: () => {
const flow = rowData as HTTPFlow
if (!flow) {
return
}
const existedTags = flow.Tags ? flow.Tags.split("|").filter(i => !!i && !i.startsWith("YAKIT_COLOR_")) : []
existedTags.push(`YAKIT_COLOR_${i.color.toUpperCase()}`)
ipcRenderer.invoke("SetTagForHTTPFlow", {
Id: flow.Id, Hash: flow.Hash,
Tags: existedTags,
}).then(() => {
info(`设置 HTTPFlow 颜色成功`)
if (!autoReload) {
setData(data.map(item => {
if (item.Hash === flow.Hash) {
item.Tags = `YAKIT_COLOR_${i.color.toUpperCase()}`
return item
}
return item
}))
}
})
}
}
}),
onClick: () => {
}
},
{
title: '移除颜色',
onClick: () => {
const flow = rowData as HTTPFlow
if (!flow) return
const existedTags = flow.Tags ? flow.Tags.split("|").filter(i => !!i && !i.startsWith("YAKIT_COLOR_")) : []
existedTags.pop()
ipcRenderer.invoke("SetTagForHTTPFlow", {
Id: flow.Id, Hash: flow.Hash,
Tags: existedTags,
}).then(() => {
info(`清除 HTTPFlow 颜色成功`)
if (!autoReload) {
setData(data.map(item => {
if (item.Hash === flow.Hash) {
item.Tags = ""
return item
}
return item
}))
}
})
return
},
},
{
title: "发送到对比器", onClick: () => {
},
subMenuItems: [
{
title: '发送到对比器左侧',
onClick: () => {
setCompareLeft({
content: new Buffer(rowData.Request).toString("utf8"),
language: 'http'
})
},
disabled: [false, true, false][compareState]
},
{
title: '发送到对比器右侧',
onClick: () => {
setCompareRight({
content: new Buffer(rowData.Request).toString("utf8"),
language: 'http'
})
},
disabled: [false, false, true][compareState]
}
]
},
]
},
event.clientX,
event.clientY
)
}}
onRowClick={(rowDate: any) => {
if (!rowDate.Hash) return
if (rowDate.Hash !== selected?.Hash) {
setSelected(rowDate)
} else {
// setSelected(undefined)
}
}}
onScroll={(scrollX, scrollY) => {
setScrollY(scrollY)
// 防止无数据触发加载
if (data.length === 0 && !getAutoReload()) {
setAutoReload(true)
return
}
// 根据页面展示内容决定是否自动刷新
let contextHeight = (data.length + 1) * ROW_HEIGHT // +1 是要把表 title 算进去
let offsetY = scrollY + tableContentHeight;
if (contextHeight < tableContentHeight) {
setAutoReload(true)
return
}
setAutoReload(false)
// 向下刷新数据
if (contextHeight <= offsetY) {
setAutoReload(false)
scrollUpdateButt(tableContentHeight)
return
}
// 锁住滚轮
if (getLockedScroll() > 0 && getLockedScroll() >= scrollY) {
if (scrollY === getLockedScroll()) {
return
}
// scrollTableTo(getLockedScroll())
return
}
const toTop = scrollY <= 0;
if (toTop) {
lockScrollTimeout(ROW_HEIGHT, 600)
scrollUpdateTop()
}
}}
/>
</div>
// </AutoCard>
)
}