@ant-design/icons#EllipsisOutlined TypeScript Examples
The following examples show how to use
@ant-design/icons#EllipsisOutlined.
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: WidgetDropdown.tsx From datart with Apache License 2.0 | 6 votes |
WidgetDropdown: React.FC<{ overlay: ReactElement }> = memo(
({ overlay }) => {
return (
<Dropdown
className="widget-tool-dropdown"
overlay={overlay}
placement="bottomCenter"
trigger={['click']}
arrow
>
<Button icon={<EllipsisOutlined />} type="link" />
</Dropdown>
);
},
)
Example #2
Source File: personalizationOptions.tsx From posthog-foss with MIT License | 6 votes |
IS_TECHNICAL: RadioSelectType[] = [
{
key: 'technical',
label: 'Yes',
icon: <MoreOutlined />,
},
{
key: 'non_technical',
label: 'No',
icon: <EllipsisOutlined />,
},
]
Example #3
Source File: general-custom-buttons.editor.tsx From next-basics with GNU General Public License v3.0 | 6 votes |
export function DropdownBtn({
isMoreButton,
}: dropdownBtnProps): React.ReactElement {
return (
<>
<BaseButton>
{isMoreButton ? (
<EllipsisOutlined />
) : (
<>
<SettingOutlined style={{ marginRight: 5 }} /> 管理{" "}
<DownOutlined style={{ marginLeft: 5 }} />
</>
)}
</BaseButton>
</>
);
}
Example #4
Source File: index.tsx From ant-design-pro-V4 with MIT License | 5 votes |
index = () => {
const actionRef = useRef<ActionType>();
return (
<PageContainer>
<Card>
<ProTable<GithubIssueItem>
columns={columns}
actionRef={actionRef}
request={async (params = {}, sort, filter) => {
console.log(sort, filter);
return request<{
data: GithubIssueItem[];
}>('https://proapi.azurewebsites.net/github/issues', {
params,
});
}}
editable={{
type: 'multiple',
}}
columnsState={{
persistenceKey: 'pro-table-singe-demos',
persistenceType: 'localStorage',
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
form={{
// 由于配置了 transform,提交的参与与定义的不同这里需要转化一下
syncToUrl: (values, type) => {
if (type === 'get') {
return {
...values,
created_at: [values.startTime, values.endTime],
};
}
return values;
},
}}
pagination={{
pageSize: 5,
}}
dateFormatter="string"
headerTitle="高级表格"
toolBarRender={() => [
<Button key="button" icon={<PlusOutlined />} type="primary">
新建
</Button>,
<Dropdown key="menu" overlay={menu}>
<Button>
<EllipsisOutlined />
</Button>
</Dropdown>,
]}
/>
</Card>
</PageContainer>
);
}
Example #5
Source File: routeSpec.tsx From yakit with GNU Affero General Public License v3.0 | 5 votes |
RouteMenuData: MenuDataProps[] = [
// {key: Route.MITM, label: "HTTP(S) 中间人劫持", icon: <FireOutlined/>},
{
key: Route.PenTest, label: "手工渗透测试", icon: <AimOutlined/>,
subMenuData: [
{key: Route.HTTPHacker, label: "MITM", icon: <FireOutlined/>},
{key: Route.HTTPFuzzer, label: "Web Fuzzer", icon: <AimOutlined/>},
],
},
{
key: Route.GeneralModule, label: "基础安全工具", icon: <RocketOutlined/>,
subMenuData: [
{key: Route.Mod_ScanPort, label: "扫描端口/指纹", icon: <EllipsisOutlined/>},
{key: Route.Mod_Brute, label: "爆破与未授权", icon: <EllipsisOutlined/>, disabled: false},
// {key: Route.Mod_Subdomain, label: "子域名发现", icon: <EllipsisOutlined/>, disabled: true},
// {key: Route.Mod_Crawler, label: "基础爬虫", icon: <EllipsisOutlined/>, disabled: true},
// {key: Route.Mod_SpaceEngine, label: "空间引擎", icon: <EllipsisOutlined/>, disabled: true},
],
},
{
key: Route.PoC, label: "专项漏洞检测",
icon: <FunctionOutlined/>,
},
{
key: Route.ModManagerDetail, label: "插件管理", icon: <AppstoreOutlined/>,
subMenuData: [
{key: Route.ModManager, label: "插件仓库", icon: <AppstoreOutlined/>},
{key: Route.BatchExecutorPage, label: "插件批量执行", icon: <AppstoreOutlined/>},
]
},
{key: Route.PayloadManager, label: "Payload 管理", icon: <AuditOutlined/>},
{key: Route.YakScript, label: "Yak Runner", icon: <CodeOutlined/>},
{
key: Route.ReverseManager, label: "反连管理", icon: <AppstoreOutlined/>,
subMenuData: [
{key: Route.ShellReceiver, label: "端口监听器", icon: <OneToOneOutlined/>},
{key: Route.ReverseServer, label: "反连服务器", icon: <OneToOneOutlined/>},
{key: Route.DNSLog, label: "DNSLog", icon: <OneToOneOutlined/>},
{key: Route.ICMPSizeLog, label: "ICMP-SizeLog", icon: <OneToOneOutlined/>},
{key: Route.TCPPortLog, label: "TCP-PortLog", icon: <OneToOneOutlined/>},
]
},
{
key: Route.DataHandler, label: "数据处理",
icon: <FunctionOutlined/>,
subMenuData: [
{key: Route.Codec, label: "Codec", icon: <FireOutlined/>},
{key: Route.DataCompare, label: "数据对比", icon: <OneToOneOutlined/>},
],
},
{
key: Route.Database, label: "数据库",
icon: <FunctionOutlined/>,
subMenuData: [
{key: Route.DB_HTTPHistory, label: "HTTP History", icon: <OneToOneOutlined/>},
{key: Route.DB_Ports, label: "端口资产", icon: <OneToOneOutlined/>},
{key: Route.DB_Domain, label: "域名资产", icon: <FireOutlined/>},
{key: Route.DB_ExecResults, label: "插件执行结果", icon: <FireOutlined/>},
{key: Route.DB_Risk, label: "漏洞与风险", icon: <BugOutlined/>},
{key: Route.DB_Report, label: "报告(Beta*)", icon: <FireOutlined/>},
],
},
// {
// key: Route.IGNORE, label: "常用工具包", icon: <FireOutlined/>,
// subMenuData: [
// {key: Route.Codec, label: "编码与解码", icon: <EllipsisOutlined/>},
// {key: Route.ShellReceiver, label: "端口开放助手", icon: <FireOutlined/>},
// ],
// },
]
Example #6
Source File: CommentOptions.tsx From foodie with MIT License | 5 votes |
CommentOptions: React.FC<IProps> = (props) => {
const [isOpen, setIsOpen] = useState(false);
const isOpenRef = useRef(isOpen);
const dispatch = useDispatch();
useEffect(() => {
document.addEventListener('click', handleClickOutside);
return () => {
document.removeEventListener('click', handleClickOutside);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
isOpenRef.current = isOpen;
}, [isOpen]);
const handleClickOutside = (e: Event) => {
const option = (e.target as HTMLDivElement).closest(`#comment_${props.comment.id}`);
if (!option && isOpenRef.current) {
setIsOpen(false);
}
}
const toggleOpen = () => {
setIsOpen(!isOpen);
}
const onClickDelete = () => {
dispatch(setTargetComment(props.comment));
props.openDeleteModal();
}
const onClickEdit = () => {
setIsOpen(false);
props.onClickEdit();
dispatch(setTargetComment(props.comment));
}
return (
<div className="relative z-10" id={`comment_${props.comment.id}`}>
<div
className="p-2 rounded-full flex items-center justify-center cursor-pointer hover:bg-gray-200 dark:text-white dark:hover:bg-indigo-1100"
onClick={toggleOpen}
>
<EllipsisOutlined style={{ fontSize: '20px' }} />
</div>
{isOpen && (
<div className=" w-56 flex flex-col bg-white dark:bg-indigo-1000 rounded-md shadow-lg overflow-hidden absolute top-8 right-3 border border-gray-200 dark:border-gray-800 divide-y divide-gray-100 dark:divide-gray-800">
{props.comment.isOwnComment && (
<h4
className="p-4 flex items-center hover:bg-indigo-700 hover:text-white cursor-pointer dark:text-white"
onClick={onClickEdit}
>
<EditOutlined className="mr-4" />
Edit Comment
</h4>
)}
<h4
className="p-4 flex items-center hover:bg-indigo-700 hover:text-white cursor-pointer dark:text-white"
onClick={onClickDelete}
>
<DeleteOutlined className="mr-4" />
Delete Comment
</h4>
</div>
)}
</div>
);
}
Example #7
Source File: FunnelPropertyCorrelationTable.tsx From posthog-foss with MIT License | 5 votes |
CorrelationActionsCell = ({ record }: { record: FunnelCorrelation }): JSX.Element => {
const { insightProps } = useValues(insightLogic)
const logic = funnelLogic(insightProps)
const { excludePropertyFromProject, setFunnelCorrelationDetails } = useActions(logic)
const { isPropertyExcludedFromProject } = useValues(logic)
const propertyName = (record.event.event || '').split('::')[0]
const [popoverOpen, setPopoverOpen] = useState(false)
return (
<Row style={{ justifyContent: 'flex-end' }}>
<Popup
visible={popoverOpen}
actionable
onClickOutside={() => setPopoverOpen(false)}
overlay={
<>
<LemonButton onClick={() => setFunnelCorrelationDetails(record)} fullWidth type="stealth">
View correlation details
</LemonButton>
<LemonButton
disabled={isPropertyExcludedFromProject(propertyName)}
onClick={() => excludePropertyFromProject(propertyName)}
fullWidth
title="Remove this property from any correlation analysis report in this project."
type="stealth"
>
Exclude property from project
</LemonButton>
</>
}
>
<LemonButton type="stealth" onClick={() => setPopoverOpen(!popoverOpen)}>
<EllipsisOutlined
style={{ color: 'var(--primary)', fontSize: 24 }}
className="insight-dropdown-actions"
/>
</LemonButton>
</Popup>
</Row>
)
}
Example #8
Source File: FunnelCorrelationTable.tsx From posthog-foss with MIT License | 5 votes |
CorrelationActionsCell = ({ record }: { record: FunnelCorrelation }): JSX.Element => {
const { insightProps } = useValues(insightLogic)
const logic = funnelLogic(insightProps)
const { excludeEventPropertyFromProject, excludeEventFromProject, setFunnelCorrelationDetails } = useActions(logic)
const { isEventPropertyExcluded, isEventExcluded } = useValues(logic)
const components = record.event.event.split('::')
const [popoverOpen, setPopoverOpen] = useState(false)
return (
<Row style={{ justifyContent: 'flex-end' }}>
<Popup
visible={popoverOpen}
actionable
onClickOutside={() => setPopoverOpen(false)}
overlay={
<>
{record.result_type === FunnelCorrelationResultsType.Events && (
<LemonButton onClick={() => setFunnelCorrelationDetails(record)} fullWidth type="stealth">
View correlation details
</LemonButton>
)}
<LemonButton
disabled={
record.result_type === FunnelCorrelationResultsType.EventWithProperties
? isEventPropertyExcluded(components[1])
: isEventExcluded(components[0])
}
onClick={() =>
record.result_type === FunnelCorrelationResultsType.EventWithProperties
? excludeEventPropertyFromProject(components[0], components[1])
: excludeEventFromProject(components[0])
}
fullWidth
title="Remove this event from any correlation analysis report in this project."
type="stealth"
>
Exclude event from project
</LemonButton>
</>
}
>
<LemonButton type="stealth" onClick={() => setPopoverOpen(!popoverOpen)}>
<EllipsisOutlined
style={{ color: 'var(--primary)', fontSize: 24 }}
className="insight-dropdown-actions"
/>
</LemonButton>
</Popup>
</Row>
)
}
Example #9
Source File: index.tsx From ql with MIT License | 4 votes |
Crontab = () => {
const columns = [
{
title: '任务名',
dataIndex: 'name',
key: 'name',
align: 'center' as const,
render: (text: string, record: any) => (
<span>{record.name || record._id}</span>
),
},
{
title: '任务',
dataIndex: 'command',
key: 'command',
width: '40%',
align: 'center' as const,
render: (text: string, record: any) => {
return (
<span
style={{
textAlign: 'left',
width: '100%',
display: 'inline-block',
wordBreak: 'break-all',
}}
>
{text}
</span>
);
},
},
{
title: '任务定时',
dataIndex: 'schedule',
key: 'schedule',
align: 'center' as const,
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
align: 'center' as const,
render: (text: string, record: any) => (
<>
{(!record.isDisabled || record.status !== CrontabStatus.idle) && (
<>
{record.status === CrontabStatus.idle && (
<Tag icon={<ClockCircleOutlined />} color="default">
空闲中
</Tag>
)}
{record.status === CrontabStatus.running && (
<Tag
icon={<Loading3QuartersOutlined spin />}
color="processing"
>
运行中
</Tag>
)}
{record.status === CrontabStatus.queued && (
<Tag icon={<FieldTimeOutlined />} color="default">
队列中
</Tag>
)}
</>
)}
{record.isDisabled === 1 && record.status === CrontabStatus.idle && (
<Tag icon={<CloseCircleOutlined />} color="error">
已禁用
</Tag>
)}
</>
),
},
{
title: '操作',
key: 'action',
align: 'center' as const,
render: (text: string, record: any, index: number) => (
<Space size="middle">
{record.status === CrontabStatus.idle && (
<Tooltip title="运行">
<a
onClick={() => {
runCron(record, index);
}}
>
<PlayCircleOutlined />
</a>
</Tooltip>
)}
{record.status !== CrontabStatus.idle && (
<Tooltip title="停止">
<a
onClick={() => {
stopCron(record, index);
}}
>
<PauseCircleOutlined />
</a>
</Tooltip>
)}
<Tooltip title="日志">
<a
onClick={() => {
setLogCron({ ...record, timestamp: Date.now() });
}}
>
<FileTextOutlined />
</a>
</Tooltip>
<MoreBtn key="more" record={record} index={index} />
</Space>
),
},
];
const [width, setWidth] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-72);
const [value, setValue] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false);
const [editedCron, setEditedCron] = useState();
const [searchText, setSearchText] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logCron, setLogCron] = useState<any>();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const getCrons = () => {
setLoading(true);
request
.get(`${config.apiPrefix}crons?searchValue=${searchText}`)
.then((data: any) => {
setValue(
data.data.sort((a: any, b: any) => {
const sortA = a.isDisabled ? 4 : a.status;
const sortB = b.isDisabled ? 4 : b.status;
return CrontabSort[sortA] - CrontabSort[sortB];
}),
);
})
.finally(() => setLoading(false));
};
const addCron = () => {
setEditedCron(null as any);
setIsModalVisible(true);
};
const editCron = (record: any, index: number) => {
setEditedCron(record);
setIsModalVisible(true);
};
const delCron = (record: any, index: number) => {
Modal.confirm({
title: '确认删除',
content: (
<>
确认删除定时任务{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
吗
</>
),
onOk() {
request
.delete(`${config.apiPrefix}crons`, { data: [record._id] })
.then((data: any) => {
if (data.code === 200) {
message.success('删除成功');
const result = [...value];
result.splice(index + pageSize * (currentPage - 1), 1);
setValue(result);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const runCron = (record: any, index: number) => {
Modal.confirm({
title: '确认运行',
content: (
<>
确认运行定时任务{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
吗
</>
),
onOk() {
request
.put(`${config.apiPrefix}crons/run`, { data: [record._id] })
.then((data: any) => {
if (data.code === 200) {
const result = [...value];
result.splice(index + pageSize * (currentPage - 1), 1, {
...record,
status: CrontabStatus.running,
});
setValue(result);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const stopCron = (record: any, index: number) => {
Modal.confirm({
title: '确认停止',
content: (
<>
确认停止定时任务{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
吗
</>
),
onOk() {
request
.put(`${config.apiPrefix}crons/stop`, { data: [record._id] })
.then((data: any) => {
if (data.code === 200) {
const result = [...value];
result.splice(index + pageSize * (currentPage - 1), 1, {
...record,
pid: null,
status: CrontabStatus.idle,
});
setValue(result);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const enabledOrDisabledCron = (record: any, index: number) => {
Modal.confirm({
title: `确认${record.isDisabled === 1 ? '启用' : '禁用'}`,
content: (
<>
确认{record.isDisabled === 1 ? '启用' : '禁用'}
定时任务{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
吗
</>
),
onOk() {
request
.put(
`${config.apiPrefix}crons/${
record.isDisabled === 1 ? 'enable' : 'disable'
}`,
{
data: [record._id],
},
)
.then((data: any) => {
if (data.code === 200) {
const newStatus = record.isDisabled === 1 ? 0 : 1;
const result = [...value];
result.splice(index + pageSize * (currentPage - 1), 1, {
...record,
isDisabled: newStatus,
});
setValue(result);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const MoreBtn: React.FC<{
record: any;
index: number;
}> = ({ record, index }) => (
<Dropdown
arrow
trigger={['click']}
overlay={
<Menu onClick={({ key }) => action(key, record, index)}>
<Menu.Item key="edit" icon={<EditOutlined />}>
编辑
</Menu.Item>
<Menu.Item
key="enableordisable"
icon={
record.isDisabled === 1 ? (
<CheckCircleOutlined />
) : (
<StopOutlined />
)
}
>
{record.isDisabled === 1 ? '启用' : '禁用'}
</Menu.Item>
{record.isSystem !== 1 && (
<Menu.Item key="delete" icon={<DeleteOutlined />}>
删除
</Menu.Item>
)}
</Menu>
}
>
<a>
<EllipsisOutlined />
</a>
</Dropdown>
);
const action = (key: string | number, record: any, index: number) => {
switch (key) {
case 'edit':
editCron(record, index);
break;
case 'enableordisable':
enabledOrDisabledCron(record, index);
break;
case 'delete':
delCron(record, index);
break;
default:
break;
}
};
const handleCancel = (cron?: any) => {
setIsModalVisible(false);
if (cron) {
handleCrons(cron);
}
};
const onSearch = (value: string) => {
setSearchText(value);
};
const handleCrons = (cron: any) => {
const index = value.findIndex((x) => x._id === cron._id);
const result = [...value];
if (index === -1) {
result.push(cron);
} else {
result.splice(index, 1, {
...cron,
});
}
setValue(result);
};
const getCronDetail = (cron: any) => {
request
.get(`${config.apiPrefix}crons/${cron._id}`)
.then((data: any) => {
console.log(value);
const index = value.findIndex((x) => x._id === cron._id);
console.log(index);
const result = [...value];
result.splice(index, 1, {
...cron,
...data.data,
});
setValue(result);
})
.finally(() => setLoading(false));
};
const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds);
};
const rowSelection = {
selectedRowIds,
onChange: onSelectChange,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE,
],
};
const delCrons = () => {
Modal.confirm({
title: '确认删除',
content: <>确认删除选中的定时任务吗</>,
onOk() {
request
.delete(`${config.apiPrefix}crons`, { data: selectedRowIds })
.then((data: any) => {
if (data.code === 200) {
message.success('批量删除成功');
setSelectedRowIds([]);
getCrons();
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const operateCrons = (operationStatus: number) => {
Modal.confirm({
title: `确认${OperationName[operationStatus]}`,
content: <>确认{OperationName[operationStatus]}选中的定时任务吗</>,
onOk() {
request
.put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, {
data: selectedRowIds,
})
.then((data: any) => {
if (data.code === 200) {
getCrons();
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const onPageChange = (page: number, pageSize: number | undefined) => {
setCurrentPage(page);
setPageSize(pageSize as number);
localStorage.setItem('pageSize', pageSize + '');
};
useEffect(() => {
if (logCron) {
localStorage.setItem('logCron', logCron._id);
setIsLogModalVisible(true);
}
}, [logCron]);
useEffect(() => {
getCrons();
}, [searchText]);
useEffect(() => {
if (document.body.clientWidth < 768) {
setWidth('auto');
setMarginLeft(0);
setMarginTop(0);
} else {
setWidth('100%');
setMarginLeft(0);
setMarginTop(-72);
}
setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
}, []);
return (
<PageContainer
className="ql-container-wrapper crontab-wrapper"
title="定时任务"
extra={[
<Search
placeholder="请输入名称或者关键词"
style={{ width: 'auto' }}
enterButton
loading={loading}
onSearch={onSearch}
/>,
<Button key="2" type="primary" onClick={() => addCron()}>
添加定时
</Button>,
]}
header={{
style: {
padding: '4px 16px 4px 15px',
position: 'sticky',
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
},
}}
>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button type="primary" style={{ marginBottom: 5 }} onClick={delCrons}>
批量删除
</Button>
<Button
type="primary"
onClick={() => operateCrons(0)}
style={{ marginLeft: 8, marginBottom: 5 }}
>
批量启用
</Button>
<Button
type="primary"
onClick={() => operateCrons(1)}
style={{ marginLeft: 8, marginRight: 8 }}
>
批量禁用
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => operateCrons(2)}
>
批量运行
</Button>
<Button type="primary" onClick={() => operateCrons(3)}>
批量停止
</Button>
<span style={{ marginLeft: 8 }}>
已选择
<a>{selectedRowIds?.length}</a>项
</span>
</div>
)}
<Table
columns={columns}
pagination={{
hideOnSinglePage: true,
current: currentPage,
onChange: onPageChange,
pageSize: pageSize,
showSizeChanger: true,
defaultPageSize: 20,
showTotal: (total: number, range: number[]) =>
`第 ${range[0]}-${range[1]} 条/总共 ${total} 条`,
}}
dataSource={value}
rowKey="_id"
size="middle"
scroll={{ x: 768 }}
loading={loading}
rowSelection={rowSelection}
/>
<CronLogModal
visible={isLogModalVisible}
handleCancel={() => {
getCronDetail(logCron);
setIsLogModalVisible(false);
}}
cron={logCron}
/>
<CronModal
visible={isModalVisible}
handleCancel={handleCancel}
cron={editedCron}
/>
</PageContainer>
);
}
Example #10
Source File: general-custom-buttons.editor.spec.tsx From next-basics with GNU General Public License v3.0 | 4 votes |
describe("GeneralCustomButtonsEditor", () => {
it("should work if no default", () => {
mockUseBuilderNode.mockReturnValueOnce({
type: "brick",
id: "B-001",
brick: "general-custom-buttons",
alias: "custom-button",
$$parsedProperties: {},
});
const wrapper = shallow(<GeneralCustomButtonsEditor nodeUid={1} />);
const custonButton = wrapper.find(BaseButton).render();
expect(custonButton.text()).toEqual("custom-button");
});
it("should render button group", () => {
mockUseBuilderNode.mockReturnValueOnce({
type: "brick",
id: "B-001",
brick: "general-custom-buttons",
alias: "custom-button",
$$parsedProperties: {
alignment: "end",
isMoreButton: false,
customButtons: [
{
buttonType: "primary",
text: "新建",
icon: {
icon: "plus-circle",
lib: "antd",
theme: "outlined",
},
},
{
icon: "save",
text: "保存",
},
{
icon: "search",
text: "搜索",
isDropdown: true,
},
],
},
});
const wrapper = shallow(<GeneralCustomButtonsEditor nodeUid={1} />);
expect(wrapper.find(BaseButton).length).toEqual(2);
expect(wrapper.find(BaseButton).at(0).render().text()).toEqual(
expect.stringContaining("新建")
);
expect(wrapper.find(".customContainer").prop("style")).toEqual({
justifyContent: "end",
});
});
it("should render dropdown button", () => {
mockUseBuilderNode.mockReturnValueOnce({
type: "brick",
id: "B-001",
brick: "general-custom-buttons",
alias: "custom-button",
$$parsedProperties: {
isMoreButton: false,
customButtons: [
{
buttonType: "primary",
text: "新建",
icon: {
icon: "plus-circle",
lib: "antd",
theme: "outlined",
},
},
{
icon: "save",
text: "保存",
isDropdown: true,
},
{
icon: "search",
text: "搜索",
isDropdown: true,
},
],
},
});
const wrapper = shallow(<GeneralCustomButtonsEditor nodeUid={1} />);
const dropDownWrapper = wrapper.find(DropdownBtn).shallow();
expect(dropDownWrapper.find(BaseButton).render().text()).toEqual(
expect.stringContaining("管理")
);
});
it("should render dropdown button with more button", () => {
mockUseBuilderNode.mockReturnValueOnce({
type: "brick",
id: "B-001",
brick: "general-custom-buttons",
alias: "custom-button",
$$parsedProperties: {
isMoreButton: true,
customButtons: [
{
buttonType: "primary",
text: "新建",
icon: {
icon: "plus-circle",
lib: "antd",
theme: "outlined",
},
},
{
icon: "save",
text: "保存",
isDropdown: true,
},
{
icon: "search",
text: "搜索",
isDropdown: true,
},
],
},
});
const wrapper = shallow(<GeneralCustomButtonsEditor nodeUid={1} />);
const dropDownWrapper = wrapper.find(DropdownBtn).shallow();
expect(dropDownWrapper.find(EllipsisOutlined).length).toEqual(1);
});
it("should work when customButtons is evaluate placeholder", () => {
mockUseBuilderNode.mockReturnValueOnce({
type: "brick",
id: "B-001",
brick: "general-custom-buttons",
alias: "custom-button",
$$parsedProperties: {
customButtons: "<% [{}] %>",
},
});
const wrapper = shallow(<GeneralCustomButtonsEditor nodeUid={1} />);
expect(wrapper.find(BaseButton).prop("children")).toEqual("<% … %>");
});
});
Example #11
Source File: Icon.tsx From html2sketch with MIT License | 4 votes |
IconSymbol: FC = () => {
return (
<Row>
{/*<CaretUpOutlined*/}
{/* className="icon"*/}
{/* symbolName={'1.General/2.Icons/1.CaretUpOutlined'}*/}
{/*/>*/}
{/* className="icon"*/}
{/* symbolName={'1.General/2.Icons/2.MailOutlined'}*/}
{/*/>*/}
{/*<StepBackwardOutlined*/}
{/* className="icon"*/}
{/* symbolName={'1.General/2.Icons/2.StepBackwardOutlined'}*/}
{/*/>*/}
{/*<StepForwardOutlined*/}
{/* className="icon"*/}
{/* symbolName={'1.General/2.Icons/2.StepBackwardOutlined'}*/}
{/*/>*/}
<StepForwardOutlined />
<ShrinkOutlined />
<ArrowsAltOutlined />
<DownOutlined />
<UpOutlined />
<LeftOutlined />
<RightOutlined />
<CaretUpOutlined />
<CaretDownOutlined />
<CaretLeftOutlined />
<CaretRightOutlined />
<VerticalAlignTopOutlined />
<RollbackOutlined />
<FastBackwardOutlined />
<FastForwardOutlined />
<DoubleRightOutlined />
<DoubleLeftOutlined />
<VerticalLeftOutlined />
<VerticalRightOutlined />
<VerticalAlignMiddleOutlined />
<VerticalAlignBottomOutlined />
<ForwardOutlined />
<BackwardOutlined />
<EnterOutlined />
<RetweetOutlined />
<SwapOutlined />
<SwapLeftOutlined />
<SwapRightOutlined />
<ArrowUpOutlined />
<ArrowDownOutlined />
<ArrowLeftOutlined />
<ArrowRightOutlined />
<LoginOutlined />
<LogoutOutlined />
<MenuFoldOutlined />
<MenuUnfoldOutlined />
<BorderBottomOutlined />
<BorderHorizontalOutlined />
<BorderInnerOutlined />
<BorderOuterOutlined />
<BorderLeftOutlined />
<BorderRightOutlined />
<BorderTopOutlined />
<BorderVerticleOutlined />
<PicCenterOutlined />
<PicLeftOutlined />
<PicRightOutlined />
<RadiusBottomleftOutlined />
<RadiusBottomrightOutlined />
<RadiusUpleftOutlined />
<RadiusUprightOutlined />
<FullscreenOutlined />
<FullscreenExitOutlined />
<QuestionOutlined />
<PauseOutlined />
<MinusOutlined />
<PauseCircleOutlined />
<InfoOutlined />
<CloseOutlined />
<ExclamationOutlined />
<CheckOutlined />
<WarningOutlined />
<IssuesCloseOutlined />
<StopOutlined />
<EditOutlined />
<CopyOutlined />
<ScissorOutlined />
<DeleteOutlined />
<SnippetsOutlined />
<DiffOutlined />
<HighlightOutlined />
<AlignCenterOutlined />
<AlignLeftOutlined />
<AlignRightOutlined />
<BgColorsOutlined />
<BoldOutlined />
<ItalicOutlined />
<UnderlineOutlined />
<StrikethroughOutlined />
<RedoOutlined />
<UndoOutlined />
<ZoomInOutlined />
<ZoomOutOutlined />
<FontColorsOutlined />
<FontSizeOutlined />
<LineHeightOutlined />
<SortAscendingOutlined />
<SortDescendingOutlined />
<DragOutlined />
<OrderedListOutlined />
<UnorderedListOutlined />
<RadiusSettingOutlined />
<ColumnWidthOutlined />
<ColumnHeightOutlined />
<AreaChartOutlined />
<PieChartOutlined />
<BarChartOutlined />
<DotChartOutlined />
<LineChartOutlined />
<RadarChartOutlined />
<HeatMapOutlined />
<FallOutlined />
<RiseOutlined />
<StockOutlined />
<BoxPlotOutlined />
<FundOutlined />
<SlidersOutlined />
<AndroidOutlined />
<AppleOutlined />
<WindowsOutlined />
<IeOutlined />
<ChromeOutlined />
<GithubOutlined />
<AliwangwangOutlined />
<DingdingOutlined />
<WeiboSquareOutlined />
<WeiboCircleOutlined />
<TaobaoCircleOutlined />
<Html5Outlined />
<WeiboOutlined />
<TwitterOutlined />
<WechatOutlined />
<AlipayCircleOutlined />
<TaobaoOutlined />
<SkypeOutlined />
<FacebookOutlined />
<CodepenOutlined />
<CodeSandboxOutlined />
<AmazonOutlined />
<GoogleOutlined />
<AlipayOutlined />
<AntDesignOutlined />
<AntCloudOutlined />
<ZhihuOutlined />
<SlackOutlined />
<SlackSquareOutlined />
<BehanceSquareOutlined />
<DribbbleOutlined />
<DribbbleSquareOutlined />
<InstagramOutlined />
<YuqueOutlined />
<AlibabaOutlined />
<YahooOutlined />
<RedditOutlined />
<SketchOutlined />
<AccountBookOutlined />
<AlertOutlined />
<ApartmentOutlined />
<ApiOutlined />
<QqOutlined />
<MediumWorkmarkOutlined />
<GitlabOutlined />
<MediumOutlined />
<GooglePlusOutlined />
<AppstoreAddOutlined />
<AppstoreOutlined />
<AudioOutlined />
<AudioMutedOutlined />
<AuditOutlined />
<BankOutlined />
<BarcodeOutlined />
<BarsOutlined />
<BellOutlined />
<BlockOutlined />
<BookOutlined />
<BorderOutlined />
<BranchesOutlined />
<BuildOutlined />
<BulbOutlined />
<CalculatorOutlined />
<CalendarOutlined />
<CameraOutlined />
<CarOutlined />
<CarryOutOutlined />
<CiCircleOutlined />
<CiOutlined />
<CloudOutlined />
<ClearOutlined />
<ClusterOutlined />
<CodeOutlined />
<CoffeeOutlined />
<CompassOutlined />
<CompressOutlined />
<ContactsOutlined />
<ContainerOutlined />
<ControlOutlined />
<CopyrightCircleOutlined />
<CopyrightOutlined />
<CreditCardOutlined />
<CrownOutlined />
<CustomerServiceOutlined />
<DashboardOutlined />
<DatabaseOutlined />
<DeleteColumnOutlined />
<DeleteRowOutlined />
<DisconnectOutlined />
<DislikeOutlined />
<DollarCircleOutlined />
<DollarOutlined />
<DownloadOutlined />
<EllipsisOutlined />
<EnvironmentOutlined />
<EuroCircleOutlined />
<EuroOutlined />
<ExceptionOutlined />
<ExpandAltOutlined />
<ExpandOutlined />
<ExperimentOutlined />
<ExportOutlined />
<EyeOutlined />
<FieldBinaryOutlined />
<FieldNumberOutlined />
<FieldStringOutlined />
<DesktopOutlined />
<DingtalkOutlined />
<FileAddOutlined />
<FileDoneOutlined />
<FileExcelOutlined />
<FileExclamationOutlined />
<FileOutlined />
<FileImageOutlined />
<FileJpgOutlined />
<FileMarkdownOutlined />
<FilePdfOutlined />
<FilePptOutlined />
<FileProtectOutlined />
<FileSearchOutlined />
<FileSyncOutlined />
<FileTextOutlined />
<FileUnknownOutlined />
<FileWordOutlined />
<FilterOutlined />
<FireOutlined />
<FlagOutlined />
<FolderAddOutlined />
<FolderOutlined />
<FolderOpenOutlined />
<ForkOutlined />
<FormatPainterOutlined />
<FrownOutlined />
<FunctionOutlined />
<FunnelPlotOutlined />
<GatewayOutlined />
<GifOutlined />
<GiftOutlined />
<GlobalOutlined />
<GoldOutlined />
<GroupOutlined />
<HddOutlined />
<HeartOutlined />
<HistoryOutlined />
<HomeOutlined />
<HourglassOutlined />
<IdcardOutlined />
<ImportOutlined />
<InboxOutlined />
<InsertRowAboveOutlined />
<InsertRowBelowOutlined />
<InsertRowLeftOutlined />
<InsertRowRightOutlined />
<InsuranceOutlined />
<InteractionOutlined />
<KeyOutlined />
<LaptopOutlined />
<LayoutOutlined />
<LikeOutlined />
<LineOutlined />
<LinkOutlined />
<Loading3QuartersOutlined />
<LoadingOutlined />
<LockOutlined />
<MailOutlined />
<ManOutlined />
<MedicineBoxOutlined />
<MehOutlined />
<MenuOutlined />
<MergeCellsOutlined />
<MessageOutlined />
<MobileOutlined />
<MoneyCollectOutlined />
<MonitorOutlined />
<MoreOutlined />
<NodeCollapseOutlined />
<NodeExpandOutlined />
<NodeIndexOutlined />
<NotificationOutlined />
<NumberOutlined />
<PaperClipOutlined />
<PartitionOutlined />
<PayCircleOutlined />
<PercentageOutlined />
<PhoneOutlined />
<PictureOutlined />
<PoundCircleOutlined />
<PoundOutlined />
<PoweroffOutlined />
<PrinterOutlined />
<ProfileOutlined />
<ProjectOutlined />
<PropertySafetyOutlined />
<PullRequestOutlined />
<PushpinOutlined />
<QrcodeOutlined />
<ReadOutlined />
<ReconciliationOutlined />
<RedEnvelopeOutlined />
<ReloadOutlined />
<RestOutlined />
<RobotOutlined />
<RocketOutlined />
<SafetyCertificateOutlined />
<SafetyOutlined />
<ScanOutlined />
<ScheduleOutlined />
<SearchOutlined />
<SecurityScanOutlined />
<SelectOutlined />
<SendOutlined />
<SettingOutlined />
<ShakeOutlined />
<ShareAltOutlined />
<ShopOutlined />
<ShoppingCartOutlined />
<ShoppingOutlined />
<SisternodeOutlined />
<SkinOutlined />
<SmileOutlined />
<SolutionOutlined />
<SoundOutlined />
<SplitCellsOutlined />
<StarOutlined />
<SubnodeOutlined />
<SyncOutlined />
<TableOutlined />
<TabletOutlined />
<TagOutlined />
<TagsOutlined />
<TeamOutlined />
<ThunderboltOutlined />
<ToTopOutlined />
<ToolOutlined />
<TrademarkCircleOutlined />
<TrademarkOutlined />
<TransactionOutlined />
<TrophyOutlined />
<UngroupOutlined />
<UnlockOutlined />
<UploadOutlined />
<UsbOutlined />
<UserAddOutlined />
<UserDeleteOutlined />
<UserOutlined />
<UserSwitchOutlined />
<UsergroupAddOutlined />
<UsergroupDeleteOutlined />
<VideoCameraOutlined />
<WalletOutlined />
<WifiOutlined />
<BorderlessTableOutlined />
<WomanOutlined />
<BehanceOutlined />
<DropboxOutlined />
<DeploymentUnitOutlined />
<UpCircleOutlined />
<DownCircleOutlined />
<LeftCircleOutlined />
<RightCircleOutlined />
<UpSquareOutlined />
<DownSquareOutlined />
<LeftSquareOutlined />
<RightSquareOutlined />
<PlayCircleOutlined />
<QuestionCircleOutlined />
<PlusCircleOutlined />
<PlusSquareOutlined />
<MinusSquareOutlined />
<MinusCircleOutlined />
<InfoCircleOutlined />
<ExclamationCircleOutlined />
<CloseCircleOutlined />
<CloseSquareOutlined />
<CheckCircleOutlined />
<CheckSquareOutlined />
<ClockCircleOutlined />
<FormOutlined />
<DashOutlined />
<SmallDashOutlined />
<YoutubeOutlined />
<CodepenCircleOutlined />
<AliyunOutlined />
<PlusOutlined />
<LinkedinOutlined />
<AimOutlined />
<BugOutlined />
<CloudDownloadOutlined />
<CloudServerOutlined />
<CloudSyncOutlined />
<CloudUploadOutlined />
<CommentOutlined />
<ConsoleSqlOutlined />
<EyeInvisibleOutlined />
<FileGifOutlined />
<DeliveredProcedureOutlined />
<FieldTimeOutlined />
<FileZipOutlined />
<FolderViewOutlined />
<FundProjectionScreenOutlined />
<FundViewOutlined />
<MacCommandOutlined />
<PlaySquareOutlined />
<OneToOneOutlined />
<RotateLeftOutlined />
<RotateRightOutlined />
<SaveOutlined />
<SwitcherOutlined />
<TranslationOutlined />
<VerifiedOutlined />
<VideoCameraAddOutlined />
<WhatsAppOutlined />
{/*</Col>*/}
</Row>
);
}
Example #12
Source File: GeneralCustomButtons.tsx From next-basics with GNU General Public License v3.0 | 4 votes |
render() {
const {
buttons: buttonConfigs,
handleClick,
dropdownPlacement,
isMoreButton,
moreButtonShape,
moreBtnIcon,
moreButtonType,
dropdownBtnIcon,
dropdownBtnText,
alignment,
dropdownBtnType,
} = this.props;
const propsButtons = buttonConfigs.filter((btn) => !btn.hide);
const buttons = propsButtons
.filter((btn) => !btn.isDropdown)
.map((button) => {
const {
icon,
buttonHref,
buttonUrl,
buttonType,
buttonShape,
buttonSize,
urlTarget,
color,
eventName,
text,
tooltip,
disabled,
disabledTooltip,
tooltipPlacement,
danger,
testId,
...restProps
} = button;
const buttonComponent = (
<Button
className={classNames({
[style.iconButton]: buttonType === "icon",
})}
icon={
icon &&
typeof icon === "string" && (
<LegacyIcon type={icon} data-icon={icon} />
)
}
onClick={() => {
handleClick(eventName, button);
}}
style={{ color: disabled ? "" : color }}
type={
AvailableButtonTypeSet.has(buttonType) ? buttonType : undefined
}
shape={buttonShape}
size={buttonSize}
disabled={disabled}
danger={danger}
data-testid={testId}
{...restProps}
>
{icon && typeof icon === "object" && <GeneralIcon icon={icon} />}
{text}
</Button>
);
const child =
buttonUrl || buttonHref ? (
<Link href={buttonHref} to={buttonUrl} target={urlTarget}>
{buttonComponent}
</Link>
) : (
buttonComponent
);
return (
<Tooltip
title={disabled ? disabledTooltip : tooltip}
placement={tooltipPlacement}
key={eventName}
>
{child}
</Tooltip>
);
});
let dropdown: React.ReactNode;
const dropdownButtons = propsButtons.filter((btn) => btn.isDropdown);
if (!isEmpty(dropdownButtons)) {
const menu = (
<Menu onClick={this.handleMenuClick}>
{dropdownButtons.map((button, idx) => {
const {
isDivider,
icon,
text,
buttonUrl,
buttonHref,
urlTarget,
disabled,
disabledTooltip,
tooltip,
tooltipPlacement,
eventName,
color,
danger,
testId,
} = button;
if (isDivider) {
return <Menu.Divider key={idx} />;
}
const wrapIcon = (
<span className={style.dropdownBtnIconContainer}>
{icon &&
(typeof icon === "string" ? (
<LegacyIcon
type={icon}
className={style.menuIcon}
data-icon={icon}
/>
) : (
<GeneralIcon icon={icon} />
))}
{text}
</span>
);
const textNode =
buttonUrl || buttonHref ? (
<Link
href={buttonHref}
to={buttonUrl}
target={urlTarget}
disabled={disabled}
>
{wrapIcon}
</Link>
) : (
wrapIcon
);
const tooltipNode = (
<Tooltip
title={disabled ? disabledTooltip : tooltip}
placement={tooltipPlacement}
>
<div
className={classNames(style.dropdownBtn, {
[style.dropdownBtnNormal]: !disabled,
})}
>
{textNode}
</div>
</Tooltip>
);
return (
<Menu.Item
className={classNames({
[style.disabledMenuItem]: disabled,
[style.dropdownMenuItem]: !disabled,
})}
key={eventName}
style={{ color: disabled ? "" : color }}
disabled={disabled}
danger={disabled ? undefined : danger}
data-button={button}
data-testid={testId}
>
{tooltipNode}
</Menu.Item>
);
})}
</Menu>
);
dropdown = (
<Dropdown
overlay={menu}
trigger={["click"]}
placement={dropdownPlacement ?? "bottomRight"}
>
{isMoreButton ? (
<Button
type={moreButtonType}
icon={
moreBtnIcon &&
typeof moreBtnIcon === "string" && (
<LegacyIcon type={moreBtnIcon} />
)
}
className={classNames(
style.moreButton,
{
[style.noShapeButton]: moreButtonShape === "no",
},
{
[style.circleShapeButton]: moreButtonShape === "circle",
},
{
[style.moreIconButton]: moreButtonShape === "icon",
}
)}
data-testid="dropdown-trigger"
>
{moreBtnIcon && typeof moreBtnIcon === "object" && (
<GeneralIcon icon={moreBtnIcon} />
)}
{!moreBtnIcon && <EllipsisOutlined />}
</Button>
) : dropdownBtnType === "link" ? (
<Button
type="link"
className={style.dropdownBtnContainer}
data-testid="dropdown-trigger"
>
{dropdownBtnText || "管理"}
</Button>
) : (
<Button
className={style.dropdownBtnContainer}
icon={
dropdownBtnIcon && typeof dropdownBtnIcon === "string" ? (
<LegacyIcon type={dropdownBtnIcon} />
) : dropdownBtnIcon &&
typeof dropdownBtnIcon === "object" ? null : (
<SettingOutlined />
)
}
data-testid="dropdown-trigger"
>
{dropdownBtnIcon && typeof dropdownBtnIcon === "object" && (
<GeneralIcon icon={dropdownBtnIcon} />
)}
{dropdownBtnText || "管理"} <DownOutlined />
</Button>
)}
</Dropdown>
);
}
return (
<div
className={style.customButtonsContainer}
style={{ justifyContent: alignment ?? "center" }}
>
{buttons}
{dropdown}
</div>
);
}
Example #13
Source File: Undo.tsx From yugong with MIT License | 4 votes |
Undo: React.FC<Props> = () => {
const timer = useRef<number>()
const { list, currentRecord, isRecordReady } = useSelector((state: RootState) => state.record);
const [, setCurrentIndex] = useState(0);
const { setCurrentRecord, setIsReady } = useDispatch<Dispatch>().record;
const { setRunningTimes } = useDispatch<Dispatch>().runningTimes;
const { updateAppData } = useDispatch<Dispatch>().appData;
const { updatePage } = useDispatch<Dispatch>().pageData;
const { forceUpdateByStateTag } = useDispatch<Dispatch>().controller;
// 同步到下游
const sendMessage = usePostMessage(() => { });
const handleDataBack = useCallback(
(item: RecordItem) => {
setIsReady(false)
if (item?.key) setCurrentRecord(item?.key)
if (item?.runningTimes) setRunningTimes(item?.runningTimes)
const win = (
document.getElementById('wrapiframe') as HTMLIFrameElement
).contentWindow;
if (item?.appData) {
updateAppData(item.appData);
if (win) {
sendMessage({ tag: 'updateAppData', value: item.appData }, win);
}
}
if (item?.pageData) {
updatePage(item.pageData)
if (win) {
sendMessage({ tag: "updatePage", value: item.pageData }, win);
}
}
forceUpdateByStateTag();
window.clearTimeout(timer.current);
timer.current = window.setTimeout(() => setIsReady(true), 3000)
},
[forceUpdateByStateTag, sendMessage, setCurrentRecord, setIsReady, setRunningTimes, updateAppData, updatePage],
)
const handleRecord = useCallback(
(item: RecordItem, index: number) => {
handleDataBack(item)
setCurrentIndex(index)
},
[handleDataBack],
)
const handleUndo = useCallback(
() => {
setCurrentIndex((index: number) => {
if (index === list.length - 1) {
return index
}
const newindex = index + 1;
handleDataBack(list[newindex])
return newindex;
})
},
[handleDataBack, list],
)
const handleRedo = useCallback(
() => {
setCurrentIndex((index: number) => {
const newindex = index - 1;
handleDataBack(list[newindex])
return newindex;
})
},
[handleDataBack, list],
)
// 模拟后退
useKeyDown(
() => {
console.log('撤销');
handleUndo()
},
'z',
'ctrlKey',
);
const getIndexByCurrentRecord = useCallback(
(currentRecord: number) => {
for (let index = 0; index < list.length; index++) {
const item = list[index];
if (item.key === currentRecord) {
return index;
}
}
},
[list],
)
const myCurrentIndex = currentRecord ? (getIndexByCurrentRecord(currentRecord) || 0) : 0;
return (
<>
{
list.length > 0 && isRecordReady ? <>
{myCurrentIndex < list.length - 1 ? <Button className={s.nbdr} icon={<Icon component={UndoIcon} />} onClick={handleUndo} /> : null}
{list.length > 1 ? <Dropdown className={s.nbr} overlay={
<Menu>
{list.map((item, index) => <Menu.Item
className={classNames(s.item, {
[s.current]: item.key === currentRecord
})}
key={item.key}
onClick={() => handleRecord(item, index)}>
{item.key}-{currentRecord}-{item.desc}
</Menu.Item>)}
</Menu>
} ><Button ><EllipsisOutlined /></Button></Dropdown> : null}
{myCurrentIndex > 0 ? <Button className={s.nbdl} icon={<Icon component={RedoIcon} />} onClick={handleRedo} /> : null}
</> : null
}
{isRecordReady ? null : <span className={s.saving}>正在同步历史记录...</span>}
</>
)
}
Example #14
Source File: DashboardHeader.tsx From posthog-foss with MIT License | 4 votes |
export function DashboardHeader(): JSX.Element {
const { dashboard, dashboardMode, lastDashboardModeSource } = useValues(dashboardLogic)
const { addNewDashboard, triggerDashboardUpdate, setDashboardMode, addGraph, saveNewTag, deleteTag } =
useActions(dashboardLogic)
const { dashboardTags } = useValues(dashboardsLogic)
const { nameSortedDashboards, dashboardsLoading, dashboardLoading } = useValues(dashboardsModel)
const { pinDashboard, unpinDashboard, deleteDashboard, duplicateDashboard } = useActions(dashboardsModel)
const { user } = useValues(userLogic)
const [newName, setNewName] = useState(dashboard?.name || null) // Used to update the input immediately, debouncing API calls
const nameInputRef = useRef<Input | null>(null)
const descriptionInputRef = useRef<HTMLInputElement | null>(null)
if (!dashboard) {
return <div />
}
const actionsDefault = (
<>
<Dropdown
trigger={['click']}
overlay={
<Menu>
{dashboard.created_by && (
<>
<Menu.Item disabled>
Created by {dashboard.created_by.first_name || dashboard.created_by.email || '-'} on{' '}
{dayjs(dashboard.created_at).format(
dayjs(dashboard.created_at).year() === dayjs().year()
? 'MMMM Do'
: 'MMMM Do YYYY'
)}
</Menu.Item>
<Menu.Divider />
</>
)}
<Menu.Item
icon={<EditOutlined />}
onClick={() => setDashboardMode(DashboardMode.Edit, DashboardEventSource.MoreDropdown)}
>
Edit mode (E)
</Menu.Item>
<Menu.Item
icon={<FullscreenOutlined />}
onClick={() =>
setDashboardMode(DashboardMode.Fullscreen, DashboardEventSource.MoreDropdown)
}
>
Full screen mode (F)
</Menu.Item>
{dashboard.pinned ? (
<Menu.Item
icon={<PushpinFilled />}
onClick={() => unpinDashboard(dashboard.id, DashboardEventSource.MoreDropdown)}
>
Unpin dashboard
</Menu.Item>
) : (
<Menu.Item
icon={<PushpinOutlined />}
onClick={() => pinDashboard(dashboard.id, DashboardEventSource.MoreDropdown)}
>
Pin dashboard
</Menu.Item>
)}
<Menu.Divider />
<Menu.Item
icon={<CopyOutlined />}
onClick={() => duplicateDashboard({ id: dashboard.id, name: dashboard.name, show: true })}
>
Duplicate dashboard
</Menu.Item>
<Menu.Item
icon={<DeleteOutlined />}
onClick={() => deleteDashboard({ id: dashboard.id, redirect: true })}
danger
>
Delete dashboard
</Menu.Item>
</Menu>
}
placement="bottomRight"
>
<Button type="link" className="btn-lg-2x" data-attr="dashboard-more" icon={<EllipsisOutlined />} />
</Dropdown>
<Button
type="link"
data-attr="dashboard-edit-mode"
icon={<EditOutlined />}
onClick={() => setDashboardMode(DashboardMode.Edit, DashboardEventSource.DashboardHeader)}
/>
<HotkeyButton
onClick={() => addGraph()}
data-attr="dashboard-add-graph-header"
icon={<PlusOutlined />}
hotkey="n"
className="hide-lte-md"
>
New insight
</HotkeyButton>
<HotkeyButton
type="primary"
onClick={() => setDashboardMode(DashboardMode.Sharing, DashboardEventSource.DashboardHeader)}
data-attr="dashboard-share-button"
icon={<ShareAltOutlined />}
hotkey="k"
>
Send or share
</HotkeyButton>
</>
)
const actionsPresentationMode = (
<Button
onClick={() => setDashboardMode(null, DashboardEventSource.DashboardHeader)}
data-attr="dashboard-exit-presentation-mode"
icon={<FullscreenExitOutlined />}
>
Exit full screen mode
</Button>
)
const actionsEditMode = (
<Button
data-attr="dashboard-edit-mode-save"
type="primary"
onClick={() => setDashboardMode(null, DashboardEventSource.DashboardHeader)}
tabIndex={10}
>
Finish editing
</Button>
)
useEffect(() => {
if (dashboardMode === DashboardMode.Edit) {
if (lastDashboardModeSource === DashboardEventSource.AddDescription) {
setTimeout(() => descriptionInputRef.current?.focus(), 10)
} else if (!isMobile()) {
setTimeout(() => nameInputRef.current?.focus(), 10)
}
}
}, [dashboardMode])
return (
<>
<div className={`dashboard-header${dashboardMode === DashboardMode.Fullscreen ? ' full-screen' : ''}`}>
{dashboardMode === DashboardMode.Fullscreen && (
<FullScreen onExit={() => setDashboardMode(null, DashboardEventSource.Browser)} />
)}
<ShareModal
onCancel={() => setDashboardMode(null, DashboardEventSource.Browser)}
visible={dashboardMode === DashboardMode.Sharing}
/>
{dashboardsLoading ? (
<Loading />
) : (
<>
{dashboardMode === DashboardMode.Edit ? (
<Input
placeholder="Dashboard name (e.g. Weekly KPIs)"
value={newName || ''}
size="large"
style={{ maxWidth: 400 }}
onChange={(e) => {
setNewName(e.target.value) // To update the input immediately
triggerDashboardUpdate({ name: e.target.value }) // This is breakpointed (i.e. debounced) to avoid multiple API calls
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setDashboardMode(null, DashboardEventSource.InputEnter)
}
}}
ref={nameInputRef}
tabIndex={0}
/>
) : (
<div className="dashboard-select">
<Select
value={(dashboard?.id || undefined) as number | 'new' | undefined}
onChange={(id) => {
if (id === 'new') {
addNewDashboard()
} else {
router.actions.push(urls.dashboard(id))
eventUsageLogic.actions.reportDashboardDropdownNavigation()
}
}}
bordered={false}
dropdownMatchSelectWidth={false}
>
{nameSortedDashboards.map((dash: DashboardType) => (
<Select.Option key={dash.id} value={dash.id}>
{dash.name || <span style={{ color: 'var(--muted)' }}>Untitled</span>}
{dash.is_shared && (
<Tooltip title="This dashboard is publicly shared">
<ShareAltOutlined style={{ marginLeft: 4, float: 'right' }} />
</Tooltip>
)}
</Select.Option>
))}
<Select.Option value="new">+ New Dashboard</Select.Option>
</Select>
</div>
)}
<div className="dashboard-meta">
{dashboardMode === DashboardMode.Edit
? actionsEditMode
: dashboardMode === DashboardMode.Fullscreen
? actionsPresentationMode
: actionsDefault}
</div>
</>
)}
</div>
{user?.organization?.available_features?.includes(AvailableFeature.DASHBOARD_COLLABORATION) && (
<>
<div className="mb" data-attr="dashboard-tags">
<ObjectTags
tags={dashboard.tags}
onTagSave={saveNewTag}
onTagDelete={deleteTag}
saving={dashboardLoading}
tagsAvailable={dashboardTags.filter((tag) => !dashboard.tags.includes(tag))}
/>
</div>
<Description
item={dashboard}
setItemMode={setDashboardMode}
itemMode={dashboardMode}
triggerItemUpdate={triggerDashboardUpdate}
descriptionInputRef={descriptionInputRef}
/>
</>
)}
</>
)
}
Example #15
Source File: PostOptions.tsx From foodie with MIT License | 4 votes |
PostOptions: React.FC<IProps> = (props) => {
const [isOpenOption, setIsOpenOption] = useState(false);
const isOpenOptionRef = useRef(isOpenOption);
const dispatch = useDispatch();
useEffect(() => {
document.addEventListener('click', handleClickOutside);
return () => {
document.removeEventListener('click', handleClickOutside);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
isOpenOptionRef.current = isOpenOption;
}, [isOpenOption]);
const handleClickOutside = (e: Event) => {
const option = (e.target as HTMLDivElement).closest(`#post_${props.post.id}`);
if (!option && isOpenOptionRef.current) {
setIsOpenOption(false);
}
}
const handleClickDelete = () => {
dispatch(showModal(EModalType.DELETE_POST));
dispatch(setTargetPost(props.post));
}
const handleClickEdit = () => {
dispatch(showModal(EModalType.EDIT_POST));
dispatch(setTargetPost(props.post));
}
return (
<div className="relative z-10" id={`post_${props.post.id}`}>
<div
className="post-option-toggle p-2 rounded-full flex items-center justify-center cursor-pointer hover:bg-gray-200 dark:text-white dark:hover:bg-indigo-1100"
onClick={() => setIsOpenOption(!isOpenOption)}
>
<EllipsisOutlined style={{ fontSize: '20px' }} />
</div>
{isOpenOption && (
<div className="w-60 flex flex-col bg-white dark:bg-indigo-1000 rounded-md shadow-lg overflow-hidden absolute top-8 right-3 border border-gray-200 dark:border-gray-800 divide-y divide-gray-100 dark:divide-gray-800">
{props.post.isOwnPost ? (
<>
<h4
className="p-4 flex items-center hover:bg-indigo-700 hover:text-white cursor-pointer dark:text-white"
onClick={handleClickEdit}
>
<EditOutlined className="mr-4" />
Edit Post
</h4>
<h4
className="p-4 flex items-center hover:bg-indigo-700 hover:text-white cursor-pointer dark:text-white"
onClick={handleClickDelete}
>
<DeleteOutlined className="mr-4" />
Delete Post
</h4>
</>
) : (
<BookmarkButton postID={props.post.id} initBookmarkState={props.post.isBookmarked}>
{({ dispatchBookmark, isBookmarked, isLoading }) => (
<h4
className="group p-4 flex items-center cursor-pointer dark:text-white hover:bg-indigo-500 hover:text-white"
onClick={dispatchBookmark}
>
{isLoading
? <LoadingOutlined className="text-gray-600 text-2xl p-2 dark:text-white group-hover:text-white" />
: isBookmarked ? (
<StarFilled className="text-red-600 group-hover:text-white text-2xl p-2 flex justify-center items-center rounded-full" />
) : (
<StarOutlined className="text-red-600 group-hover:text-white text-2xl p-2 flex justify-center items-center rounded-full" />
)}
<span className={`${isLoading && 'opacity-50'}`}>{isBookmarked ? 'Unbookmark Post' : 'Bookmark Post'} </span>
</h4>
)}
</BookmarkButton>
)}
</div>
)}
</div>
);
}
Example #16
Source File: Stats.tsx From leek with Apache License 2.0 | 4 votes |
function Stats(stats: any) {
return [
{
number: stats.SEEN_TASKS,
text: "Total Tasks",
icon: <UnorderedListOutlined />,
tooltip: "Seen tasks names",
},
{
number: stats.SEEN_WORKERS,
text: "Total Workers",
icon: <RobotFilled />,
tooltip: "The total offline/online and beat workers",
},
{
number: stats.PROCESSED_EVENTS,
text: "Events Processed",
icon: <ThunderboltOutlined />,
tooltip: "The total processed events",
},
{
number: stats.PROCESSED_TASKS,
text: "Tasks Processed",
icon: <SyncOutlined />,
tooltip: "The total processed tasks",
},
{
number: stats.QUEUED,
text: "Tasks Queued",
icon: <EllipsisOutlined />,
tooltip: "The total tasks in the queues",
},
{
number: stats.RETRY,
text: "To Retry",
icon: <RetweetOutlined style={{ color: STATES_COLORS.RETRY }} />,
tooltip: "Tasks that are failed and waiting for retry",
},
{
number: stats.RECEIVED,
text: "Received",
icon: <SendOutlined style={{ color: STATES_COLORS.RECEIVED }} />,
tooltip: "Tasks were received by a worker. but not yet started",
},
{
number: stats.STARTED,
text: "Started",
icon: <LoadingOutlined style={{ color: STATES_COLORS.STARTED }} />,
tooltip:
"Tasks that were started by a worker and still active, set (task_track_started) to True on worker level to report started tasks",
},
{
number: stats.SUCCEEDED,
text: "Succeeded",
icon: <CheckCircleOutlined style={{ color: STATES_COLORS.SUCCEEDED }} />,
tooltip: "Tasks that were succeeded",
},
{
number: stats.RECOVERED,
text: "Recovered",
icon: <IssuesCloseOutlined style={{ color: STATES_COLORS.RECOVERED }} />,
tooltip: "Tasks that were succeeded after retries.",
},
{
number: stats.FAILED,
text: "Failed",
icon: <WarningOutlined style={{ color: STATES_COLORS.FAILED }} />,
tooltip: "Tasks that were failed",
},
{
number: stats.CRITICAL,
text: "Critical",
icon: <CloseCircleOutlined style={{ color: STATES_COLORS.CRITICAL }} />,
tooltip: "Tasks that were failed after max retries.",
},
{
number: stats.REJECTED,
text: "Rejected",
icon: <RollbackOutlined style={{ color: STATES_COLORS.REJECTED }} />,
tooltip:
"Tasks that were rejected by workers and requeued, or moved to a dead letter queue",
},
{
number: stats.REVOKED,
text: "Revoked",
icon: <StopOutlined style={{ color: STATES_COLORS.REVOKED }} />,
tooltip: "Tasks that were revoked by workers, but still in the queue.",
},
];
}
Example #17
Source File: index.tsx From condo with MIT License | 4 votes |
EmployeesPageContent = ({
tableColumns,
filtersToQuery,
filtersApplied,
setFiltersApplied,
searchEmployeeQuery,
sortBy,
canManageEmployee,
}) => {
const intl = useIntl()
const PageTitleMessage = intl.formatMessage({ id: 'pages.condo.employee.PageTitle' })
const SearchPlaceholder = intl.formatMessage({ id: 'filters.FullSearch' })
const EmptyListLabel = intl.formatMessage({ id: 'employee.EmptyList.header' })
const EmptyListMessage = intl.formatMessage({ id: 'employee.EmptyList.title' })
const CreateEmployee = intl.formatMessage({ id: 'AddEmployee' })
const NotImplementedYetMessage = intl.formatMessage({ id: 'NotImplementedYet' })
const AddItemUsingUploadLabel = intl.formatMessage({ id: 'AddItemUsingFileUpload' })
const router = useRouter()
const offsetFromQuery = getPageIndexFromQuery(router.query)
const filtersFromQuery = getFiltersFromQuery<IFilters>(router.query)
const { shouldTableScroll } = useLayoutContext()
const {
fetchMore,
loading,
count: total,
objs: employees,
} = OrganizationEmployee.useObjects({
sortBy,
where: searchEmployeeQuery,
skip: (offsetFromQuery * EMPLOYEE_PAGE_SIZE) - EMPLOYEE_PAGE_SIZE,
first: EMPLOYEE_PAGE_SIZE,
}, {
fetchPolicy: 'network-only',
})
const handleRowAction = useCallback((record) => {
return {
onClick: () => {
router.push(`/employee/${record.id}/`)
},
}
}, [])
const handleTableChange = useCallback(debounce((...tableChangeArguments) => {
const [nextPagination, nextFilters, nextSorter] = tableChangeArguments
const { current, pageSize } = nextPagination
const offset = filtersApplied ? 0 : current * pageSize - pageSize
const sort = sorterToQuery(nextSorter)
const filters = filtersToQuery(nextFilters)
setFiltersApplied(false)
if (!loading) {
fetchMore({
// @ts-ignore
sortBy: sort,
where: filters,
skip: offset,
first: EMPLOYEE_PAGE_SIZE,
}).then(async () => {
await updateQuery(router, { ...filtersFromQuery, ...nextFilters }, sort, offset)
})
}
}, 400), [loading])
const [search, handleSearchChange] = useSearch<IFilters>(loading)
const handleAddEmployee = () => router.push(ADD_EMPLOYEE_ROUTE)
const dropDownMenu = (
<Menu>
<Menu.Item key="1">
<Tooltip title={NotImplementedYetMessage}>
{AddItemUsingUploadLabel}
</Tooltip>
</Menu.Item>
</Menu>
)
return (
<>
<Head>
<title>{PageTitleMessage}</title>
</Head>
<PageWrapper>
<PageHeader title={<Typography.Title style={{ margin: 0 }}>{PageTitleMessage}</Typography.Title>}/>
<TablePageContent>
{
!employees.length && !filtersFromQuery
? <EmptyListView
label={EmptyListLabel}
message={EmptyListMessage}
createRoute={ADD_EMPLOYEE_ROUTE}
createLabel={CreateEmployee}/>
: <Row gutter={[0, 40]} align={'middle'}>
<Col span={24}>
<TableFiltersContainer>
<Row justify={'space-between'} gutter={[0, 40]}>
<Col xs={24} lg={6}>
<Input
placeholder={SearchPlaceholder}
onChange={(e) => {
handleSearchChange(e.target.value)
}}
value={search}
allowClear={true}
/>
</Col>
{
canManageEmployee && (
<Dropdown.Button
overlay={dropDownMenu}
buttonsRender={() => [
<Button
key="left"
type={'sberPrimary'}
style={{ borderRight: '1px solid white' }}
onClick={handleAddEmployee}
>
{CreateEmployee}
</Button>,
<Button
key="right"
type={'sberPrimary'}
style={{ borderLeft: '1px solid white', lineHeight: '150%' }}
icon={<EllipsisOutlined/>}
/>,
]}
/>
)
}
</Row>
</TableFiltersContainer>
</Col>
<Col span={24}>
<Table
scroll={getTableScrollConfig(shouldTableScroll)}
bordered
tableLayout={'fixed'}
loading={loading}
dataSource={employees}
columns={tableColumns}
onRow={handleRowAction}
onChange={handleTableChange}
rowKey={record => record.id}
pagination={{
total,
current: offsetFromQuery,
pageSize: EMPLOYEE_PAGE_SIZE,
position: ['bottomLeft'],
}}
/>
</Col>
</Row>
}
</TablePageContent>
</PageWrapper>
</>
)
}
Example #18
Source File: WidgetActionDropdown.tsx From datart with Apache License 2.0 | 4 votes |
WidgetActionDropdown: React.FC<WidgetActionDropdownProps> = memo(
({ widget }) => {
const { editing: boardEditing } = useContext(BoardContext);
const widgetAction = useWidgetAction();
const dataChart = useContext(WidgetChartContext)!;
const t = useI18NPrefix(`viz.widget.action`);
const menuClick = useCallback(
({ key }) => {
widgetAction(key, widget);
},
[widgetAction, widget],
);
const getAllList = useCallback(() => {
const allWidgetActionList: WidgetActionListItem<widgetActionType>[] = [
{
key: 'refresh',
label: t('refresh'),
icon: <SyncOutlined />,
},
{
key: 'fullScreen',
label: t('fullScreen'),
icon: <FullscreenOutlined />,
},
{
key: 'edit',
label: t('edit'),
icon: <EditOutlined />,
},
{
key: 'delete',
label: t('delete'),
icon: <DeleteOutlined />,
danger: true,
},
{
key: 'info',
label: t('info'),
icon: <InfoOutlined />,
},
{
key: 'lock',
label: t('lock'),
icon: <LockOutlined />,
},
{
key: 'makeLinkage',
label: t('makeLinkage'),
icon: <LinkOutlined />,
divider: true,
},
{
key: 'closeLinkage',
label: t('closeLinkage'),
icon: <CloseCircleOutlined />,
danger: true,
},
{
key: 'makeJump',
label: t('makeJump'),
icon: <BranchesOutlined />,
divider: true,
},
{
key: 'closeJump',
label: t('closeJump'),
icon: <CloseCircleOutlined />,
danger: true,
},
];
return allWidgetActionList;
}, [t]);
const actionList = useMemo(() => {
return (
getWidgetActionList({
allList: getAllList(),
widget,
boardEditing,
chartGraphId: dataChart?.config?.chartGraphId,
}) || []
);
}, [boardEditing, dataChart?.config?.chartGraphId, getAllList, widget]);
const dropdownList = useMemo(() => {
const menuItems = actionList.map(item => {
return (
<React.Fragment key={item.key}>
{item.divider && <Menu.Divider />}
<Menu.Item
danger={item.danger}
icon={item.icon}
disabled={item.disabled}
key={item.key}
>
{item.label}
</Menu.Item>
</React.Fragment>
);
});
return <Menu onClick={menuClick}>{menuItems}</Menu>;
}, [actionList, menuClick]);
if (actionList.length === 0) {
return null;
}
return (
<Dropdown
className="widget-tool-dropdown"
overlay={dropdownList}
placement="bottomCenter"
trigger={['click']}
arrow
>
<Button icon={<EllipsisOutlined />} type="link" />
</Dropdown>
);
},
)
Example #19
Source File: create.tsx From electron-playground with MIT License | 4 votes |
CreateModal = ({ show, onClose }: CreateModalProps) => {
const [showCreate, setShowCreate] = useState<boolean>(show)
const [formData, setFormData] = useState<INewDownloadFile>({
url: '',
path: '',
})
const disabled = useMemo(() => !(formData.url && formData.path), [formData.url, formData.path])
// 获取光标,选中内容
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
event.target.select()
}
// 设置表单值
const handleFormChange = (field: string, data?: string) => {
setFormData({
...formData,
[field]: data,
})
}
// 下载开始
const handleOk = async () => {
if (!/^(http(s?)|ftp|blob):|data:.*;base64/.test(formData.url)) {
message.error('下载地址只支持 http、ftp、base64、blob 协议')
return
}
const item = await newDownloadFile(formData)
if (!item) return
Modal.confirm({
content: (
<p>
已存在<strong>{item.fileName}</strong>文件,确认覆盖?
</p>
),
okText: '确认',
cancelText: '取消',
okButtonProps: {
type: 'default',
},
cancelButtonProps: {
type: 'primary',
},
onOk: () => {
retryDownloadFile(item)
},
})
}
// 关闭新建对话框
const handleCancel = () => {
setShowCreate(false)
onClose?.()
}
// 选择保存位置
const handleChoosePath = async () => {
const newPath = await openFileDialog(formData.path || '')
setFormData({
...formData,
path: newPath,
})
handleFormChange('path', newPath)
}
useEffect(() => {
setShowCreate(show)
return () => {
setFormData({
url: '',
fileName: '',
path: getDownloadPath(),
})
}
}, [show])
return (
<Modal
title='新建下载'
centered
visible={showCreate}
okText='下载'
cancelText='取消'
okButtonProps={{ disabled }}
onOk={handleOk}
onCancel={handleCancel}>
<Form labelCol={{ span: 3 }}>
<Form.Item label='地址:'>
<Input
placeholder='支持 http、ftp、base64、blob 协议'
value={formData?.url}
onChange={e => handleFormChange('url', e.target.value)}
onFocus={handleFocus}
/>
</Form.Item>
<Form.Item label='文件名:'>
<Input
value={formData?.fileName}
onChange={e => handleFormChange('fileName', e.target.value)}
onFocus={handleFocus}
/>
</Form.Item>
<Form.Item label='位置:'>
<Input
readOnly
value={formData?.path}
addonAfter={<EllipsisOutlined onClick={handleChoosePath} />}
onClick={handleChoosePath}
/>
</Form.Item>
</Form>
</Modal>
)
}
Example #20
Source File: MainOperator.tsx From yakit with GNU Affero General Public License v3.0 | 4 votes |
Main: React.FC<MainProp> = (props) => {
const [engineStatus, setEngineStatus] = useState<"ok" | "error">("ok")
const [status, setStatus] = useState<{ addr: string; isTLS: boolean }>()
const [collapsed, setCollapsed] = useState(false)
const [hideMenu, setHideMenu] = useState(false)
const [loading, setLoading] = useState(false)
const [menuItems, setMenuItems] = useState<MenuItemGroup[]>([])
const [routeMenuData, setRouteMenuData] = useState<MenuDataProps[]>(RouteMenuData)
const [notification, setNotification] = useState("")
const [pageCache, setPageCache] = useState<PageCache[]>([
{
verbose: "MITM",
route: Route.HTTPHacker,
singleNode: ContentByRoute(Route.HTTPHacker),
multipleNode: []
}
])
const [currentTabKey, setCurrentTabKey] = useState<string>(Route.HTTPHacker)
// 系统类型
const [system, setSystem] = useState<string>("")
useEffect(() => {
ipcRenderer.invoke('fetch-system-name').then((res) => setSystem(res))
}, [])
// yakit页面关闭是否二次确认提示
const [winCloseFlag, setWinCloseFlag] = useState<boolean>(true)
const [winCloseShow, setWinCloseShow] = useState<boolean>(false)
useEffect(() => {
ipcRenderer
.invoke("get-value", WindowsCloseFlag)
.then((flag: any) => setWinCloseFlag(flag === undefined ? true : flag))
}, [])
// 获取自定义菜单
const updateMenuItems = () => {
setLoading(true)
// Fetch User Defined Plugins
ipcRenderer
.invoke("GetAllMenuItem", {})
.then((data: { Groups: MenuItemGroup[] }) => {
setMenuItems(data.Groups)
})
.catch((e: any) => failed("Update Menu Item Failed"))
.finally(() => setTimeout(() => setLoading(false), 300))
// Fetch Official General Plugins
ipcRenderer
.invoke("QueryYakScript", {
Pagination: genDefaultPagination(1000),
IsGeneralModule: true,
Type: "yak"
} as QueryYakScriptRequest)
.then((data: QueryYakScriptsResponse) => {
const tabList: MenuDataProps[] = cloneDeep(RouteMenuData)
for (let item of tabList) {
if (item.subMenuData) {
if (item.key === Route.GeneralModule) {
const extraMenus: MenuDataProps[] = data.Data.map((i) => {
return {
icon: <EllipsisOutlined/>,
key: `plugin:${i.Id}`,
label: i.ScriptName,
} as unknown as MenuDataProps
})
item.subMenuData.push(...extraMenus)
}
item.subMenuData.sort((a, b) => a.label.localeCompare(b.label))
}
}
setRouteMenuData(tabList)
})
}
useEffect(() => {
updateMenuItems()
ipcRenderer.on("fetch-new-main-menu", (e) => {
updateMenuItems()
})
return () => {
ipcRenderer.removeAllListeners("fetch-new-main-menu")
}
}, [])
useEffect(() => {
if (engineStatus === "error") props.onErrorConfirmed && props.onErrorConfirmed()
}, [engineStatus])
// 整合路由对应名称
const pluginKey = (item: PluginMenuItem) => `plugin:${item.Group}:${item.YakScriptId}`;
const routeKeyToLabel = new Map<string, string>();
routeMenuData.forEach(k => {
(k.subMenuData || []).forEach(subKey => {
routeKeyToLabel.set(`${subKey.key}`, subKey.label)
})
routeKeyToLabel.set(`${k.key}`, k.label)
})
menuItems.forEach((k) => {
k.Items.forEach((value) => {
routeKeyToLabel.set(pluginKey(value), value.Verbose)
})
})
// Tabs Bar Operation Function
const getCacheIndex = (route: string) => {
const targets = pageCache.filter((i) => i.route === route)
return targets.length > 0 ? pageCache.indexOf(targets[0]) : -1
}
const addTabPage = useMemoizedFn(
(route: Route, nodeParams?: { time?: string; node: ReactNode; isRecord?: boolean }) => {
const filterPage = pageCache.filter((i) => i.route === route)
const filterPageLength = filterPage.length
if (singletonRoute.includes(route)) {
if (filterPageLength > 0) {
setCurrentTabKey(route)
} else {
const tabName = routeKeyToLabel.get(route) || `${route}`
setPageCache([
...pageCache,
{
verbose: tabName,
route: route,
singleNode: ContentByRoute(route),
multipleNode: []
}
])
setCurrentTabKey(route)
}
} else {
if (filterPageLength > 0) {
const tabName = routeKeyToLabel.get(route) || `${route}`
const tabId = `${route}-[${randomString(49)}]`
const time = new Date().getTime().toString()
const node: multipleNodeInfo = {
id: tabId,
verbose: `${tabName}-[${filterPage[0].multipleNode.length + 1}]`,
node: nodeParams && nodeParams.node ? nodeParams?.node || <></> : ContentByRoute(route),
time: nodeParams && nodeParams.node ? nodeParams?.time || time : time
}
const pages = pageCache.map((item) => {
if (item.route === route) {
item.multipleNode.push(node)
item.multipleCurrentKey = tabId
return item
}
return item
})
setPageCache([...pages])
setCurrentTabKey(route)
if (nodeParams && !!nodeParams.isRecord) addFuzzerList(nodeParams?.time || time)
} else {
const tabName = routeKeyToLabel.get(route) || `${route}`
const tabId = `${route}-[${randomString(49)}]`
const time = new Date().getTime().toString()
const node: multipleNodeInfo = {
id: tabId,
verbose: `${tabName}-[1]`,
node: nodeParams && nodeParams.node ? nodeParams?.node || <></> : ContentByRoute(route),
time: nodeParams && nodeParams.node ? nodeParams?.time || time : time
}
setPageCache([
...pageCache,
{
verbose: tabName,
route: route,
singleNode: undefined,
multipleNode: [node],
multipleCurrentKey: tabId
}
])
setCurrentTabKey(route)
if (nodeParams && !!nodeParams.isRecord) addFuzzerList(nodeParams?.time || time)
}
}
}
)
const menuAddPage = useMemoizedFn((route: Route) => {
if (route === "ignore") return
if (route === Route.HTTPFuzzer) {
const time = new Date().getTime().toString()
addTabPage(Route.HTTPFuzzer, {
time: time,
node: ContentByRoute(Route.HTTPFuzzer, undefined, {
system: system,
order: time
}),
isRecord: true
})
} else addTabPage(route as Route)
})
const removePage = (route: string) => {
const targetIndex = getCacheIndex(route)
if (targetIndex > 0 && pageCache[targetIndex - 1]) {
const targetCache = pageCache[targetIndex - 1]
setCurrentTabKey(targetCache.route)
}
if (targetIndex === 0 && pageCache[targetIndex + 1]) {
const targetCache = pageCache[targetIndex + 1]
setCurrentTabKey(targetCache.route)
}
if (targetIndex === 0 && pageCache.length === 1) setCurrentTabKey("")
setPageCache(pageCache.filter((i) => i.route !== route))
if (route === Route.HTTPFuzzer) delFuzzerList(1)
}
const updateCacheVerbose = (id: string, verbose: string) => {
const index = getCacheIndex(id)
if (index < 0) return
pageCache[index].verbose = verbose
setPageCache([...pageCache])
}
const setMultipleCurrentKey = useMemoizedFn((key: string, type: Route) => {
const arr = pageCache.map(item => {
if (item.route === type) {
item.multipleCurrentKey = key
return item
}
return item
})
setPageCache([...arr])
})
const removeMultipleNodePage = useMemoizedFn((key: string, type: Route) => {
const removeArr: multipleNodeInfo[] = pageCache.filter(item => item.route === type)[0]?.multipleNode || []
if (removeArr.length === 0) return
const nodes = removeArr.filter(item => item.id === key)
const time = nodes[0].time
let index = 0
for (let i in removeArr) {
if (removeArr[i].id === key) {
index = +i
break
}
}
if (index === 0 && removeArr.length === 1) {
removePage(type)
return
}
let current = ""
let filterArr: multipleNodeInfo[] = []
if (index > 0 && removeArr[index - 1]) {
current = removeArr[index - 1].id
filterArr = removeArr.filter(item => item.id !== key)
}
if (index === 0 && removeArr[index + 1]) {
current = removeArr[index + 1].id
filterArr = removeArr.filter(item => item.id !== key)
}
if (current) {
const arr = pageCache.map(item => {
if (item.route === type) {
item.multipleNode = [...filterArr]
item.multipleCurrentKey = current
return item
}
return item
})
setPageCache([...arr])
if (type === Route.HTTPFuzzer) delFuzzerList(2, time)
}
})
const removeOtherMultipleNodePage = useMemoizedFn((key: string, type: Route) => {
const removeArr: multipleNodeInfo[] = pageCache.filter(item => item.route === type)[0]?.multipleNode || []
if (removeArr.length === 0) return
const nodes = removeArr.filter(item => item.id === key)
const time = nodes[0].time
const arr = pageCache.map(item => {
if (item.route === type) {
item.multipleNode = [...nodes]
item.multipleCurrentKey = key
return item
}
return item
})
setPageCache([...arr])
if (type === Route.HTTPFuzzer) delFuzzerList(3, time)
})
// 全局记录鼠标坐标位置(为右键菜单提供定位)
const coordinateTimer = useRef<any>(null)
useEffect(() => {
document.onmousemove = (e) => {
const {screenX, screenY, clientX, clientY, pageX, pageY} = e
if (coordinateTimer.current) {
clearTimeout(coordinateTimer.current)
coordinateTimer.current = null
}
coordinateTimer.current = setTimeout(() => {
coordinate.screenX = screenX
coordinate.screenY = screenY
coordinate.clientX = clientX
coordinate.clientY = clientY
coordinate.pageX = pageX
coordinate.pageY = pageY
}, 50);
}
}, [])
// 全局注册快捷键功能
const documentKeyDown = useMemoizedFn((e: any) => {
// ctrl + w 关闭tab页面
if (e.code === "KeyW" && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
if (pageCache.length === 0) return
setLoading(true)
removePage(currentTabKey)
setTimeout(() => setLoading(false), 300);
return
}
})
useEffect(() => {
document.onkeydown = documentKeyDown
}, [])
// fuzzer本地缓存
const fuzzerList = useRef<Map<string, fuzzerInfoProp>>(new Map<string, fuzzerInfoProp>())
const saveFuzzerList = debounce(() => {
const historys: fuzzerInfoProp[] = []
fuzzerList.current.forEach((value) => historys.push(value))
historys.sort((a, b) => +a.time - +b.time)
const filters = historys.filter(item => (item.request || "").length < 1000000 && (item.request || "").length > 0)
ipcRenderer.invoke("set-value", FuzzerCache, JSON.stringify(filters.slice(-5)))
}, 500)
const fetchFuzzerList = useMemoizedFn(() => {
setLoading(true)
fuzzerList.current.clear()
ipcRenderer
.invoke("get-value", FuzzerCache)
.then((res: any) => {
const cache = JSON.parse(res || "[]")
for (let item of cache) {
const time = new Date().getTime().toString()
fuzzerList.current.set(time, {...item, time: time})
addTabPage(Route.HTTPFuzzer, {
time: time,
node:
ContentByRoute(
Route.HTTPFuzzer,
undefined,
{
isHttps: item.isHttps || false,
request: item.request || "",
fuzzerParams: item,
system: system,
order: time
}
)
})
}
})
.catch((e) => console.info(e))
.finally(() => setTimeout(() => setLoading(false), 300))
})
const addFuzzerList = (key: string, request?: string, isHttps?: boolean) => {
fuzzerList.current.set(key, {request, isHttps, time: key})
}
const delFuzzerList = (type: number, key?: string) => {
if (type === 1) fuzzerList.current.clear()
if (type === 2 && key) if (fuzzerList.current.has(key)) fuzzerList.current.delete(key)
if (type === 3 && key) {
const info = fuzzerList.current.get(key)
if (info) {
fuzzerList.current.clear()
fuzzerList.current.set(key, info)
}
}
saveFuzzerList()
}
const updateFuzzerList = (key: string, param: fuzzerInfoProp) => {
fuzzerList.current.set(key, param)
saveFuzzerList()
}
useEffect(() => {
ipcRenderer.on("fetch-fuzzer-setting-data", (e, res: any) => updateFuzzerList(res.key, JSON.parse(res.param)))
// 开发环境不展示fuzzer缓存
ipcRenderer.invoke("is-dev").then((flag) => {
if (!flag) fetchFuzzerList()
// fetchFuzzerList()
})
return () => ipcRenderer.removeAllListeners("fetch-fuzzer-setting-data")
}, [])
// 加载补全
useEffect(() => {
ipcRenderer.invoke("GetYakitCompletionRaw").then((data: { RawJson: Uint8Array }) => {
const completionJson = Buffer.from(data.RawJson).toString("utf8")
setCompletions(JSON.parse(completionJson) as CompletionTotal)
// success("加载 Yak 语言自动补全成功 / Load Yak IDE Auto Completion Finished")
})
}, [])
useEffect(() => {
ipcRenderer.invoke("yakit-connect-status").then((data) => {
setStatus(data)
})
ipcRenderer.on("client-engine-status-ok", (e, reason) => {
if (engineStatus !== "ok") setEngineStatus("ok")
})
ipcRenderer.on("client-engine-status-error", (e, reason) => {
if (engineStatus === "ok") setEngineStatus("error")
})
const updateEngineStatus = () => {
ipcRenderer
.invoke("engine-status")
.catch((e: any) => {
setEngineStatus("error")
})
.finally(() => {
})
}
let id = setInterval(updateEngineStatus, 3000)
return () => {
ipcRenderer.removeAllListeners("client-engine-status-error")
ipcRenderer.removeAllListeners("client-engine-status-ok")
clearInterval(id)
}
}, [])
useHotkeys("Ctrl+Alt+T", () => {
})
useEffect(() => {
ipcRenderer.invoke("query-latest-notification").then((e: string) => {
setNotification(e)
if (e) {
success(
<>
<Space direction={"vertical"}>
<span>来自于 yaklang.io 的通知</span>
<Button
type={"link"}
onClick={() => {
showModal({
title: "Notification",
content: (
<>
<MDEditor.Markdown source={e}/>
</>
)
})
}}
>
点击查看
</Button>
</Space>
</>
)
}
})
}, [])
// 新增数据对比页面
useEffect(() => {
ipcRenderer.on("main-container-add-compare", (e, params) => {
const newTabId = `${Route.DataCompare}-[${randomString(49)}]`;
const verboseNameRaw = routeKeyToLabel.get(Route.DataCompare) || `${Route.DataCompare}`;
addTabPage(Route.DataCompare, {node: ContentByRoute(Route.DataCompare, undefined, {system: system})})
// 区分新建对比页面还是别的页面请求对比的情况
ipcRenderer.invoke("created-data-compare")
})
return () => {
ipcRenderer.removeAllListeners("main-container-add-compare")
}
}, [pageCache])
// Global Sending Function(全局发送功能|通过发送新增功能页面)
const addFuzzer = useMemoizedFn((res: any) => {
const {isHttps, request} = res || {}
if (request) {
const time = new Date().getTime().toString()
addTabPage(Route.HTTPFuzzer, {
time: time,
node:
ContentByRoute(
Route.HTTPFuzzer,
undefined,
{
isHttps: isHttps || false,
request: request || "",
system: system,
order: time
}
)
})
addFuzzerList(time, request || "", isHttps || false)
}
})
const addScanPort = useMemoizedFn((res: any) => {
const {URL = ""} = res || {}
if (URL) {
addTabPage(Route.Mod_ScanPort, {
node: ContentByRoute(Route.Mod_ScanPort, undefined, {scanportParams: URL})
})
}
})
const addBrute = useMemoizedFn((res: any) => {
const {URL = ""} = res || {}
if (URL) {
addTabPage(Route.Mod_Brute, {
node: ContentByRoute(Route.Mod_Brute, undefined, {bruteParams: URL})
})
}
})
// 发送到专项漏洞检测modal-show变量
const [bugTestShow, setBugTestShow] = useState<boolean>(false)
const [bugList, setBugList] = useState<BugInfoProps[]>([])
const [bugTestValue, setBugTestValue] = useState<BugInfoProps[]>([])
const [bugUrl, setBugUrl] = useState<string>("")
const addBugTest = useMemoizedFn((type: number, res?: any) => {
const {URL = ""} = res || {}
if (type === 1 && URL) {
setBugUrl(URL)
ipcRenderer.invoke("get-value", CustomBugList)
.then((res: any) => {
setBugList(res ? JSON.parse(res) : [])
setBugTestShow(true)
})
.catch(() => {
})
}
if (type === 2) {
const filter = pageCache.filter(item => item.route === Route.PoC)
if (filter.length === 0) {
addTabPage(Route.PoC)
setTimeout(() => {
ipcRenderer.invoke("send-to-bug-test", {type: bugTestValue, data: bugUrl})
setBugTestValue([])
setBugUrl("")
}, 300);
} else {
ipcRenderer.invoke("send-to-bug-test", {type: bugTestValue, data: bugUrl})
setCurrentTabKey(Route.PoC)
setBugTestValue([])
setBugUrl("")
}
}
})
const addYakRunning = useMemoizedFn((res: any) => {
const {name = "", code = ""} = res || {}
const filter = pageCache.filter(item => item.route === Route.YakScript)
if (!name || !code) return false
if ((filter || []).length === 0) {
addTabPage(Route.YakScript)
setTimeout(() => {
ipcRenderer.invoke("send-to-yak-running", {name, code})
}, 300);
} else {
ipcRenderer.invoke("send-to-yak-running", {name, code})
setCurrentTabKey(Route.YakScript)
}
})
useEffect(() => {
ipcRenderer.on("fetch-send-to-tab", (e, res: any) => {
const {type, data = {}} = res
if (type === "fuzzer") addFuzzer(data)
if (type === "scan-port") addScanPort(data)
if (type === "brute") addBrute(data)
if (type === "bug-test") addBugTest(1, data)
if (type === "plugin-store") addYakRunning(data)
})
return () => {
ipcRenderer.removeAllListeners("fetch-send-to-tab")
}
}, [])
// Tabs Bar 组件
const closeAllCache = useMemoizedFn(() => {
Modal.confirm({
title: "确定要关闭所有 Tabs?",
content: "这样将会关闭所有进行中的进程",
onOk: () => {
delFuzzerList(1)
setPageCache([])
}
})
})
const closeOtherCache = useMemoizedFn((route: string) => {
Modal.confirm({
title: "确定要关闭除此之外所有 Tabs?",
content: "这样将会关闭所有进行中的进程",
onOk: () => {
const arr = pageCache.filter((i) => i.route === route)
setPageCache(arr)
if (route === Route.HTTPFuzzer) delFuzzerList(1)
}
})
})
const bars = (props: any, TabBarDefault: any) => {
return (
<TabBarDefault
{...props}
children={(barNode: React.ReactElement) => {
return (
<DropdownMenu
menu={{
data: [
{key: "all", title: "关闭所有Tabs"},
{key: "other", title: "关闭其他Tabs"}
]
}}
dropdown={{trigger: ["contextMenu"]}}
onClick={(key) => {
switch (key) {
case "all":
closeAllCache()
break
case "other":
closeOtherCache(barNode.key as Route)
break
default:
break
}
}}
>
{barNode}
</DropdownMenu>
)
}}
/>
)
}
return (
<Layout className="yakit-main-layout">
<AutoSpin spinning={loading}>
<Header className="main-laytou-header">
<Row>
<Col span={8}>
<Space>
<div style={{marginLeft: 18, textAlign: "center", height: 60}}>
<Image src={YakLogoBanner} preview={false} width={130} style={{marginTop: 6}}/>
</div>
<Divider type={"vertical"}/>
<YakVersion/>
<YakitVersion/>
{!hideMenu && (
<Button
style={{marginLeft: 4, color: "#207ee8"}}
type={"ghost"}
ghost={true}
onClick={(e) => {
setCollapsed(!collapsed)
}}
icon={collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
/>
)}
<Button
style={{marginLeft: 4, color: "#207ee8"}}
type={"ghost"}
ghost={true}
onClick={(e) => {
updateMenuItems()
}}
icon={<ReloadOutlined/>}
/>
</Space>
</Col>
<Col span={16} style={{textAlign: "right", paddingRight: 28}}>
<PerformanceDisplay/>
<RiskStatsTag professionalMode={true}/>
<Space>
{/* {status?.isTLS ? <Tag color={"green"}>TLS:通信已加密</Tag> : <Tag color={"red"}>通信未加密</Tag>} */}
{status?.addr && <Tag color={"geekblue"}>{status?.addr}</Tag>}
{/* <Tag color={engineStatus === "ok" ? "green" : "red"}>Yak 引擎状态:{engineStatus}</Tag> */}
<ReversePlatformStatus/>
<Dropdown forceRender={true} overlay={<Menu>
<Menu.Item key={"update"}>
<AutoUpdateYakModuleButton/>
</Menu.Item>
<Menu.Item key={"reverse-global"}>
<ConfigGlobalReverseButton/>
</Menu.Item>
</Menu>} trigger={["click"]}>
<Button icon={<SettingOutlined/>}>
配置
</Button>
</Dropdown>
<Button type={"link"} danger={true} icon={<PoweroffOutlined/>} onClick={() => {
if (winCloseFlag) setWinCloseShow(true)
else {
success("退出当前 Yak 服务器成功")
setEngineStatus("error")
}
}}/>
</Space>
</Col>
</Row>
</Header>
<Content
style={{
margin: 12,
backgroundColor: "#fff",
overflow: "auto"
}}
>
<Layout style={{height: "100%", overflow: "hidden"}}>
{!hideMenu && (
<Sider
style={{backgroundColor: "#fff", overflow: "auto"}}
collapsed={collapsed}
>
<Spin spinning={loading}>
<Space
direction={"vertical"}
style={{
width: "100%"
}}
>
<Menu
theme={"light"}
style={{}}
selectedKeys={[]}
mode={"inline"}
onSelect={(e) => {
if (e.key === "ignore") return
const flag = pageCache.filter(item => item.route === (e.key as Route)).length === 0
if (flag) menuAddPage(e.key as Route)
else setCurrentTabKey(e.key)
}}
>
{menuItems.map((i) => {
if (i.Group === "UserDefined") {
i.Group = "社区插件"
}
return (
<Menu.SubMenu icon={<EllipsisOutlined/>} key={i.Group}
title={i.Group}>
{i.Items.map((item) => {
if (item.YakScriptId > 0) {
return (
<MenuItem icon={<EllipsisOutlined/>}
key={`plugin:${item.Group}:${item.YakScriptId}`}>
<Text
ellipsis={{tooltip: true}}>{item.Verbose}</Text>
</MenuItem>
)
}
return (
<MenuItem icon={<EllipsisOutlined/>}
key={`batch:${item.Group}:${item.Verbose}:${item.MenuItemId}`}>
<Text
ellipsis={{tooltip: true}}>{item.Verbose}</Text>
</MenuItem>
)
})}
</Menu.SubMenu>
)
})}
{(routeMenuData || []).map((i) => {
if (i.subMenuData) {
return (
<Menu.SubMenu icon={i.icon} key={i.key} title={i.label}>
{(i.subMenuData || []).map((subMenu) => {
return (
<MenuItem icon={subMenu.icon} key={subMenu.key}
disabled={subMenu.disabled}>
<Text
ellipsis={{tooltip: true}}>{subMenu.label}</Text>
</MenuItem>
)
})}
</Menu.SubMenu>
)
}
return (
<MenuItem icon={i.icon} key={i.key} disabled={i.disabled}>
{i.label}
</MenuItem>
)
})}
</Menu>
</Space>
</Spin>
</Sider>
)}
<Content style={{
overflow: "hidden",
backgroundColor: "#fff",
marginLeft: 12,
height: "100%",
display: "flex",
flexFlow: "column"
}}>
<div style={{
padding: 12,
paddingTop: 8,
overflow: "hidden",
flex: "1",
display: "flex",
flexFlow: "column"
}}>
{pageCache.length > 0 ? (
<Tabs
style={{display: "flex", flex: "1"}}
tabBarStyle={{marginBottom: 8}}
className='main-content-tabs yakit-layout-tabs'
activeKey={currentTabKey}
onChange={setCurrentTabKey}
size={"small"}
type={"editable-card"}
renderTabBar={(props, TabBarDefault) => {
return bars(props, TabBarDefault)
}}
hideAdd={true}
onTabClick={(key, e) => {
const divExisted = document.getElementById("yakit-cursor-menu")
if (divExisted) {
const div: HTMLDivElement = divExisted as HTMLDivElement
const unmountResult = ReactDOM.unmountComponentAtNode(div)
if (unmountResult && div.parentNode) {
div.parentNode.removeChild(div)
}
}
}}
>
{pageCache.map((i) => {
return (
<Tabs.TabPane
forceRender={true}
key={i.route}
tab={i.verbose}
closeIcon={
<Space>
<Popover
trigger={"click"}
title={"修改名称"}
content={
<>
<Input
size={"small"}
defaultValue={i.verbose}
onBlur={(e) => updateCacheVerbose(i.route, e.target.value)}
/>
</>
}
>
<EditOutlined className='main-container-cion'/>
</Popover>
<CloseOutlined
className='main-container-cion'
onClick={() => removePage(i.route)}
/>
</Space>
}
>
<div
style={{
overflowY: NoScrollRoutes.includes(i.route)
? "hidden"
: "auto",
overflowX: "hidden",
height: "100%",
maxHeight: "100%"
}}
>
{i.singleNode ? (
i.singleNode
) : (
<MainTabs
currentTabKey={currentTabKey}
tabType={i.route}
pages={i.multipleNode}
currentKey={i.multipleCurrentKey || ""}
isShowAdd={true}
setCurrentKey={(key, type) => {
setMultipleCurrentKey(key, type as Route)
}}
removePage={(key, type) => {
removeMultipleNodePage(key, type as Route)
}}
removeOtherPage={(key, type) => {
removeOtherMultipleNodePage(key, type as Route)
}}
onAddTab={() => menuAddPage(i.route)}
></MainTabs>
)}
</div>
</Tabs.TabPane>
)
})}
</Tabs>
) : (
<></>
)}
</div>
</Content>
</Layout>
</Content>
</AutoSpin>
<Modal
visible={winCloseShow}
onCancel={() => setWinCloseShow(false)}
footer={[
<Button key='link' onClick={() => setWinCloseShow(false)}>
取消
</Button>,
<Button key='back' type='primary' onClick={() => {
success("退出当前 Yak 服务器成功")
setEngineStatus("error")
}}>
退出
</Button>
]}
>
<div style={{height: 40}}>
<ExclamationCircleOutlined style={{fontSize: 22, color: "#faad14"}}/>
<span style={{fontSize: 18, marginLeft: 15}}>提示</span>
</div>
<p style={{fontSize: 15, marginLeft: 37}}>是否要退出yakit操作界面,一旦退出,界面内打开内容除fuzzer页外都会销毁</p>
<div style={{marginLeft: 37}}>
<Checkbox
defaultChecked={!winCloseFlag}
value={!winCloseFlag}
onChange={() => {
setWinCloseFlag(!winCloseFlag)
ipcRenderer.invoke("set-value", WindowsCloseFlag, false)
}}
></Checkbox>
<span style={{marginLeft: 8}}>不再出现该提示信息</span>
</div>
</Modal>
<Modal
visible={bugTestShow}
onCancel={() => setBugTestShow(false)}
footer={[
<Button key='link' onClick={() => setBugTestShow(false)}>
取消
</Button>,
<Button key='back' type='primary' onClick={() => {
if ((bugTestValue || []).length === 0) return failed("请选择类型后再次提交")
addBugTest(2)
setBugTestShow(false)
}}>
确定
</Button>
]}
>
<ItemSelects
item={{
label: "专项漏洞类型",
style: {marginTop: 20}
}}
select={{
allowClear: true,
data: BugList.concat(bugList) || [],
optText: "title",
optValue: "key",
value: (bugTestValue || [])[0]?.key,
onChange: (value, option: any) => setBugTestValue(value ? [{
key: option?.key,
title: option?.title
}] : [])
}}
></ItemSelects>
</Modal>
</Layout>
)
}
Example #21
Source File: YakExecutor.tsx From yakit with GNU Affero General Public License v3.0 | 4 votes |
YakExecutor: React.FC<YakExecutorProp> = (props) => {
const [codePath, setCodePath] = useState<string>("")
const [loading, setLoading] = useState<boolean>(false)
const [fileList, setFileList] = useState<tabCodeProps[]>([])
const [tabList, setTabList] = useState<tabCodeProps[]>([])
const [activeTab, setActiveTab] = useState<string>("")
const [unTitleCount, setUnTitleCount] = useState(1)
const [hintShow, setHintShow] = useState<boolean>(false)
const [hintFile, setHintFile] = useState<string>("")
const [hintIndex, setHintIndex] = useState<number>(0)
const [renameHint, setRenameHint] = useState<boolean>(false)
const [renameIndex, setRenameIndex] = useState<number>(-1)
const [renameFlag, setRenameFlag] = useState<boolean>(false)
const [renameCache, setRenameCache] = useState<string>("")
const [fullScreen, setFullScreen] = useState<boolean>(false)
const [errors, setErrors] = useState<string[]>([])
const [executing, setExecuting] = useState(false)
const [outputEncoding, setOutputEncoding] = useState<"utf8" | "latin1">("utf8")
const xtermAsideRef = useRef(null)
const xtermRef = useRef(null)
const timer = useRef<any>(null)
const [extraParams, setExtraParams] = useState("")
// trigger for updating
const [triggerForUpdatingHistory, setTriggerForUpdatingHistory] = useState<any>(0)
const addFileTab = useMemoizedFn((res: any) => {
const {name, code} = res
const tab: tabCodeProps = {
tab: `${name}.yak`,
code: code,
suffix: "yak",
isFile: false
}
setActiveTab(`${tabList.length}`)
setTabList(tabList.concat([tab]))
setUnTitleCount(unTitleCount + 1)
})
useEffect(() => {
ipcRenderer.on("fetch-send-to-yak-running", (e, res: any) => addFileTab(res))
return () => ipcRenderer.removeAllListeners("fetch-send-to-yak-running")
}, [])
// 自动保存
const autoSave = useMemoizedFn(() => {
for (let tabInfo of tabList) {
if (tabInfo.isFile) {
ipcRenderer.invoke("write-file", {
route: tabInfo.route,
data: tabInfo.code
})
}
}
})
// 保存近期文件内的15个
const saveFiliList = useMemoizedFn(() => {
let files = cloneDeep(fileList).reverse()
files.splice(14)
files = files.reverse()
ipcRenderer.invoke("set-value", RecentFileList, files)
})
// 获取和保存近期打开文件信息,同时展示打开默认内容
useEffect(() => {
let time: any = null
let timer: any = null
setLoading(true)
ipcRenderer
.invoke("get-value", RecentFileList)
.then((value: any) => {
if ((value || []).length !== 0) {
setFileList(value)
} else {
const tab: tabCodeProps = {
tab: `Untitle-${unTitleCount}.yak`,
code: "# input your yak code\nprintln(`Hello Yak World!`)",
suffix: "yak",
isFile: false
}
setActiveTab(`${tabList.length}`)
setTabList([tab])
setUnTitleCount(unTitleCount + 1)
}
})
.catch(() => {})
.finally(() => {
setTimeout(() => setLoading(false), 300)
time = setInterval(() => {
autoSave()
}, 2000)
timer = setInterval(() => {
saveFiliList()
}, 5000)
})
return () => {
saveFiliList()
if (time) clearInterval(time)
if (timer) clearInterval(timer)
}
}, [])
// 全局监听重命名事件是否被打断
useEffect(() => {
document.onmousedown = (e) => {
// @ts-ignore
if (e.path[0].id !== "rename-input" && renameFlag) {
renameCode(renameIndex)
setRenameFlag(false)
}
}
}, [renameFlag])
// 打开文件
const addFile = useMemoizedFn((file: any) => {
const isExists = fileList.filter((item) => item.tab === file.name && item.route === file.path).length === 1
if (isExists) {
for (let index in tabList) {
const item = tabList[index]
if (item.tab === file.name && item.route === file.path) {
setActiveTab(`${index}`)
return false
}
}
}
ipcRenderer
.invoke("fetch-file-content", file.path)
.then((res) => {
const tab: tabCodeProps = {
tab: file.name,
code: res,
suffix: file.name.split(".").pop() === "yak" ? "yak" : "http",
isFile: true,
route: file.path,
extraParams: file.extraParams
}
setActiveTab(`${tabList.length}`)
if (!isExists) setFileList(fileList.concat([tab]))
setTabList(tabList.concat([tab]))
})
.catch(() => {
failed("无法获取该文件内容,请检查后后重试!")
const files = cloneDeep(fileList)
for (let i in files) if (files[i].route === file.path) files.splice(i, 1)
setFileList(files)
})
return false
})
// 新建文件
const newFile = useMemoizedFn(() => {
const tab: tabCodeProps = {
tab: `Untitle-${unTitleCount}.yak`,
code: "# input your yak code\nprintln(`Hello Yak World!`)",
suffix: "yak",
isFile: false
}
setActiveTab(`${tabList.length}`)
setTabList(tabList.concat([tab]))
setUnTitleCount(unTitleCount + 1)
})
//修改文件
const modifyCode = useMemoizedFn((value: string, index: number) => {
const tabs = cloneDeep(tabList)
tabs[index].code = value
setTabList(tabs)
})
// 保存文件
const saveCode = useMemoizedFn((info: tabCodeProps, index: number) => {
if (info.isFile) {
ipcRenderer.invoke("write-file", {
route: info.route,
data: info.code
})
} else {
ipcRenderer.invoke("show-save-dialog", `${codePath}${codePath ? '/' : ''}${info.tab}`).then((res) => {
if (res.canceled) return
const path = res.filePath
const name = res.name
ipcRenderer
.invoke("write-file", {
route: res.filePath,
data: info.code
})
.then(() => {
const suffix = name.split(".").pop()
var tabs = cloneDeep(tabList)
var active = null
tabs = tabs.filter((item) => item.route !== path)
tabs = tabs.map((item, index) => {
if (!item.route && item.tab === info.tab) {
active = index
item.tab = name
item.isFile = true
item.suffix = suffix === "yak" ? suffix : "http"
item.route = path
return item
}
return item
})
if (active !== null) setActiveTab(`${active}`)
setTabList(tabs)
const file: tabCodeProps = {
tab: name,
code: info.code,
isFile: true,
suffix: suffix === "yak" ? suffix : "http",
route: res.filePath,
extraParams: info.extraParams
}
for (let item of fileList) {
if (item.route === file.route) {
return
}
}
setFileList(fileList.concat([file]))
})
})
}
})
//关闭文件
const closeCode = useMemoizedFn((index, isFileList: boolean) => {
const tabInfo = isFileList ? fileList[+index] : tabList[+index]
if (isFileList) {
for (let i in tabList) {
if (tabList[i].tab === tabInfo.tab && tabList[i].route === tabInfo.route) {
const tabs = cloneDeep(tabList)
tabs.splice(i, 1)
setTabList(tabs)
setActiveTab(tabs.length >= 1 ? `0` : "")
}
}
const files = cloneDeep(fileList)
files.splice(+index, 1)
setFileList(files)
} else {
setActiveTab(index)
if (!tabInfo.isFile) {
setHintFile(tabInfo.tab)
setHintIndex(index)
setHintShow(true)
} else {
const tabs = cloneDeep(tabList)
tabs.splice(+index, 1)
setTabList(tabs)
setActiveTab(tabs.length >= 1 ? `0` : "")
}
}
})
// 关闭虚拟文件不保存
const ownCloseCode = useMemoizedFn(() => {
const tabs = cloneDeep(tabList)
tabs.splice(hintIndex, 1)
setTabList(tabs)
setHintShow(false)
setActiveTab(tabs.length >= 1 ? `0` : "")
})
// 删除文件
const delCode = useMemoizedFn((index) => {
const fileInfo = fileList[index]
ipcRenderer
.invoke("delelte-code-file", fileInfo.route)
.then(() => {
for (let i in tabList) {
if (tabList[i].tab === fileInfo.tab && tabList[i].route === fileInfo.route) {
const tabs = cloneDeep(tabList)
tabs.splice(i, 1)
setTabList(tabs)
setActiveTab(tabs.length >= 1 ? `0` : "")
}
}
const arr = cloneDeep(fileList)
arr.splice(index === undefined ? hintIndex : index, 1)
setFileList(arr)
})
.catch(() => {
failed("文件删除失败!")
})
})
//重命名操作
const renameCode = useMemoizedFn((index: number) => {
const tabInfo = fileList[index]
if (renameCache === tabInfo.tab) return
if (!renameCache) return
if (!tabInfo.route) return
const flagStr = tabInfo.route?.indexOf("/") > -1 ? "/" : "\\"
const routes = tabInfo.route?.split(flagStr)
routes?.pop()
ipcRenderer
.invoke("is-exists-file", routes?.concat([renameCache]).join(flagStr))
.then(() => {
const newRoute = routes?.concat([renameCache]).join(flagStr)
if (!tabInfo.route || !newRoute) return
renameFile(index, renameCache, tabInfo.route, newRoute)
})
.catch(() => {
setRenameHint(true)
})
})
// 重命名文件
const renameFile = useMemoizedFn(
(index: number, rename: string, oldRoute: string, newRoute: string, callback?: () => void) => {
ipcRenderer.invoke("rename-file", {old: oldRoute, new: newRoute}).then(() => {
const suffix = rename.split(".").pop()
var files = cloneDeep(fileList)
var tabs = cloneDeep(tabList)
var active = null
files = files.filter((item) => item.route !== newRoute)
tabs = tabs.filter((item) => item.route !== newRoute)
files = files.map((item) => {
if (item.route === oldRoute) {
item.tab = rename
item.suffix = suffix === "yak" ? suffix : "http"
item.route = newRoute
return item
}
return item
})
tabs = tabs.map((item, index) => {
if (item.route === oldRoute) {
active = index
item.tab = rename
item.suffix = suffix === "yak" ? suffix : "http"
item.route = newRoute
return item
}
return item
})
if (active !== null) setActiveTab(`${active}`)
setFileList(files)
setTabList(tabs)
if (callback) callback()
})
}
)
const fileFunction = (kind: string, index: string, isFileList: boolean) => {
const tabCodeInfo = isFileList ? fileList[index] : tabList[index]
switch (kind) {
case "own":
closeCode(index, isFileList)
return
case "other":
const tabInfo: tabCodeProps = cloneDeep(tabList[index])
for (let i in tabList) {
if (i !== index && !tabList[i].isFile) {
const arr: tabCodeProps[] =
+i > +index
? [tabInfo].concat(tabList.splice(+i, tabList.length))
: tabList.splice(+i, tabList.length)
const num = +i > +index ? 1 : 0
setActiveTab(`${num}`)
setTabList(arr)
setHintFile(arr[num].tab)
setHintIndex(num)
setHintShow(true)
return
}
}
const code = cloneDeep(tabList[index])
setTabList([code])
setActiveTab(`0`)
return
case "all":
for (let i in tabList) {
if (!tabList[i].isFile) {
const arr = tabList.splice(+i, tabList.length)
setActiveTab("0")
setTabList(arr)
setHintFile(arr[0].tab)
setHintIndex(0)
setHintShow(true)
return
}
}
setActiveTab("")
setTabList([])
return
case "remove":
closeCode(index, isFileList)
return
case "delete":
delCode(index)
return
case "rename":
setRenameIndex(+index)
setRenameFlag(true)
setRenameCache(tabCodeInfo.tab)
return
}
}
const openFileLayout = (file: any) => {
addFile(file)
}
useEffect(() => {
ipcRenderer.invoke("fetch-code-path")
.then((path: string) => {
ipcRenderer.invoke("is-exists-file", path)
.then(() => {
setCodePath("")
})
.catch(() => {
setCodePath(path)
})
})
}, [])
useEffect(() => {
if (tabList.length === 0) setFullScreen(false)
}, [tabList])
useEffect(() => {
if (!xtermRef) {
return
}
// let buffer = "";
ipcRenderer.on("client-yak-error", async (e: any, data) => {
failed(`FoundError: ${JSON.stringify(data)}`)
if (typeof data === "object") {
setErrors([...errors, `${JSON.stringify(data)}`])
} else if (typeof data === "string") {
setErrors([...errors, data])
} else {
setErrors([...errors, `${data}`])
}
})
ipcRenderer.on("client-yak-end", () => {
info("Yak 代码执行完毕")
setTriggerForUpdatingHistory(getRandomInt(100000))
setTimeout(() => {
setExecuting(false)
}, 300)
})
ipcRenderer.on("client-yak-data", async (e: any, data: ExecResult) => {
if (data.IsMessage) {
// alert(Buffer.from(data.Message).toString("utf8"))
}
if (data?.Raw) {
writeExecResultXTerm(xtermRef, data, outputEncoding)
// writeXTerm(xtermRef, Buffer.from(data.Raw).toString(outputEncoding).replaceAll("\n", "\r\n"))
// monacoEditorWrite(currentOutputEditor, )
}
})
return () => {
ipcRenderer.removeAllListeners("client-yak-data")
ipcRenderer.removeAllListeners("client-yak-end")
ipcRenderer.removeAllListeners("client-yak-error")
}
}, [xtermRef])
const bars = (props: any, TabBarDefault: any) => {
return (
<TabBarDefault
{...props}
children={(barNode: React.ReactElement) => {
return (
<Dropdown
overlay={CustomMenu(barNode.key, false, tabMenu, fileFunction)}
trigger={["contextMenu"]}
>
{barNode}
</Dropdown>
)
}}
/>
)
}
return (
<AutoCard
className={"yak-executor-body"}
// title={"Yak Runner"}
headStyle={{minHeight: 0}}
bodyStyle={{padding: 0, overflow: "hidden"}}
>
<div
style={{width: "100%", height: "100%", display: "flex", backgroundColor: "#E8E9E8"}}
tabIndex={0}
onKeyDown={(e) => {
if (e.keyCode === 78 && (e.ctrlKey || e.metaKey)) {
newFile()
}
if (e.keyCode === 83 && (e.ctrlKey || e.metaKey) && activeTab) {
saveCode(tabList[+activeTab], +activeTab)
}
}}
>
<div style={{width: `${fullScreen ? 0 : 15}%`}}>
<AutoSpin spinning={loading}>
<ExecutorFileList
lists={fileList}
activeFile={tabList[+activeTab]?.route || ""}
renameFlag={renameFlag}
renameIndex={renameIndex}
renameCache={renameCache}
setRenameCache={setRenameCache}
addFile={addFile}
newFile={newFile}
openFile={openFileLayout}
fileFunction={fileFunction}
/>
</AutoSpin>
</div>
<div style={{width: `${fullScreen ? 100 : 85}%`}} className='executor-right-body'>
{tabList.length > 0 && (
<ResizeBox
isVer
firstNode={
<Tabs
className={"right-editor"}
style={{height: "100%"}}
type='editable-card'
activeKey={activeTab}
hideAdd={true}
onChange={(activeTab) => setActiveTab(activeTab)}
onEdit={(key, event: "add" | "remove") => {
switch (event) {
case "remove":
closeCode(key, false)
return
case "add":
return
}
}}
renderTabBar={(props, TabBarDefault) => {
return bars(props, TabBarDefault)
}}
tabBarExtraContent={
tabList.length && (
<Space style={{marginRight: 5}} size={0}>
<Button
style={{height: 25}}
type={"link"}
size={"small"}
disabled={
tabList[+activeTab] && tabList[+activeTab].suffix !== "yak"
}
onClick={(e) => {
let m = showDrawer({
width: "60%",
placement: "left",
title: "选择你的 Yak 模块执行特定功能",
content: (
<>
<YakScriptManagerPage
type={"yak"}
onLoadYakScript={(s) => {
const tab: tabCodeProps = {
tab: `Untitle-${unTitleCount}.yak`,
code: s.Content,
suffix: "yak",
isFile: false
}
info(`加载 Yak 模块:${s.ScriptName}`)
xtermClear(xtermRef)
setActiveTab(`${tabList.length}`)
setTabList(tabList.concat([tab]))
setUnTitleCount(unTitleCount + 1)
m.destroy()
}}
/>
</>
)
})
}}
>
Yak脚本模板
</Button>
<Button
icon={
fullScreen ? (
<FullscreenExitOutlined style={{fontSize: 15}} />
) : (
<FullscreenOutlined style={{fontSize: 15}} />
)
}
type={"link"}
size={"small"}
style={{width: 30, height: 25}}
onClick={() => setFullScreen(!fullScreen)}
/>
<Popover
trigger={["click"]}
title={"设置命令行额外参数"}
placement="bottomRight"
content={
<Space style={{width: 400}}>
<div>yak {tabList[+activeTab]?.tab || "[file]"}</div>
<Divider type={"vertical"} />
<Paragraph
style={{width: 200, marginBottom: 0}}
editable={{
icon: <Space>
<EditOutlined />
<SaveOutlined onClick={(e) => {
e.stopPropagation()
tabList[+activeTab].extraParams = extraParams
setTabList(tabList)
if(tabList[+activeTab].isFile){
const files = fileList.map(item => {
if(item.route === tabList[+activeTab].route){
item.extraParams = extraParams
return item
}
return item
})
setFileList(files)
}
success("保存成功")
}}
/></Space>,
tooltip: '编辑/保存为该文件默认参数',
onChange: setExtraParams
}}
>
{extraParams}
</Paragraph>
</Space>
}
>
<Button type={"link"} icon={<EllipsisOutlined />} onClick={() => {
setExtraParams(tabList[+activeTab]?.extraParams || "")
}} />
</Popover>
{executing ? (
<Button
icon={<PoweroffOutlined style={{fontSize: 15}} />}
type={"link"}
danger={true}
size={"small"}
style={{width: 30, height: 25}}
onClick={() => ipcRenderer.invoke("cancel-yak")}
/>
) : (
<Button
icon={<CaretRightOutlined style={{fontSize: 15}} />}
type={"link"}
ghost={true}
size={"small"}
style={{width: 30, height: 25}}
disabled={
tabList[+activeTab] && tabList[+activeTab].suffix !== "yak"
}
onClick={() => {
setErrors([])
setExecuting(true)
ipcRenderer.invoke("exec-yak", {
Script: tabList[+activeTab].code,
Params: [],
RunnerParamRaw: extraParams
})
}}
/>
)}
</Space>
)
}
>
{tabList.map((item, index) => {
return (
<TabPane tab={item.tab} key={`${index}`}>
<div style={{height: "100%"}}>
<AutoSpin spinning={executing}>
<div style={{height: "100%"}}>
<YakEditor
type={item.suffix}
value={item.code}
setValue={(value) => {
modifyCode(value, index)
}}
/>
</div>
</AutoSpin>
</div>
</TabPane>
)
})}
</Tabs>
}
firstRatio='70%'
secondNode={
<div
ref={xtermAsideRef}
style={{
width: "100%",
height: "100%",
overflow: "hidden",
borderTop: "1px solid #dfdfdf"
}}
>
<Tabs
style={{height: "100%"}}
className={"right-xterm"}
size={"small"}
tabBarExtraContent={
<Space>
<SelectOne
formItemStyle={{marginBottom: 0}}
value={outputEncoding}
setValue={setOutputEncoding}
size={"small"}
data={[
{text: "GBxxx编码", value: "latin1"},
{text: "UTF-8编码", value: "utf8"}
]}
/>
<Button
size={"small"}
icon={<DeleteOutlined />}
type={"link"}
onClick={(e) => {
xtermClear(xtermRef)
}}
/>
</Space>
}
>
<TabPane
tab={<div style={{width: 50, textAlign: "center"}}>输出</div>}
key={"output"}
>
<div style={{width: "100%", height: "100%"}}>
<CVXterm
ref={xtermRef}
options={{
convertEol: true,
theme: {
foreground: "#536870",
background: "#E8E9E8",
cursor: "#536870",
black: "#002831",
brightBlack: "#001e27",
red: "#d11c24",
brightRed: "#bd3613",
green: "#738a05",
brightGreen: "#475b62",
yellow: "#a57706",
brightYellow: "#536870",
blue: "#2176c7",
brightBlue: "#708284",
magenta: "#c61c6f",
brightMagenta: "#5956ba",
cyan: "#259286",
brightCyan: "#819090",
white: "#eae3cb",
brightWhite: "#fcf4dc"
}
}}
/>
</div>
</TabPane>
<TabPane
tab={
<div style={{width: 50, textAlign: "center"}} key={"terminal"}>
终端(监修中)
</div>
}
disabled
>
<Terminal />
</TabPane>
</Tabs>
</div>
}
secondRatio='30%'
/>
)}
{tabList.length === 0 && (
<Empty className='right-empty' description={<p>请点击左侧打开或新建文件</p>}></Empty>
)}
</div>
<Modal
visible={hintShow}
onCancel={() => setHintShow(false)}
footer={[
<Button key='link' onClick={() => setHintShow(false)}>
取消
</Button>,
<Button key='submit' onClick={() => ownCloseCode()}>
不保存
</Button>,
<Button key='back' type='primary' onClick={() => saveCode(tabList[hintIndex], hintIndex)}>
保存
</Button>
]}
>
<div style={{height: 40}}>
<ExclamationCircleOutlined style={{fontSize: 22, color: "#faad14"}} />
<span style={{fontSize: 18, marginLeft: 15}}>文件未保存</span>
</div>
<p style={{fontSize: 15, marginLeft: 37}}>{`是否要保存${hintFile}里面的内容吗?`}</p>
</Modal>
<Modal
visible={renameHint}
onCancel={() => setHintShow(false)}
footer={[
<Button key='link' onClick={() => setRenameHint(false)}>
取消
</Button>,
<Button
key='back'
type='primary'
onClick={() => {
const oldRoute = tabList[renameIndex].route
if (!oldRoute) return
const flagStr = oldRoute?.indexOf("/") > -1 ? "/" : "\\"
const routes = oldRoute?.split(flagStr)
routes?.pop()
const newRoute = routes?.concat([renameCache]).join(flagStr)
if (!oldRoute || !newRoute) return
renameFile(renameIndex, renameCache, oldRoute, newRoute, () => {
setRenameHint(false)
})
}}
>
确定
</Button>
]}
>
<div style={{height: 40}}>
<ExclamationCircleOutlined style={{fontSize: 22, color: "#faad14"}} />
<span style={{fontSize: 18, marginLeft: 15}}>文件已存在</span>
</div>
<p style={{fontSize: 15, marginLeft: 37}}>{`是否要覆盖已存在的文件吗?`}</p>
</Modal>
</div>
</AutoCard>
)
}
Example #22
Source File: DashboardItem.tsx From posthog-foss with MIT License | 4 votes |
export function DashboardItem({
item,
dashboardId,
receivedErrorFromAPI,
updateItemColor,
setDiveDashboard,
loadDashboardItems,
isDraggingRef,
isReloading,
reload,
dashboardMode,
isOnEditMode,
setEditMode,
index,
layout,
footer,
onClick,
moveDashboardItem,
saveDashboardItem,
duplicateDashboardItem,
isHighlighted = false,
doNotLoad = false,
}: DashboardItemProps): JSX.Element {
const [initialLoaded, setInitialLoaded] = useState(false)
const [showSaveModal, setShowSaveModal] = useState(false)
const { currentTeamId } = useValues(teamLogic)
const { nameSortedDashboards } = useValues(dashboardsModel)
const { renameInsight } = useActions(insightsModel)
const { featureFlags } = useValues(featureFlagLogic)
const _type = getDisplayedType(item.filters)
const insightTypeDisplayName =
item.filters.insight === InsightType.RETENTION
? 'Retention'
: item.filters.insight === InsightType.PATHS
? 'Paths'
: item.filters.insight === InsightType.FUNNELS
? 'Funnel'
: item.filters.insight === InsightType.STICKINESS
? 'Stickiness'
: 'Trends'
const className = displayMap[_type].className
const Element = displayMap[_type].element
const viewText = displayMap[_type].viewText
const link = combineUrl(urls.insightView(item.short_id, item.filters), undefined, {
fromDashboard: item.dashboard,
}).url
const color = item.color || 'white'
const otherDashboards: DashboardType[] = nameSortedDashboards.filter((d: DashboardType) => d.id !== dashboardId)
const getDashboard = (id: number): DashboardType | undefined => nameSortedDashboards.find((d) => d.id === id)
const longPressProps = useLongPress(setEditMode, {
ms: 500,
touch: true,
click: false,
exclude: 'table, table *',
})
const filters = { ...item.filters, from_dashboard: item.dashboard || undefined }
const logicProps: InsightLogicProps = {
dashboardItemId: item.short_id,
filters: filters,
cachedResults: (item as any).result,
doNotLoad,
}
const { insightProps, showTimeoutMessage, showErrorMessage, insight, insightLoading, isLoading } = useValues(
insightLogic(logicProps)
)
const { loadResults } = useActions(insightLogic(logicProps))
const { reportDashboardItemRefreshed } = useActions(eventUsageLogic)
const { areFiltersValid, isValidFunnel, areExclusionFiltersValid } = useValues(funnelLogic(insightProps))
const previousLoading = usePrevious(insightLoading)
const diveDashboard = item.dive_dashboard ? getDashboard(item.dive_dashboard) : null
// if a load is performed and returns that is not the initial load, we refresh dashboard item to update timestamp
useEffect(() => {
if (previousLoading && !insightLoading && !initialLoaded) {
setInitialLoaded(true)
}
}, [insightLoading])
// Empty states that completely replace the graph
const BlockingEmptyState = (() => {
// Insight specific empty states - note order is important here
if (item.filters.insight === InsightType.FUNNELS) {
if (!areFiltersValid) {
return <FunnelSingleStepState />
}
if (!areExclusionFiltersValid) {
return <FunnelInvalidExclusionState />
}
if (!isValidFunnel && !(insightLoading || isLoading)) {
return <InsightEmptyState />
}
}
// Insight agnostic empty states
if (showErrorMessage || receivedErrorFromAPI) {
return <InsightErrorState excludeDetail={true} />
}
if (showTimeoutMessage) {
return <InsightTimeoutState isLoading={isLoading} />
}
// Deprecated insights
if ((item.filters.insight as string) === 'SESSIONS') {
return <InsightDeprecatedState deleteCallback={loadDashboardItems} itemId={item.id} itemName={item.name} />
}
return null
})()
// Empty states that can coexist with the graph (e.g. Loading)
const CoexistingEmptyState = (() => {
if (isLoading || insightLoading || isReloading) {
return <Loading />
}
return null
})()
const response = (
<div
key={item.short_id}
className={`dashboard-item ${item.color || 'white'} di-width-${layout?.w || 0} di-height-${
layout?.h || 0
} ph-no-capture`}
{...longPressProps}
data-attr={'dashboard-item-' + index}
style={{ border: isHighlighted ? '1px solid var(--primary)' : undefined }}
>
{!BlockingEmptyState && CoexistingEmptyState}
<div className={`dashboard-item-container ${className}`}>
<div className="dashboard-item-header" style={{ cursor: isOnEditMode ? 'move' : 'inherit' }}>
<div className="dashboard-item-title" data-attr="dashboard-item-title">
{dashboardMode === DashboardMode.Public ? (
item.name
) : (
<Link
draggable={false}
to={link}
title={item.name}
preventClick
onClick={() => {
if (!isDraggingRef?.current) {
onClick ? onClick() : router.actions.push(link)
}
}}
style={{ fontSize: 16, fontWeight: 500 }}
>
{item.name || `Untitled ${insightTypeDisplayName} Query`}
</Link>
)}
</div>
{dashboardMode !== DashboardMode.Public && (
<div className="dashboard-item-settings">
{saveDashboardItem &&
dashboardMode !== DashboardMode.Internal &&
(!item.saved && item.dashboard ? (
<Link to={'/dashboard/' + item.dashboard}>
<small>View dashboard</small>
</Link>
) : (
<Tooltip title="Save insight">
<SaveOutlined
style={{
cursor: 'pointer',
marginTop: -3,
...(item.saved
? {
background: 'var(--primary)',
color: 'white',
}
: {}),
}}
onClick={() => {
if (item.saved) {
return saveDashboardItem({ ...item, saved: false })
}
if (item.name) {
// If item already has a name we don't have to ask for it again
return saveDashboardItem({ ...item, saved: true })
}
setShowSaveModal(true)
}}
/>
</Tooltip>
))}
{dashboardMode !== DashboardMode.Internal && (
<>
{featureFlags[FEATURE_FLAGS.DIVE_DASHBOARDS] &&
typeof item.dive_dashboard === 'number' && (
<Tooltip title={`Dive to ${diveDashboard?.name || 'connected dashboard'}`}>
<LinkButton
to={dashboardDiveLink(item.dive_dashboard, item.short_id)}
icon={
<span role="img" aria-label="dive" className="anticon">
<DiveIcon />
</span>
}
data-attr="dive-btn-dive"
className="dive-btn dive-btn-dive"
>
Dive
</LinkButton>
</Tooltip>
)}
<Dropdown
overlayStyle={{ minWidth: 240, border: '1px solid var(--primary)' }}
placement="bottomRight"
trigger={['click']}
overlay={
<Menu
data-attr={'dashboard-item-' + index + '-dropdown-menu'}
style={{ padding: '12px 4px' }}
>
<Menu.Item data-attr={'dashboard-item-' + index + '-dropdown-view'}>
<Link to={link}>{viewText}</Link>
</Menu.Item>
<Menu.Item
data-attr={'dashboard-item-' + index + '-dropdown-refresh'}
onClick={() => {
// On dashboards we use custom reloading logic, which updates a
// global "loading 1 out of n" label, and loads 4 items at a time
if (reload) {
reload()
} else {
loadResults(true)
}
reportDashboardItemRefreshed(item)
}}
>
<Tooltip
placement="left"
title={
<i>
Last updated:{' '}
{item.last_refresh
? dayjs(item.last_refresh).fromNow()
: 'recently'}
</i>
}
>
Refresh
</Tooltip>
</Menu.Item>
<Menu.Item
data-attr={'dashboard-item-' + index + '-dropdown-rename'}
onClick={() => renameInsight(item)}
>
Rename
</Menu.Item>
{updateItemColor && (
<Menu.SubMenu
data-attr={'dashboard-item-' + index + '-dropdown-color'}
key="colors"
title="Set color"
>
{Object.entries(dashboardColorNames).map(
([itemClassName, itemColor], colorIndex) => (
<Menu.Item
key={itemClassName}
onClick={() =>
updateItemColor(item.id, itemClassName)
}
data-attr={
'dashboard-item-' +
index +
'-dropdown-color-' +
colorIndex
}
>
<span
style={{
background: dashboardColors[itemClassName],
border: '1px solid #eee',
display: 'inline-block',
width: 13,
height: 13,
verticalAlign: 'middle',
marginRight: 5,
marginBottom: 1,
}}
/>
{itemColor}
</Menu.Item>
)
)}
</Menu.SubMenu>
)}
{featureFlags[FEATURE_FLAGS.DIVE_DASHBOARDS] && setDiveDashboard && (
<Menu.SubMenu
data-attr={'dashboard-item-' + index + '-dive-dashboard'}
key="dive"
title={`Set dive dashboard`}
>
{otherDashboards.map((dashboard, diveIndex) => (
<Menu.Item
data-attr={
'dashboard-item-' +
index +
'-dive-dashboard-' +
diveIndex
}
key={dashboard.id}
onClick={() => setDiveDashboard(item.id, dashboard.id)}
disabled={dashboard.id === item.dive_dashboard}
>
{dashboard.name}
</Menu.Item>
))}
<Menu.Item
data-attr={
'dashboard-item-' + index + '-dive-dashboard-remove'
}
key="remove"
onClick={() => setDiveDashboard(item.id, null)}
className="text-danger"
>
Remove
</Menu.Item>
</Menu.SubMenu>
)}
{duplicateDashboardItem && otherDashboards.length > 0 && (
<Menu.SubMenu
data-attr={'dashboard-item-' + index + '-dropdown-copy'}
key="copy"
title="Copy to"
>
{otherDashboards.map((dashboard, copyIndex) => (
<Menu.Item
data-attr={
'dashboard-item-' +
index +
'-dropdown-copy-' +
copyIndex
}
key={dashboard.id}
onClick={() =>
duplicateDashboardItem(item, dashboard.id)
}
>
<span
style={{
background: dashboardColors[className],
border: '1px solid #eee',
display: 'inline-block',
width: 13,
height: 13,
verticalAlign: 'middle',
marginRight: 5,
marginBottom: 1,
}}
/>
{dashboard.name}
</Menu.Item>
))}
</Menu.SubMenu>
)}
{moveDashboardItem &&
(otherDashboards.length > 0 ? (
<Menu.SubMenu
data-attr={'dashboard-item-' + index + '-dropdown-move'}
key="move"
title="Move to"
>
{otherDashboards.map((dashboard, moveIndex) => (
<Menu.Item
data-attr={
'dashboard-item-' +
index +
'-dropdown-move-' +
moveIndex
}
key={dashboard.id}
onClick={() =>
moveDashboardItem(item, dashboard.id)
}
>
{dashboard.name}
</Menu.Item>
))}
</Menu.SubMenu>
) : null)}
{duplicateDashboardItem && (
<Menu.Item
data-attr={'dashboard-item-' + index + '-dropdown-duplicate'}
onClick={() => duplicateDashboardItem(item)}
>
Duplicate
</Menu.Item>
)}
<Menu.Item
data-attr={'dashboard-item-' + index + '-dropdown-delete'}
onClick={() =>
deleteWithUndo({
object: {
id: item.id,
name: item.name,
},
endpoint: `projects/${currentTeamId}/insights`,
callback: loadDashboardItems,
})
}
className="text-danger"
>
Delete
</Menu.Item>
</Menu>
}
>
<span
data-attr={'dashboard-item-' + index + '-dropdown'}
style={{ cursor: 'pointer', marginTop: -3 }}
>
<EllipsisOutlined />
</span>
</Dropdown>
</>
)}
</div>
)}
</div>
{item.description && (
<div style={{ padding: '0 16px', marginBottom: 16, fontSize: 12 }}>{item.description}</div>
)}
<div className={`dashboard-item-content ${_type}`} onClickCapture={onClick}>
{!!BlockingEmptyState ? (
BlockingEmptyState
) : (
<Alert.ErrorBoundary message="Error rendering graph!">
{dashboardMode === DashboardMode.Public && !insight.result && !item.result ? (
<Skeleton />
) : (
<Element
dashboardItemId={item.short_id}
cachedResults={item.result}
filters={filters}
color={color}
theme={color === 'white' ? 'light' : 'dark'}
inSharedMode={dashboardMode === DashboardMode.Public}
/>
)}
</Alert.ErrorBoundary>
)}
</div>
{footer}
</div>
{showSaveModal && saveDashboardItem && (
<SaveModal
title="Save Chart"
prompt="Name of Chart"
textLabel="Name"
textPlaceholder="DAUs Last 14 days"
visible={true}
onCancel={() => {
setShowSaveModal(false)
}}
onSubmit={(text) => {
saveDashboardItem({ ...item, name: text, saved: true })
setShowSaveModal(false)
}}
/>
)}
</div>
)
return (
<BindLogic logic={insightLogic} props={insightProps}>
{response}
</BindLogic>
)
}