@ant-design/icons#RightOutlined TypeScript Examples
The following examples show how to use
@ant-design/icons#RightOutlined.
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: ProviderGroupMenu.tsx From next-basics with GNU General Public License v3.0 | 6 votes |
export function ProviderGroupMenu(
props: ProviderGroupMenuProps
): React.ReactElement {
const { itemList, onFold, containerStyle = {} } = props;
const [fold, setFold] = useState(false);
const handleClick = (): void => {
const curFold = !fold;
setFold(curFold);
onFold?.(curFold);
};
return (
<div style={{ position: "relative" }}>
<span
onClick={handleClick}
className={classNames(styles.customIcon, { [styles.fasten]: fold })}
>
{fold ? <LeftOutlined /> : <RightOutlined />}
</span>
<div
className={styles.container}
style={{
display: fold ? "none" : "block",
...containerStyle,
}}
>
{itemList?.map((name) => (
<div key={name} className={styles.itemWrapper} title={name}>
<HashLink to={`#${name}`} className={styles.item}>
{name}
</HashLink>
</div>
))}
</div>
</div>
);
}
Example #2
Source File: Panel.tsx From fe-v5 with Apache License 2.0 | 6 votes |
export default function Panel(props: IProps) {
const [isActive, setIsActive] = useState<boolean>(props.isActive || true);
return (
<div
className={classnames({
'n9e-collapse-item': true,
'n9e-collapse-item-active': isActive,
'n9e-collapse-item-inner': props.isInner,
})}
>
<div
className='n9e-collapse-header'
onClick={() => {
setIsActive(!isActive);
}}
>
{isActive ? <DownOutlined className='n9e-collapse-arrow' /> : <RightOutlined className='n9e-collapse-arrow' />}
{props.header}
<div
className='n9e-collapse-extra'
onClick={(e) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
}}
>
{props.extra}
</div>
</div>
<div
className={classnames({
'n9e-collapse-content': true,
'n9e-collapse-content-hidden': !isActive,
})}
>
<div className='n9e-collapse-content-box'>{props.children}</div>
</div>
</div>
);
}
Example #3
Source File: DesktopSideNav.tsx From condo with MIT License | 5 votes |
DesktopSideNav: React.FC<ISideNavProps> = (props) => {
const { onLogoClick, menuData } = props
const { link } = useOrganization()
const { isSmall, toggleCollapsed, isCollapsed } = useLayoutContext()
const isEmployeeBlocked = get(link, 'isBlocked', false)
if (isEmployeeBlocked) {
return null
}
// TODO: (Dimitreee) implement mobile nav later
if (isSmall) {
return null
}
return (
<>
<Layout.Sider
collapsed={isCollapsed}
theme='light'
css={SIDE_NAV_STYLES}
width={SIDE_MENU_WIDTH}
collapsedWidth={COLLAPSED_SIDE_MENU_WIDTH}
>
<LogoContainer>
<Logo onClick={onLogoClick} minified={isCollapsed}/>
</LogoContainer>
<LayoutTriggerWrapper>
<Button
onClick={toggleCollapsed}
size={'small'}
shape={'circle'}
icon={isCollapsed ? <RightOutlined style={{ fontSize: '13px' }} /> : <LeftOutlined style={{ fontSize: '13px' }}/>}
/>
</LayoutTriggerWrapper>
<ActionsContainer minified={isCollapsed}>
<ResidentActions minified={isCollapsed}/>
</ActionsContainer>
<MenuItemsContainer>
{menuData}
</MenuItemsContainer>
<ServiceSubscriptionIndicator/>
</Layout.Sider>
<Layout.Sider
collapsed={isCollapsed}
width={SIDE_MENU_WIDTH}
collapsedWidth={COLLAPSED_SIDE_MENU_WIDTH}
/>
</>
)
}
Example #4
Source File: StatsCard.tsx From condo with MIT License | 5 votes |
StatsCard: React.FC<IStatsCardProps> = (props) => {
const intl = useIntl()
const extraTitle = intl.formatMessage({ id: 'component.statscard.ExtraTitle' })
const SELECTED_PERIOD: SelectedPeriod = {
calendarWeek: intl.formatMessage({ id: 'component.statscard.periodtypes.Week' }),
month: intl.formatMessage({ id: 'component.statscard.periodtypes.Month' }),
quarter: intl.formatMessage({ id: 'component.statscard.periodtypes.Quarter' }),
year: intl.formatMessage({ id: 'component.statscard.periodtypes.Year' }),
}
const { title, children, link, loading = false, onFilterChange, dependencyArray } = props
const [selectedPeriod, setSelectedPeriod] = useState<string>(Object.keys(SELECTED_PERIOD)[0])
const updateDependencies = [selectedPeriod, ...dependencyArray]
useEffect(() => {
onFilterChange(selectedPeriod)
}, updateDependencies)
const menuClick = useCallback(({ key }) => { setSelectedPeriod(key)}, [])
const linkClick = useCallback(() => { Router.push(link) }, [link])
const menuOverlay = (
<Menu onClick={menuClick} disabled={loading}>
{
Object.keys(SELECTED_PERIOD).map((period) => (
<Menu.Item key={period}>{SELECTED_PERIOD[period]}</Menu.Item>
))
}
</Menu>
)
const cardTitle = (
<Space css={cardTitleCss}>
{title}
<Dropdown overlay={menuOverlay} >
<span style={DROPDOWN_TEXT_STYLE}>{SELECTED_PERIOD[selectedPeriod]} <DownOutlined /></span>
</Dropdown>
</Space>
)
const cardExtra = (
<Button style={CARD_EXTRA_STYLE} type={'inlineLink'} onClick={linkClick}>
{extraTitle}{<RightOutlined />}
</Button>
)
return (
<Row gutter={STATS_CARD_ROW_GUTTER} align={'middle'}>
<Col span={24}>
<Card
title={cardTitle}
bordered={false}
headStyle={CARD_HEAD_STYLE}
extra={cardExtra}
>
{loading ? <Skeleton active round paragraph={{ rows: 1 }} /> : children}
</Card>
</Col>
</Row>
)
}
Example #5
Source File: Pagination.tsx From wildduck-ui with MIT License | 5 votes |
Pagination: React.FC<IPagination> = ({
limit,
previous,
next,
page,
setLimit,
setPrevious,
setNext,
setPage,
}: IPagination) => {
return (
<Space>
<Tooltip title={'Previous'}>
<Button
className='ant-btn-icon'
disabled={_.isEmpty(previous)}
onClick={() => {
setPrevious(previous);
setPage(Math.max(page - 1, 1));
}}
>
<LeftOutlined className='blue-color' />
</Button>
</Tooltip>
<Select
style={{ width: 120 }}
defaultValue={limit}
onSelect={(value) => {
setLimit(value);
}}
>
{_.map([10, 20, 30, 50, 100, 250], (value) => (
<Select.Option key={value} value={value}>
{value + ' / page'}
</Select.Option>
))}
</Select>
<Tooltip title={'Next'}>
<Button
className='ant-btn-icon'
disabled={_.isEmpty(next)}
onClick={() => {
setNext(next);
setPage(page + 1);
}}
>
<RightOutlined className='blue-color' />
</Button>
</Tooltip>
</Space>
);
}
Example #6
Source File: WorkbenchPane.spec.tsx From next-basics with GNU General Public License v3.0 | 5 votes |
describe("WorkbenchPane", () => {
beforeEach(() => {
jest.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => {
Promise.resolve(1).then(cb);
return 1;
});
});
afterEach(() => {
(window.requestAnimationFrame as jest.Mock).mockRestore();
});
it("should work", async () => {
const onActiveChange = jest.fn();
const onFirstActivated = jest.fn();
const wrapper = mount(
<WorkbenchPane
titleLabel="Hello"
active={false}
onActiveChange={onActiveChange}
onFirstActivated={onFirstActivated}
/>
);
expect(wrapper.find(RightOutlined).length).toBe(1);
expect(onActiveChange).toBeCalledTimes(1);
expect(onActiveChange).toHaveBeenNthCalledWith(1, false);
expect(onFirstActivated).not.toBeCalled();
wrapper.find(".pane-header").simulate("click");
expect(wrapper.find(DownOutlined).length).toBe(1);
expect(onActiveChange).toBeCalledTimes(2);
expect(onActiveChange).toHaveBeenNthCalledWith(2, true);
expect(onFirstActivated).toBeCalledTimes(1);
wrapper.find(".pane-header").simulate("click");
expect(wrapper.find(RightOutlined).length).toBe(1);
expect(onActiveChange).toBeCalledTimes(3);
expect(onActiveChange).toHaveBeenNthCalledWith(3, false);
// Re-active will be ignored.
wrapper.find(".pane-header").simulate("click");
expect(onFirstActivated).toBeCalledTimes(1);
expect(wrapper.find(".pane").hasClass("scrolled")).toBe(false);
await act(async () => {
wrapper.find(".pane-body").getDOMNode().scrollTop = 20;
wrapper.find(".pane-body").simulate("scroll");
await (global as any).flushPromises();
wrapper.update();
});
expect(wrapper.find(".pane").hasClass("scrolled")).toBe(true);
});
});
Example #7
Source File: WorkbenchPane.tsx From next-basics with GNU General Public License v3.0 | 5 votes |
export function WorkbenchPane({
titleLabel,
active,
badge,
onActiveChange,
onFirstActivated,
}: WorkbenchPaneProps): React.ReactElement {
const [internalActive, setInternalActive] = useState(active);
const [activatedOnce, setActivatedOnce] = useState(false);
useEffect(() => {
setInternalActive(active);
}, [active]);
useEffect(() => {
onActiveChange?.(internalActive);
}, [internalActive, onActiveChange]);
const handleClick = useCallback(() => {
setInternalActive((previousActive) => !previousActive);
if (!activatedOnce && !internalActive) {
setActivatedOnce(true);
onFirstActivated?.();
}
}, [activatedOnce, internalActive, onFirstActivated]);
const scrollBodyRef = useRef<HTMLDivElement>();
const [scrolled, setScrolled] = useState(false);
const handleScroll = useMemo(
() =>
debounceByAnimationFrame((): void => {
setScrolled(scrollBodyRef.current.scrollTop > 0);
}),
[]
);
return (
<div
className={classNames("pane", {
scrolled,
})}
>
<div className="pane-header" tabIndex={0} onClick={handleClick}>
<div className="pane-title">
<span className="title-icon">
{internalActive ? <DownOutlined /> : <RightOutlined />}
</span>
<div className="title-label">{titleLabel}</div>
</div>
<slot name="actions" />
{badge !== null && <div className="badge">{badge}</div>}
<div className="pane-scroll-shadow"></div>
</div>
<div
className="pane-body custom-scrollbar-container"
onScroll={handleScroll}
ref={scrollBodyRef}
>
<slot name="content">
<div
style={{
padding: "10px 20px",
color: "var(--text-color-secondary)",
}}
>
No content
</div>
</slot>
</div>
</div>
);
}
Example #8
Source File: GeneralCarousel.tsx From next-basics with GNU General Public License v3.0 | 5 votes |
export function GeneralCarousel({
speed,
slidesToShow,
slidesToScroll,
autoplay,
dots,
components,
carouselStyle,
pauseOnDotsHover,
adaptiveHeight,
infinite,
responsive,
onHandleClick,
noDataDesc,
arrows,
dotsTheme,
useBrick,
dataSource,
autoplaySpeed,
}: GeneralCarouselProps): React.ReactElement {
const comps = Array.isArray(components) ? components : compact([components]);
const data = Array.isArray(dataSource) ? dataSource : compact([dataSource]);
const carousel = (
<Carousel
className={classNames({
"carousel-dots-dark": dotsTheme === "dark",
})}
style={carouselStyle}
autoplay={autoplay}
dots={dots}
speed={speed}
autoplaySpeed={autoplaySpeed}
slidesToShow={slidesToShow}
slidesToScroll={slidesToScroll}
pauseOnDotsHover={pauseOnDotsHover}
arrows={arrows}
infinite={infinite}
adaptiveHeight={adaptiveHeight}
responsive={responsive}
prevArrow={<LeftOutlined />}
nextArrow={<RightOutlined />}
>
{useBrick
? renderCustomBrick(useBrick, data, onHandleClick)
: renderCustomComp(comps, onHandleClick)}
</Carousel>
);
return (
<div className={style.generalCarousel}>
{useBrick ? (
data.length !== 0 ? (
carousel
) : (
<Empty description={noDataDesc} />
)
) : comps.length !== 0 ? (
carousel
) : (
<Empty description={noDataDesc} />
)}
</div>
);
}
Example #9
Source File: SecondaryList.tsx From ant-simple-draw with MIT License | 5 votes |
SecondaryList: FC<SecondaryListType> = memo(({ data, fatherData }) => {
const [oneModuleAll, setOneModuleAll] = useSetState<oneModuleAllType>({
isShow: false,
componentInfo: {},
});
return (
<>
<div
className={styles.contentContainer}
style={{
display: !oneModuleAll.isShow ? 'block' : 'none',
}}
>
{data.map((child, k) => (
<React.Fragment key={k}>
<>
<div className={styles.head}>
<h2 className={styles.title}>{child.title}</h2>
<button
className={styles.more}
onClick={() => setOneModuleAll({ isShow: true, componentInfo: child })}
>
<span>全部</span>
<RightOutlined />
</button>
</div>
<Drag list={child.componentList} category={fatherData.category} />
</>
</React.Fragment>
))}
</div>
<div
className={styles.contentContainer}
style={{
display: oneModuleAll.isShow ? 'block' : 'none',
}}
>
<div className={styles.moreList}>
<button className={styles.more} onClick={() => setOneModuleAll({ isShow: false })}>
<LeftOutlined />
<span>{oneModuleAll.componentInfo.title}</span>
</button>
<Drag list={oneModuleAll.componentInfo.componentList!} category={fatherData.category} />
</div>
</div>
</>
);
})
Example #10
Source File: index.tsx From fe-v5 with Apache License 2.0 | 5 votes |
LeftTree: React.FC<LeftTreeProps> = ({ clusterGroup = {}, busiGroup = {}, eventLevelGroup = {}, eventTypeGroup = {} }) => {
const history = useHistory();
const [collapse, setCollapse] = useState(localStorage.getItem('leftlist') === '1');
const groupItems: IGroupItemProps[] = [
clustersGroupContent(clusterGroup),
busiGroupContent(busiGroup),
{
title: '事件级别',
isShow: eventLevelGroup.isShow,
render() {
return (
<SelectList
dataSource={[
{ label: '一级告警', value: 1 },
{ label: '二级告警', value: 2 },
{ label: '三级告警', value: 3 },
]}
defaultSelect={eventLevelGroup.defaultSelect}
allowNotSelect={true}
onChange={eventLevelGroup?.onChange}
/>
);
},
},
{
title: '事件类别',
isShow: eventTypeGroup.isShow,
render() {
return (
<SelectList
dataSource={[
{ label: 'Triggered', value: 0 },
{ label: 'Recovered', value: 1 },
]}
defaultSelect={eventTypeGroup.defaultSelect}
allowNotSelect={true}
onChange={eventTypeGroup?.onChange}
/>
);
},
},
];
return (
<div className={collapse ? 'left-area collapse' : 'left-area'}>
<div
className='collapse-btn'
onClick={() => {
localStorage.setItem('leftlist', !collapse ? '1' : '0');
setCollapse(!collapse);
}}
>
{!collapse ? <LeftOutlined /> : <RightOutlined />}
</div>
{/* 遍历渲染左侧栏内容 */}
{groupItems.map(
({ title, isShow, shrink = false, render }: IGroupItemProps, i) =>
isShow && (
<div key={i} className={`left-area-group ${shrink ? 'group-shrink' : ''}`} style={typeof shrink === 'object' ? shrink.style : {}}>
<div className='left-area-group-title'>
{title}
{title === '业务组' && <SettingOutlined onClick={() => history.push(`/busi-groups`)} />}
</div>
{render()}
</div>
),
)}
</div>
);
}
Example #11
Source File: detail.tsx From dashboard with Apache License 2.0 | 4 votes |
CustomerDetail: React.FC = () => {
const queryFormRef = useRef<FormInstance>();
const actionRef = useRef<ActionType>();
const [customerDetail, setCustomerDetail] = useState<CustomerItem>()
const [staffMap, setStaffMap] = useState<Dictionary<StaffOption>>({});
const [allCustomerTagGroups, setAllCustomerTagGroups] = useState<CustomerTagGroupItem[]>([]);
const [defaultCustomerTags, setDefaultCustomerTags] = useState<CustomerTag[]>([]);
const [defaultInternalTagsIds, setDefaultInternalTagsIds] = useState<string []>([])
const [personalTagModalVisible, setPersonalTagModalVisible] = useState(false)
const [customerTagModalVisible, setCustomerTagModalVisible] = useState(false)
const [internalTagList, setInternalTagList] = useState<InternalTags.Item[]>([])
const [internalTagListMap, setInternalTagListMap] = useState<Dictionary<InternalTags.Item>>({});
const [initialEvents, setInitialEvents] = useState<CustomerEvents.Item[]>([])
const [currentTab, setCurrentTab] = useState('survey')
const [basicInfoDisplay, setBasicInfoDisplay] = useState({} as any)// 展示哪些基本信息
const [basicInfoValues, setBasicInfoValues] = useState({} as any) // 基本信息取值
const [remarkValues, setRemarkValues] = useState<Remark[]>([])
const [reloadCusDataTimesTamp, setReloadCusDataTimesTamp] = useState(Date.now)
const [formRef] = Form.useForm()
const params = new URLSearchParams(window.location.search);
const currentStaff = localStorage.getItem('extStaffAdminID') as string
const extCustomerID = params.get('ext_customer_id') || "";
if (!extCustomerID) {
message.error('传入参数请带上ID');
}
const extStaff = () => {
const staffs: StaffItem[] = [];
customerDetail?.staff_relations?.forEach((staff_relation) => {
// @ts-ignore
const staff = staffMap[staff_relation.ext_staff_id];
if (staff) {
staffs.push(staff);
}
});
return staffs;
}
const getCustomerDetail = () => {
const hide = message.loading("加载数据中");
GetCustomerDetail(extCustomerID).then(res => {
hide();
if (res?.code !== 0) {
message.error("获取客户详情失败");
return;
}
setCustomerDetail(res?.data);
const cusTags: any[] = [];
const interTagsIds: any[] = []
res?.data?.staff_relations?.forEach((relation: any) => {
if (relation.ext_staff_id === currentStaff) {
relation.customer_staff_tags?.forEach((tag: any) => {
cusTags.push({...tag, name: tag.tag_name, ext_id: tag.ext_tag_id});
});
relation.internal_tags?.forEach((tagId: string) => {
interTagsIds.push(tagId);
})
}
});
setDefaultCustomerTags(cusTags)
setDefaultInternalTagsIds(interTagsIds)
}).catch(() => {
hide();
})
}
const getInternalTags = () => {
QueryInternalTags({page_size: 5000, ext_staff_id: currentStaff}).then(res => {
if (res?.code === 0) {
setInternalTagList(res?.data?.items)
setInternalTagListMap(_.keyBy(res?.data?.items, 'id'))
} else {
message.error(res?.message)
}
})
}
const getCustomerRemark = () => { // 自定义信息id-key
QueryCustomerRemark().then(res => {
if (res?.code === 0) {
console.log('QueryCustomerRemark', res.data)
} else {
message.error(res?.message)
}
})
}
const getBasicInfoDisplay = () => {
GetCustomerBasicInfoDisplay().then(res => {
if (res?.code === 0) {
const displayData = res?.data
delete displayData.id
delete displayData.ext_corp_id
delete displayData.created_at
delete displayData.updated_at
delete displayData.deleted_at
setBasicInfoDisplay(displayData || {})
} else {
message.error(res?.message)
}
})
}
const getBasicInfoAndRemarkValues = () => {
GetBasicInfoAndRemarkValues({
ext_customer_id: extCustomerID,
ext_staff_id: currentStaff,
}).then(res => {
if (res?.code === 0) {
const resData = res?.data
delete resData.id
delete resData.ext_corp_id
delete resData.ext_creator_id
delete resData.ext_customer_id
delete resData.ext_staff_id
delete resData.created_at
delete resData.updated_at
delete resData.deleted_at
delete resData.remark_values
setBasicInfoValues(resData)
setRemarkValues(res?.data?.remark_values || [])
}
})
}
const updateBasicInfoAndRemark = (basicInfoParams: any) => {
UpdateBasicInfoAndRemark({
ext_staff_id: currentStaff,
ext_customer_id: extCustomerID,
...basicInfoParams
}).then(res => {
if (res?.code === 0) {
message.success('客户信息更新成功')
setReloadCusDataTimesTamp(Date.now)
} else {
message.error('客户信息更新失败')
}
})
}
useEffect(() => {
getInternalTags()
getCustomerDetail()
getCustomerRemark()
getBasicInfoDisplay()
getBasicInfoAndRemarkValues()
}, [reloadCusDataTimesTamp])
useEffect(() => {
QueryCustomerTagGroups({page_size: 5000}).then((res) => {
if (res.code === 0) {
setAllCustomerTagGroups(res?.data?.items);
} else {
message.error(res.message);
}
});
}, []);
useEffect(() => {
QuerySimpleStaffs({page_size: 5000}).then((res) => {
if (res.code === 0) {
const staffs = res?.data?.items?.map((item: SimpleStaffInterface) => {
return {
label: item.name,
value: item.ext_id,
...item,
};
}) || [];
setStaffMap(_.keyBy<StaffOption>(staffs, 'ext_id'));
} else {
message.error(res.message);
}
});
}, []);
useEffect(() => {
QueryCustomerEvents({
ext_customer_id: extCustomerID,
ext_staff_id: currentStaff,
page_size: 5
}).then(res => {
console.log('QueryCustomerEventsQueryCustomerEvents', res)
setInitialEvents(res?.data?.items || [])
})
}, [])
useEffect(() => {
formRef.setFieldsValue(basicInfoValues)
}, [basicInfoValues])
return (
<PageContainer
fixedHeader
onBack={() => history.go(-1)}
backIcon={<LeftOutlined/>}
header={{
title: '客户详情',
}}
>
<ProCard>
<Descriptions title="客户信息" column={1}>
<Descriptions.Item>
<div className={'customer-info-field'}>
<div><img src={customerDetail?.avatar} alt={customerDetail?.name} style={{borderRadius: 5}}/></div>
<div style={{fontSize: 16, marginLeft: 10}}>
<p>{customerDetail?.name}</p>
{customerDetail?.corp_name && (
<p style={{color: '#eda150', marginTop: 10}}>@{customerDetail?.corp_name}</p>
)}
{customerDetail?.type === 1 && (
<p style={{
color: '#5ec75d',
fontSize: '13px'
}}>@微信</p>
)}
</div>
</div>
</Descriptions.Item>
<div>
<div style={{width: 70, display: 'inline-block'}}>企业标签:</div>
<div className={styles.tagContainer}>
<Space direction={'horizontal'} wrap={true}>
{
defaultCustomerTags?.length > 0 && defaultCustomerTags?.map((tag) =>
<Tag
key={tag?.id}
className={'tag-item selected-tag-item'}
>
{tag?.name}
</Tag>
)}
</Space>
</div>
<Button
key='addCusTags'
icon={<EditOutlined/>}
type={'link'}
onClick={() => {
setCustomerTagModalVisible(true);
}}
>
编辑
</Button>
</div>
<div>
<div style={{width: 70, display: 'inline-block'}}>个人标签:</div>
<div className={styles.tagContainer}>
<Space direction={'horizontal'} wrap={true}>
{
defaultInternalTagsIds?.length > 0 && defaultInternalTagsIds.map(id => internalTagListMap[id])?.map((tag) =>
<Tag
key={tag?.id}
className={'tag-item selected-tag-item'}
>
{tag?.name}
<span>
</span>
</Tag>
)}
</Space>
</div>
<Button
key='addInternalTags'
icon={<EditOutlined/>}
type={'link'}
onClick={() => {
setPersonalTagModalVisible(true);
}}
>
编辑
</Button>
</div>
</Descriptions>
</ProCard>
<ProCard
tabs={{
onChange: (activeKey: string) => setCurrentTab(activeKey),
activeKey: currentTab
}}
style={{marginTop: 25}}
>
<ProCard.TabPane key="survey" tab="客户概况">
<div className={styles.survey}>
<div className={styles.cusSurveyLeft}>
<div>
<Descriptions title={<div><ContactsFilled/> 添加客服信息</div>} layout="vertical" bordered
column={4}>
<Descriptions.Item label="所属员工">
<CollapsedStaffs limit={2} staffs={extStaff()}/>
</Descriptions.Item>
<Descriptions.Item label="客户来源">
<span>
{customerDetail?.staff_relations?.map((para) => {
return (`${addWayEnums[para.add_way || 0]}\n`);
})}
</span>
</Descriptions.Item>
<Descriptions.Item label="添加时间">
<Space>
{customerDetail?.staff_relations?.map((para) => {
return (
<div className={styles.staffTag}
dangerouslySetInnerHTML={{
__html: moment(para.createtime)
.format('YYYY-MM-DD HH:mm')
.split(' ')
.join('<br />'),
}}
/>
);
})}
</Space>
</Descriptions.Item>
<Descriptions.Item label="更新时间">
<div
dangerouslySetInnerHTML={{
__html: moment(customerDetail?.updated_at)
.format('YYYY-MM-DD HH:mm')
.split(' ')
.join('<br />'),
}}
/>
</Descriptions.Item>
</Descriptions>
</div>
<Form form={formRef} onFinish={(values) => {
console.log('ooooooooooooooovaluesvalues', values)
const basicInfoParams = {...values}
updateBasicInfoAndRemark(basicInfoParams)
}}>
<div style={{paddingTop: 20}} className={styles.baseInfoContainer}>
<Descriptions
title={<div><BookFilled/> 基本信息</div>}
bordered
column={2}
size={'small'}
>
{
Object.keys(basicInfoDisplay).map(key => {
return <Descriptions.Item label={basicInfo[key]}>
<TableInput name={key} />
</Descriptions.Item>
})
}
</Descriptions>
</div>
{
remarkValues.length > 0 && <div style={{paddingTop: 20}} className={styles.customInfoContainer}>
<Descriptions
title={<div><EditFilled/> 自定义信息</div>}
bordered
column={2}
size={'small'}
>
<Descriptions.Item label="sfdsf">
<TableInput name={'aa'}/>
</Descriptions.Item>
<Descriptions.Item label="违法的">
<TableInput name={'bb'}/>
</Descriptions.Item>
<Descriptions.Item label="sdf434">
<TableInput name={'cc'}/>
</Descriptions.Item>
<Descriptions.Item label="yjkyujy">
<TableInput name={'dd'}/>
</Descriptions.Item>
</Descriptions>
</div>
}
<div style={{display: 'flex', justifyContent: 'center', marginTop: 40}}>
<Space>
<Button onClick={() => formRef.setFieldsValue(basicInfoValues)}>重置</Button>
<Button type={"primary"} onClick={() => {
formRef.submit()
}}>提交</Button>
</Space>
</div>
</Form>
</div>
<div className={styles.cusSurveyRight}>
<div className={styles.eventsTitle}>
<span className={styles.titleText}><SoundFilled/> 客户动态</span>
<a onClick={() => setCurrentTab('events')} style={{fontSize: 12}}>查看更多<RightOutlined/></a>
</div>
<Events data={initialEvents.filter(elem => elem !== null)} simpleRender={true} staffMap={staffMap}
extCustomerID={extCustomerID}/>
</div>
</div>
</ProCard.TabPane>
<ProCard.TabPane key="events" tab="客户动态">
<Events staffMap={staffMap} extCustomerID={extCustomerID}/>
</ProCard.TabPane>
<ProCard.TabPane key="room" tab="所在群聊">
<ProTable<GroupChatItem>
search={false}
formRef={queryFormRef}
actionRef={actionRef}
className={'table'}
scroll={{x: 'max-content'}}
columns={columns}
rowKey="id"
toolBarRender={false}
bordered={false}
tableAlertRender={false}
dateFormatter="string"
request={async (originParams: any, sort, filter) => {
return ProTableRequestAdapter(
originParams,
sort,
filter,
QueryCustomerGroupsList,
);
}}
/>
</ProCard.TabPane>
<ProCard.TabPane key="chat" tab="聊天记录">
{
setStaffMap[currentStaff]?.enable_msg_arch === 1 ? <Button
key={'chatSession'}
type={"link"}
icon={<ClockCircleOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
onClick={() => {
window.open(`/staff-admin/corp-risk-control/chat-session?staff=${currentStaff}`)
}}
>
聊天记录查询
</Button>
:
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={<span>员工暂未开启消息存档</span>}/>
}
</ProCard.TabPane>
</ProCard>
<CustomerTagSelectionModal
type={'customerDetailEnterpriseTag'}
isEditable={true}
withLogicalCondition={false}
width={'630px'}
visible={customerTagModalVisible}
setVisible={setCustomerTagModalVisible}
defaultCheckedTags={defaultCustomerTags}
onFinish={(selectedTags) => {
const removeAry = _.difference(defaultCustomerTags.map(dt => dt.ext_id), selectedTags.map(st => st.ext_id))
UpdateCustomerTags({
// @ts-ignore
add_ext_tag_ids: selectedTags.map((tag) => tag.ext_id),
ext_customer_ids: [extCustomerID],
ext_staff_id: currentStaff,
// @ts-ignore
remove_ext_tag_ids: removeAry
}).then(() => {
getCustomerDetail()
})
}}
allTagGroups={allCustomerTagGroups}
/>
<InternalTagModal
width={560}
allTags={internalTagList}
allTagsMap={internalTagListMap}
setAllTags={setInternalTagList}
visible={personalTagModalVisible}
setVisible={setPersonalTagModalVisible}
defaultCheckedTagsIds={defaultInternalTagsIds}
reloadTags={getInternalTags}
onFinish={(selectedTags) => {
console.log('selectedTags', selectedTags)
const removeAry = _.difference(defaultInternalTagsIds, selectedTags.map(st => st.id))
CustomerInternalTags({
// @ts-ignore
add_ext_tag_ids: selectedTags.map((tag) => tag.id),
ext_customer_id: extCustomerID,
ext_staff_id: currentStaff,
// @ts-ignore
remove_ext_tag_ids: removeAry
}).then(() => {
getCustomerDetail()
})
}
}
/>
</PageContainer>
);
}
Example #12
Source File: App.tsx From pcap2socks-gui with MIT License | 4 votes |
render() {
return (
<Layout className="layout">
<Content className="content-wrapper">
<div className="content">
{(() => {
switch (this.state.stage) {
case STAGE_WELCOME:
return this.renderWelcome();
case STAGE_INTERFACE:
return this.renderInterface();
case STAGE_DEVICE:
return this.renderDevice();
case STAGE_PROXY:
return this.renderProxy();
case STAGE_RUNNING:
return this.renderRunning();
default:
return;
}
})()}
</div>
<div className="footer">
{(() => {
if (this.state.stage > STAGE_WELCOME && this.state.stage <= STAGE_PROXY) {
return (
<Button
className="button"
disabled={this.state.loading > 0}
icon={<LeftOutlined />}
onClick={() => this.setState({ stage: this.state.stage - 1 })}
>
上一步
</Button>
);
}
})()}
{(() => {
if (this.state.stage === STAGE_INTERFACE) {
return (
<Button
className="button"
disabled={this.state.loading > 0 && this.state.loading !== 1}
icon={<ReloadOutlined />}
onClick={this.updateInterfaces}
>
刷新网卡列表
</Button>
);
}
})()}
{(() => {
if (this.state.stage === STAGE_PROXY) {
return (
<Button
className="button"
disabled={this.state.loading > 0}
icon={<FolderOpenOutlined />}
onClick={() => {
const node = document.getElementById("open");
if (node) {
node.click();
}
}}
>
导入代理配置
<input id="open" type="file" onChange={this.import} style={{ display: "none" }} />
</Button>
);
}
})()}
{(() => {
if (this.state.stage === STAGE_PROXY) {
return (
<Button className="button" icon={<ExportOutlined />} onClick={this.export}>
导出代理配置
</Button>
);
}
})()}
{(() => {
if (this.state.stage === STAGE_PROXY) {
return (
<Button
className="button"
disabled={this.state.loading > 0 && this.state.loading !== 2}
loading={this.state.loading === 2}
icon={<ExperimentOutlined />}
onClick={this.test}
>
测试代理服务器
</Button>
);
}
})()}
{(() => {
if (this.state.stage === STAGE_WELCOME && this.state.ready) {
return (
<Tooltip title={this.state.destination}>
<Button
className="button"
type="primary"
disabled={this.state.loading > 0 && this.state.loading !== 3}
loading={this.state.loading === 3}
icon={<PlayCircleOutlined />}
onClick={this.run}
>
以上次的配置运行
</Button>
</Tooltip>
);
}
})()}
{(() => {
if (this.state.stage >= STAGE_WELCOME && this.state.stage < STAGE_PROXY) {
return (
<Button
className="button"
disabled={this.state.loading > 0}
icon={<RightOutlined />}
type="primary"
onClick={() => this.setState({ stage: this.state.stage + 1 })}
>
下一步
</Button>
);
}
})()}
{(() => {
if (this.state.stage === STAGE_PROXY) {
return (
<Button
className="button"
type="primary"
disabled={this.state.loading > 0 && this.state.loading !== 3}
loading={this.state.loading === 3}
icon={<PoweroffOutlined />}
onClick={this.run}
>
运行
</Button>
);
}
})()}
{(() => {
if (this.state.stage === STAGE_RUNNING) {
return (
<Button className="button" icon={<GlobalOutlined />} onClick={this.notifyNetwork}>
显示网络设置
</Button>
);
}
})()}
{(() => {
if (this.state.stage === STAGE_RUNNING) {
return (
<Button
className="button"
type="primary"
danger
disabled={this.state.loading > 0 && this.state.loading !== 4}
loading={this.state.loading === 4}
icon={<PoweroffOutlined />}
onClick={this.stopConfirm}
>
停止
</Button>
);
}
})()}
</div>
</Content>
</Layout>
);
}
Example #13
Source File: HorizontalScrollable.tsx From oxen-website with GNU General Public License v3.0 | 4 votes |
function HorizontalScrollableInner(props: Props) {
const { onItemClick, children } = props;
const scrollRef = useRef(null);
const innerContentRef = useRef(null);
const { x } = useScroll(scrollRef);
const pageWidth = useWindowSize().width;
const scrollDistance = pageWidth > 1400 ? 450 : pageWidth / 3;
const [rightScrollHidden, setRightScrollHidden] = useState(false);
const { isDesktop } = useContext(ScreenContext);
const handleLeftScroll = () => {
scrollRef.current.scrollBy({
left: -scrollDistance,
behavior: 'smooth',
});
};
const handleRightScroll = () => {
scrollRef.current.scrollBy({
left: scrollDistance,
behavior: 'smooth',
});
};
function handleItemClick() {
if (onItemClick) {
onItemClick();
}
}
useEffect(() => {
const isFullRight =
scrollRef.current.scrollWidth - scrollRef.current.clientWidth ===
scrollRef.current.scrollLeft;
const tooSmallToScroll =
innerContentRef.current.clientWidth < scrollRef.current.clientWidth;
setRightScrollHidden(tooSmallToScroll || isFullRight);
}, [x, children]);
return (
<div className="relative flex w-full">
<div
className={classNames(
'absolute left-0 flex items-center justify-between h-full w-full',
!isDesktop && 'hidden',
)}
>
<div
className={classNames(
'flex flex-col justify-center h-full z-50 duration-300 -ml-8',
x <= 1 && 'opacity-0',
)}
>
<LeftOutlined
onClick={handleLeftScroll}
className={classNames('h-20 mt-1 cursor-pointer')}
/>
</div>
<div
className={classNames(
'flex flex-col justify-center h-full z-50 duration-300 -mr-8',
rightScrollHidden && 'opacity-0',
)}
>
<RightOutlined
onClick={handleRightScroll}
className="h-20 mt-1 cursor-pointer"
/>
</div>
</div>
<div
ref={scrollRef}
className={classNames(
'relative',
'w-full',
'hide_scroll',
'scrolling-touch hide-scroll',
isDesktop ? 'overflow-x-scroll' : 'overflow-x-scroll',
)}
>
<div
ref={innerContentRef}
className={classNames('flex space-x-8 overflow-y-visible')}
style={{
width: 'min-content',
marginLeft: `${!isDesktop ? UI.PAGE_CONTAINED_PADDING_VW : 0}vw`,
paddingRight: `${!isDesktop ? UI.PAGE_CONTAINED_PADDING_VW : 0}vw`,
}}
>
{children}
</div>
</div>
</div>
);
}
Example #14
Source File: PopularTimes.tsx From office-hours with GNU General Public License v3.0 | 4 votes |
export default function PopularTimes({ heatmap }: HeatmapProps): ReactElement {
const [currentDayOfWeek, setCurrentDayOfWeek] = useState(new Date().getDay());
const [firstHour, lastHour] = findWeekMinAndMax(heatmap);
const dailyAvgWaitTimes: number[] = chunk(heatmap, 24).map((hours) => {
const filteredOfficeHours = hours.filter((v) => v !== -1);
return filteredOfficeHours.length > 0 ? mean(filteredOfficeHours) : -1;
});
return (
<div className="hide-in-percy">
<TitleRow>
<h2>Wait Times on</h2>
<Dropdown
trigger={["click"]}
overlay={
<Menu>
{DAYS_OF_WEEK.map((dayName, i) => (
<Menu.Item key={dayName}>
<a onClick={() => setCurrentDayOfWeek(i)}>{dayName}</a>
</Menu.Item>
))}
</Menu>
}
>
<WeekdayDropdown>
{DAYS_OF_WEEK[currentDayOfWeek]}
<DownOutlined />
</WeekdayDropdown>
</Dropdown>
</TitleRow>
<GraphWithArrow>
<GraphArrowButtons
onClick={() => setCurrentDayOfWeek((7 + currentDayOfWeek - 1) % 7)}
>
<LeftOutlined />
</GraphArrowButtons>
<GraphContainer>
<ParentSize>
{({ width }) => (
<TimeGraph
values={heatmap
.slice(currentDayOfWeek * 24, (currentDayOfWeek + 1) * 24 - 1)
.map((i) => (i < 0 ? 0 : Math.floor(i)))}
maxTime={Math.max(...heatmap)}
firstHour={firstHour}
lastHour={lastHour}
width={width}
height={220}
/>
)}
</ParentSize>
</GraphContainer>
<GraphArrowButtons
onClick={() => setCurrentDayOfWeek((currentDayOfWeek + 1) % 7)}
>
<RightOutlined />
</GraphArrowButtons>
</GraphWithArrow>
{dailyAvgWaitTimes[currentDayOfWeek] >= 0 && (
<GraphNotes>
<ClockCircleOutlined /> {DAYS_OF_WEEK[currentDayOfWeek]}s have{" "}
<strong>
{generateBusyText(currentDayOfWeek, dailyAvgWaitTimes)}
</strong>{" "}
wait times.
</GraphNotes>
)}
{new Date().getDay() === currentDayOfWeek &&
heatmap[currentDayOfWeek * 24 + new Date().getHours()] >= 0 && (
<GraphNotes>
<HourglassOutlined /> At {formatDateHour(new Date().getHours())},
people generally wait{" "}
<strong>
{formatWaitTime(
heatmap[currentDayOfWeek * 24 + new Date().getHours()]
)}
</strong>
.
</GraphNotes>
)}
</div>
);
}
Example #15
Source File: index.tsx From amiya with MIT License | 4 votes |
export default function Demo() {
// 列表控制
const listRef = useRef<any>()
// 选中的年份
const [activeYear, setActiveYear] = useState(1)
// 选中的状态
const [activeStatus, setActiveStatus] = useState(1)
// 选中的 tab
const [activeTab, setActiveTab] = useState(1)
// 选中的标签
const [activeTag, setActiveTag] = useState(undefined)
// 标签是否可见
const [tagVisible, setTagVisible] = useState(false)
// 查询参数 打印用
const [searchValue, setSearchValue] = useState({})
// 查询区域是否可见
const searchVisible = useMemo(() => {
return activeTab !== 6
}, [activeTab])
// 查询参数
const searchParams = useMemo(() => {
return {
activeYear,
activeStatus,
activeTab,
activeTag
}
}, [activeTab, activeYear, activeStatus, activeTag])
useEffect(() => {
// 查询参数改变,刷新列表
listRef.current.reset()
}, [searchParams])
useEffect(() => {
// 如果不显示标签,则把标签值值设置为空
if (!tagVisible) {
setActiveTag(undefined)
}
}, [tagVisible])
useEffect(() => {
// 查询区域不显示时,清空它的值
if (!searchVisible) {
setActiveTag(undefined)
setTagVisible(false)
}
}, [searchVisible])
return (
<div className="space-list">
<AySearchList
ref={listRef}
title={
<Space size={24}>
<Tabs activeKey={activeTab + ''} onChange={key => setActiveTab(Number(key))}>
{tabOptions.map(option => (
<Tabs.TabPane key={option.value} className="goods" tab={option.label} />
))}
</Tabs>
</Space>
}
api={(searchValues: AnyKeyProps) =>
new Promise(resolve => {
setSearchValue(searchValues)
setTimeout(() => {
resolve({
content: data,
totalCount: data.length
})
}, 500)
})
}
extendSearchParams={searchParams}
extraVisible={false}
autoload={false}
listExtend={{
itemLayout: 'vertical'
}}
onParamsChange={(values: AnyKeyProps) => {
// 翻页 & 查询时,滚动到最顶部
window.scrollTo({ behavior: 'smooth', top: 0 })
}}
listHeader={
<div>
{tagVisible && (
<div>
<div className="space-list-search-tags">
<label>订单类型:</label>
<AyTagGroup value={activeTag} onChange={setActiveTag} options={tagOptions} />
</div>
<div className="space-list-search-row">
<Button onClick={() => setTagVisible(false)}>返回</Button>
</div>
</div>
)}
{!searchVisible && (
<div className="space-list-search-row">
<Alert
showIcon
message="说明"
description={
<ul>
<li>1. 只有已取消和已完成的订单可以删除;</li>
<li>
2.
被删除的订单将无法进行评价、晒单和申请售后等操作;如果想继续这些操作,可以先将被删除的订单还原;
</li>
<li>3. 订单被永久删除后无法还原。</li>
</ul>
}
/>
</div>
)}
<Row className="space-list-header">
<Col flex="150px">
<Select
style={{ minWidth: 200 }}
options={yearOptions}
bordered={false}
value={activeYear}
onChange={setActiveYear}
/>
</Col>
<Col flex="1" style={{ paddingLeft: 8 }}>
订单详情
</Col>
<Col span={3} className="space-list-center">
收货人
</Col>
<Col span={3} className="space-list-center">
金额
</Col>
<Col span={3} className="space-list-center">
<Select
style={{ width: '100%' }}
options={statusOptions}
bordered={false}
value={activeStatus}
onChange={setActiveStatus}
/>
</Col>
<Col span={3} className="space-list-center">
操作
</Col>
</Row>
</div>
}
renderItem={(record: Record) => {
// 卡片渲染内容
return (
<List.Item key={record.id}>
<div className="space-list-card">
{record.splitInfo && (
<>
<div className="space-list-card-header light">
<Space size="large">
<Text>2020-05-06 23:59:59</Text>
<span>
<Text type="secondary">订单号:</Text>78074445382
</span>
</Space>
<Text type="secondary">
您订单中的商品在不同库房或属不同商家,故拆分为以下订单分开配送,给您带来的不便敬请谅解。
</Text>
</div>
<div className="space-list-card-header gray">
<Space size="large">
<span>
<Text type="secondary">收货人:</Text>123
</span>
<span>
<Text type="secondary">订单金额:</Text>¥123
</span>
<span>
<Text type="secondary">支付方式:</Text>在线支付
</span>
<span>
<Text type="secondary">订单状态:</Text>已拆分
</span>
</Space>
<AyButton sub>
查看拆分详情
<RightOutlined />
</AyButton>
</div>
</>
)}
{record.groups?.map((group: Record, index: number) => (
<div className="space-list-card-group">
<div
className={classNames('space-list-card-header', !record.splitInfo && index === 0 ? 'light' : '')}
>
<Space size="large">
<Text>2020-05-06 23:59:59</Text>
<span>
<Text type="secondary">订单号:</Text> 78074445382
</span>
<a>
<Space size="small">
<SmileOutlined />
卖橘子的商家
</Space>
</a>
</Space>
<AyButton
className="space-list-card-delete"
confirm
confirmMsg="确定要删除吗?删除后,您可以在订单回收站还原该订单,也可以做永久删除。"
size="small"
icon={<DeleteOutlined />}
/>
</div>
<Row className="space-list-card-info">
<Col span={12} className="space-list-card-left">
{group.goods?.map((goods: Record) => (
<Row key={goods.id} wrap={false} gutter={8} className="space-list-card-goods">
<Col flex="90px">
<Image src={orange} width={80} height={80} />
</Col>
<Col flex="1">
<Paragraph ellipsis={{ rows: 2, tooltip: '好吃的橘子', symbol: '...' }}>
{record.message || '商品名称'}
</Paragraph>
<a>
<Space size="small">
<AppstoreAddOutlined />
找搭配
</Space>
</a>
</Col>
<Col flex="50px" className="space-list-center">
x1
</Col>
<Col flex="100px" className="space-list-center">
<a>申请售后</a>
</Col>
</Row>
))}
</Col>
<Col span={3} className="space-list-center space-list-cell">
<Space>
<Avatar src="购买者头像" />
购买者
</Space>
</Col>
<Col span={3} className="space-list-center space-list-cell">
<div>¥25.55</div>
<div>
<Text type="secondary">在线支付</Text>
</div>
</Col>
<Col span={3} className="space-list-center space-list-cell">
<div>已完成</div>
<div>
<a>订单详情</a>
</div>
</Col>
<Col span={3} className="space-list-center space-list-cell">
<div>
<a>查看发票</a>
</div>
<div>
<a>立即购买</a>
</div>
</Col>
</Row>
</div>
))}
{record.steps?.map((step: Record, index: number) => (
<Row className="space-list-card-footer gray" key={step.id}>
<Col span={15}>
阶段{index + 1}:{step.label}
</Col>
<Col span={3} className="space-list-center">
¥50
</Col>
<Col span={3} className="space-list-center">
已完成
</Col>
<Col span={3} className="space-list-center">
2
</Col>
</Row>
))}
</div>
</List.Item>
)
}}
>
<AyFields>
<AyField key="__search" type="input-group" search={{ position: 'more', hidden: !searchVisible }}>
<AyField
key="keyword"
type="search"
placeholder="商品名称/商品编号/订单号"
enterButton={false}
style={{ width: 300 }}
/>
<AyField
key="__btn"
type="custom"
renderContent={() => (
<Button className="space-list-toggle" onClick={() => setTagVisible(!tagVisible)}>
高级
{tagVisible ? <UpOutlined /> : <DownOutlined />}
</Button>
)}
/>
</AyField>
</AyFields>
</AySearchList>
{/* 页面打印用,实际使用删掉即可 */}
<Divider orientation="left">查询参数</Divider>
{Object.keys(searchValue).length && <pre>{JSON.stringify(searchValue, null, 2)}</pre>}
</div>
)
}
Example #16
Source File: HTTPFlowDetail.tsx From yakit with GNU Affero General Public License v3.0 | 4 votes |
HTTPFlowDetail: React.FC<HTTPFlowDetailProp> = (props) => {
const [flow, setFlow] = useState<HTTPFlow>();
const [loading, setLoading] = useState(false);
const actionFuzzer = [
{
id: 'send-fuzzer-info',
label: '发送到Fuzzer',
contextMenuGroupId: 'send-fuzzer-info',
run: () => {
ipcRenderer.invoke("send-to-tab", {
type: "fuzzer",
data: {
isHttps: flow?.IsHTTPS,
request: Buffer.from(flow?.Request || []).toString("utf8")
}
})
if (props.onClose) props.onClose()
}
},
{
id: 'send-to-plugin',
label: '发送到数据包扫描',
contextMenuGroupId: 'send-fuzzer-info',
run: () => ipcRenderer.invoke("send-to-packet-hack", {
request: flow?.Request,
ishttps: flow?.IsHTTPS,
response: flow?.Response
})
}
]
useEffect(() => {
if (!props.hash) {
return
}
//
// ipcRenderer.on(props.hash, (e: any, data: HTTPFlow) => {
// setFlow(data)
// setTimeout(() => setLoading(false), 300)
// })
// ipcRenderer.on(`ERROR:${props.hash}`, (e: any, details: any) => {
// failed(`查询该请求失败[${props.hash}]: ` + details)
// })
setLoading(true)
ipcRenderer.invoke("GetHTTPFlowByHash", {Hash: props.hash}).then((data: HTTPFlow) => {
setFlow(data)
}).catch(e => {
failed(`GetHTTPFlow ByHash[${props.hash}] failed`)
}).finally(() => setTimeout(() => setLoading(false), 300))
// ipcRenderer.invoke("get-http-flow", props.hash)
return () => {
// ipcRenderer.removeAllListeners(props.hash)
// ipcRenderer.removeAllListeners(`ERROR:${props.hash}`)
}
}, [props.hash])
return <Spin spinning={loading} style={{width: "100%", marginBottom: 24}}>
{flow ? <>
{props.noHeader ? undefined : <PageHeader
title={`请求详情`} subTitle={props.hash}
extra={
props.fetchRequest ?
<Space>
<Tooltip title={"上一个请求"}>
<Button type="link" disabled={!!props.isFront}
icon={<LeftOutlined/>} onClick={() => {
props?.fetchRequest!(1)
}}></Button>
</Tooltip>
<Tooltip title={"下一个请求"}>
<Button type="link" disabled={!!props.isBehind}
icon={<RightOutlined/>} onClick={() => {
props?.fetchRequest!(2)
}}></Button>
</Tooltip>
</Space>
:
<></>
}/>
}
<Space direction={"vertical"} style={{width: "100%"}}>
<Descriptions column={4} bordered={true} size={"small"}>
<Descriptions.Item key={"method"} span={1} label={"HTTP 方法"}><Tag color={"geekblue"}><Text
style={{maxWidth: 500}}>{flow.Method}</Text></Tag></Descriptions.Item>
<Descriptions.Item key={"url"} span={3} label={"请求 URL"}>
<Text style={{maxWidth: 500}} copyable={true}>{flow.Url}</Text>
</Descriptions.Item>
<Descriptions.Item key={"https"} span={1} label={"HTTPS"}><Tag color={"geekblue"}>
<div
style={{maxWidth: 500}}>{flow.IsHTTPS ? "True" : "False"}</div>
</Tag></Descriptions.Item>
<Descriptions.Item key={"status"} span={1} label={"StatusCode"}><Tag
color={"geekblue"}>{flow.StatusCode}</Tag></Descriptions.Item>
<Descriptions.Item key={"size"} span={1} label={"Body大小"}><Tag color={"geekblue"}>
<div style={{maxWidth: 500}}>{flow.BodySizeVerbose}</div>
</Tag></Descriptions.Item>
<Descriptions.Item key={"type"} span={1} label={"Content-Type"}><Tag color={"geekblue"}>
<div style={{maxWidth: 500}}>{flow.ContentType}</div>
</Tag></Descriptions.Item>
</Descriptions>
<div style={{width: "100%", overflow: "auto"}}>
{flow.GetParams.length > 0 || flow.PostParams.length > 0 || flow.CookieParams.length > 0 ? <Tabs>
{flow.GetParams.length > 0 && <Tabs.TabPane key={"get"} tab={"GET 参数"}>
<FuzzableParamList data={flow.GetParams} sendToWebFuzzer={() => {
if (props.onClose) props.onClose()
}}/>
</Tabs.TabPane>}
{flow.PostParams.length > 0 && <Tabs.TabPane key={"post"} tab={"POST 参数"}>
<FuzzableParamList data={flow.PostParams} sendToWebFuzzer={() => {
if (props.onClose) props.onClose()
}}/>
</Tabs.TabPane>}
{flow.CookieParams.length > 0 && <Tabs.TabPane key={"cookie"} tab={"Cookie 参数"}>
<FuzzableParamList data={flow.CookieParams} sendToWebFuzzer={() => {
if (props.onClose) props.onClose()
}}/>
</Tabs.TabPane>}
</Tabs> : ""}
</div>
<Row gutter={8}>
<Col span={12}>
<Card title={"原始 HTTP 请求"} size={"small"} bodyStyle={{padding: 0}}>
<div style={{height: 350}}>
<YakEditor readOnly={true} type={"http"}//theme={"fuzz-http-theme"}
value={new Buffer(flow.Request).toString("utf-8")}
actions={[...actionFuzzer]}/>
</div>
</Card>
</Col>
<Col span={12}>
<Card title={"原始 HTTP 响应"} size={"small"} bodyStyle={{padding: 0}}>
<div style={{height: 350}}>
<YakEditor readOnly={true} type={"http"}// theme={"fuzz-http-theme"}
value={new Buffer(flow.Response).toString("utf-8")}
/>
</div>
</Card>
</Col>
</Row>
{/*<Collapse>*/}
{/* <Collapse.Panel key={"request-raw"} header={"原始 HTTP 请求数据包内容"}>*/}
{/* </Collapse.Panel>*/}
{/* <Collapse.Panel key={"response-raw"} header={"原始 HTTP 响应数据包内容"}>*/}
{/* </Collapse.Panel>*/}
{/*</Collapse>*/}
<Row gutter={8}>
<Col span={12}>
<Collapse defaultActiveKey={"request"}>
<Collapse.Panel key={"request"} header={"Request Headers"}>
<Descriptions bordered={true} column={1} size={"small"}>
{(flow?.RequestHeader || []).sort((i, e) => {
return i.Header.localeCompare(e.Header)
}).map(i => {
return <Descriptions.Item key={i.Header} label={<Text style={{width: 240}}>
<Tag>{i.Header}</Tag>
</Text>}>
<Text
copyable={true}
style={{maxWidth: 500}}
ellipsis={{tooltip: true}}>{i.Value}</Text>
</Descriptions.Item>
})}
</Descriptions>
</Collapse.Panel>
</Collapse>
</Col>
<Col span={12}>
<Collapse defaultActiveKey={"response"}>
<Collapse.Panel key={"response"} header={"Response Headers"}>
<Descriptions bordered={true} column={1} size={"small"}>
{(flow?.ResponseHeader || []).sort((i, e) => {
return i.Header.localeCompare(e.Header)
}).map(i => {
return <Descriptions.Item key={i.Header} label={<Text style={{width: 240}}>
<Tag>{i.Header}</Tag>
</Text>}>
<Text
copyable={true}
style={{maxWidth: 500}}
ellipsis={{tooltip: true}}>{i.Value}</Text>
</Descriptions.Item>
})}
</Descriptions>
</Collapse.Panel>
</Collapse>
</Col>
</Row>
</Space>
</> : ""}
</Spin>
}
Example #17
Source File: HTTPFuzzerPage.tsx From yakit with GNU Affero General Public License v3.0 | 4 votes |
HTTPFuzzerPage: React.FC<HTTPFuzzerPageProp> = (props) => {
// params
const [isHttps, setIsHttps, getIsHttps] = useGetState<boolean>(props.fuzzerParams?.isHttps || props.isHttps || false)
const [noFixContentLength, setNoFixContentLength] = useState(false)
const [request, setRequest, getRequest] = useGetState(props.fuzzerParams?.request || props.request || defaultPostTemplate)
const [concurrent, setConcurrent] = useState(props.fuzzerParams?.concurrent || 20)
const [forceFuzz, setForceFuzz] = useState<boolean>(props.fuzzerParams?.forceFuzz || true)
const [timeout, setParamTimeout] = useState(props.fuzzerParams?.timeout || 10.0)
const [proxy, setProxy] = useState(props.fuzzerParams?.proxy || "")
const [actualHost, setActualHost] = useState(props.fuzzerParams?.actualHost || "")
const [advancedConfig, setAdvancedConfig] = useState(false)
const [redirectedResponse, setRedirectedResponse] = useState<FuzzerResponse>()
const [historyTask, setHistoryTask] = useState<HistoryHTTPFuzzerTask>();
const [hotPatchCode, setHotPatchCode] = useState<string>("");
// filter
const [_, setFilter, getFilter] = useGetState<FuzzResponseFilter>({
Keywords: [],
MaxBodySize: 0,
MinBodySize: 0,
Regexps: [],
StatusCode: []
});
const [droppedCount, setDroppedCount] = useState(0);
// state
const [loading, setLoading] = useState(false)
const [content, setContent] = useState<FuzzerResponse[]>([])
const [reqEditor, setReqEditor] = useState<IMonacoEditor>()
const [fuzzToken, setFuzzToken] = useState("")
const [search, setSearch] = useState("")
const [targetUrl, setTargetUrl] = useState("")
const [refreshTrigger, setRefreshTrigger] = useState(false)
const refreshRequest = () => {
setRefreshTrigger(!refreshTrigger)
}
// history
const [history, setHistory] = useState<string[]>([])
const [currentHistoryIndex, setCurrentHistoryIndex] = useState<number>()
const [urlPacketShow, setUrlPacketShow] = useState<boolean>(false)
// filter
const [keyword, setKeyword] = useState<string>("")
const [filterContent, setFilterContent] = useState<FuzzerResponse[]>([])
const [timer, setTimer] = useState<any>()
useEffect(() => {
getValue(WEB_FUZZ_HOTPATCH_CODE).then((data: any) => {
if (!data) {
return
}
setHotPatchCode(`${data}`)
})
}, [])
// 定时器
const sendTimer = useRef<any>(null)
const withdrawRequest = useMemoizedFn(() => {
const targetIndex = history.indexOf(request) - 1
if (targetIndex >= 0) {
setRequest(history[targetIndex])
setCurrentHistoryIndex(targetIndex)
}
})
const forwardRequest = useMemoizedFn(() => {
const targetIndex = history.indexOf(request) + 1
if (targetIndex < history.length) {
setCurrentHistoryIndex(targetIndex)
setRequest(history[targetIndex])
}
})
const sendToFuzzer = useMemoizedFn((isHttps: boolean, request: string) => {
ipcRenderer.invoke("send-to-tab", {
type: "fuzzer",
data: {isHttps: isHttps, request: request}
})
})
const sendToPlugin = useMemoizedFn((request: Uint8Array, isHTTPS: boolean, response?: Uint8Array) => {
let m = showDrawer({
width: "80%",
content: <HackerPlugin request={request} isHTTPS={isHTTPS} response={response}></HackerPlugin>
})
})
// 从历史记录中恢复
useEffect(() => {
if (!historyTask) {
return
}
setRequest(historyTask.Request)
setIsHttps(historyTask.IsHTTPS)
setProxy(historyTask.Proxy)
refreshRequest()
}, [historyTask])
useEffect(() => {
// 缓存全局参数
getValue(WEB_FUZZ_PROXY).then(e => {
if (!e) {
return
}
setProxy(`${e}`)
})
}, [])
useEffect(() => {
if (currentHistoryIndex === undefined) {
return
}
refreshRequest()
}, [currentHistoryIndex])
useEffect(() => {
setIsHttps(!!props.isHttps)
if (props.request) {
setRequest(props.request)
setContent([])
}
}, [props.isHttps, props.request])
const loadHistory = useMemoizedFn((id: number) => {
setLoading(true)
setHistory([])
ipcRenderer.invoke(
"HTTPFuzzer",
{HistoryWebFuzzerId: id}, fuzzToken
).then(() => {
ipcRenderer.invoke("GetHistoryHTTPFuzzerTask", {Id: id}).then((data: { OriginRequest: HistoryHTTPFuzzerTask }) => {
setHistoryTask(data.OriginRequest)
})
})
})
const submitToHTTPFuzzer = useMemoizedFn(() => {
// 清楚历史任务的标记
setHistoryTask(undefined);
saveValue(WEB_FUZZ_PROXY, proxy)
setLoading(true)
if (history.includes(request)) {
history.splice(history.indexOf(request), 1)
}
history.push(request)
setHistory([...history])
setDroppedCount(0)
ipcRenderer.invoke(
"HTTPFuzzer",
{
Request: request,
ForceFuzz: forceFuzz,
IsHTTPS: isHttps,
Concurrent: concurrent,
PerRequestTimeoutSeconds: timeout,
NoFixContentLength: noFixContentLength,
Proxy: proxy,
ActualAddr: actualHost,
HotPatchCode: hotPatchCode,
Filter: getFilter(),
},
fuzzToken
)
})
const cancelCurrentHTTPFuzzer = useMemoizedFn(() => {
ipcRenderer.invoke("cancel-HTTPFuzzer", fuzzToken)
})
useEffect(() => {
const token = randomString(60)
setFuzzToken(token)
const dataToken = `${token}-data`
const errToken = `${token}-error`
const endToken = `${token}-end`
ipcRenderer.on(errToken, (e, details) => {
notification["error"]({
message: `提交模糊测试请求失败 ${details}`,
placement: "bottomRight"
})
})
let buffer: FuzzerResponse[] = []
let droppedCount = 0;
let count: number = 0
const updateData = () => {
if (buffer.length <= 0) {
return
}
if (JSON.stringify(buffer) !== JSON.stringify(content)) setContent([...buffer])
}
ipcRenderer.on(dataToken, (e: any, data: any) => {
if (data["MatchedByFilter"] !== true && !filterIsEmpty(getFilter())) {
// 不匹配的
droppedCount++
setDroppedCount(droppedCount)
return
}
// const response = new Buffer(data.ResponseRaw).toString(fixEncoding(data.GuessResponseEncoding))
buffer.push({
StatusCode: data.StatusCode,
Ok: data.Ok,
Reason: data.Reason,
Method: data.Method,
Host: data.Host,
ContentType: data.ContentType,
Headers: (data.Headers || []).map((i: any) => {
return {Header: i.Header, Value: i.Value}
}),
DurationMs: data.DurationMs,
BodyLength: data.BodyLength,
UUID: data.UUID,
Timestamp: data.Timestamp,
ResponseRaw: data.ResponseRaw,
RequestRaw: data.RequestRaw,
Payloads: data.Payloads,
IsHTTPS: data.IsHTTPS,
Count: count,
BodySimilarity: data.BodySimilarity,
HeaderSimilarity: data.HeaderSimilarity,
} as FuzzerResponse)
count++
// setContent([...buffer])
})
ipcRenderer.on(endToken, () => {
updateData()
buffer = []
count = 0
droppedCount = 0
setLoading(false)
})
const updateDataId = setInterval(() => {
updateData()
}, 200)
return () => {
ipcRenderer.invoke("cancel-HTTPFuzzer", token)
clearInterval(updateDataId)
ipcRenderer.removeAllListeners(errToken)
ipcRenderer.removeAllListeners(dataToken)
ipcRenderer.removeAllListeners(endToken)
}
}, [])
const searchContent = (keyword: string) => {
if (timer) {
clearTimeout(timer)
setTimer(null)
}
setTimer(
setTimeout(() => {
try {
const filters = content.filter((item) => {
return Buffer.from(item.ResponseRaw).toString("utf8").match(new RegExp(keyword, "g"))
})
setFilterContent(filters)
} catch (error) {
}
}, 500)
)
}
const downloadContent = useMemoizedFn(() => {
if (!keyword) {
failed('请先输入需要搜索的关键词')
return
}
const strs = []
try {
const reg = new RegExp(keyword)
for (let info of filterContent) {
let str = Buffer.from(info.ResponseRaw).toString('utf8')
let temp: any
while ((temp = reg.exec(str)) !== null) {
// @ts-ignore
if (temp[1]) {
// @ts-ignore
strs.push(temp[1])
str = str.substring(temp['index'] + 1)
reg.lastIndex = 0
} else {
break
}
}
}
} catch (error) {
failed("正则有问题,请检查后重新输入")
return
}
if (strs.length === 0) {
failed('未捕获到关键词信息')
return
}
ipcRenderer.invoke("show-save-dialog", 'fuzzer列表命中内容.txt').then((res) => {
if (res.canceled) return
ipcRenderer
.invoke("write-file", {
route: res.filePath,
data: strs.join('\n\r')
})
.then(() => {
success('下载完成')
ipcRenderer.invoke("open-specified-file", res.filePath)
})
})
})
useEffect(() => {
if (!!keyword) {
searchContent(keyword)
} else {
setFilterContent([])
}
}, [keyword])
useEffect(() => {
if (keyword && content.length !== 0) {
const filters = content.filter(item => {
return Buffer.from(item.ResponseRaw).toString("utf8").match(new RegExp(keyword, 'g'))
})
setFilterContent(filters)
}
}, [content])
const onlyOneResponse = !loading && (content || []).length === 1
const filtredResponses =
search === ""
? content || []
: (content || []).filter((i) => {
return Buffer.from(i.ResponseRaw).toString().includes(search)
})
const successResults = filtredResponses.filter((i) => i.Ok)
const failedResults = filtredResponses.filter((i) => !i.Ok)
const sendFuzzerSettingInfo = useMemoizedFn(() => {
const info: fuzzerInfoProp = {
time: new Date().getTime().toString(),
isHttps: isHttps,
forceFuzz: forceFuzz,
concurrent: concurrent,
proxy: proxy,
actualHost: actualHost,
timeout: timeout,
request: request
}
if (sendTimer.current) {
clearTimeout(sendTimer.current)
sendTimer.current = null
}
sendTimer.current = setTimeout(() => {
ipcRenderer.invoke('send-fuzzer-setting-data', {key: props.order || "", param: JSON.stringify(info)})
}, 1000);
})
useEffect(() => {
sendFuzzerSettingInfo()
}, [isHttps, forceFuzz, concurrent, proxy, actualHost, timeout, request])
const responseViewer = useMemoizedFn((rsp: FuzzerResponse) => {
return (
<HTTPPacketEditor
system={props.system}
originValue={rsp.ResponseRaw}
bordered={true}
hideSearch={true}
emptyOr={
!rsp?.Ok && (
<Result
status={"error"}
title={"请求失败"}
// no such host
subTitle={(() => {
const reason = content[0]!.Reason
if (reason.includes("tcp: i/o timeout")) {
return "网络超时"
}
if (reason.includes("no such host")) {
return "DNS 错误或主机错误"
}
return undefined
})()}
>
<>详细原因:{rsp.Reason}</>
</Result>
)
}
readOnly={true}
extra={
(
<Space>
{loading && <Spin size={"small"} spinning={loading}/>}
{onlyOneResponse ? (
<Space>
{content[0].IsHTTPS && <Tag>{content[0].IsHTTPS ? "https" : ""}</Tag>}
<Tag>{content[0].DurationMs}ms</Tag>
<Space key='single'>
<Button
size={"small"}
onClick={() => {
analyzeFuzzerResponse(
rsp,
(bool, r) => {
// setRequest(r)
// refreshRequest()
}
)
}}
type={"primary"}
icon={<ProfileOutlined/>}
>
详情
</Button>
<Button
type={"primary"}
size={"small"}
onClick={() => {
setContent([])
}}
danger={true}
icon={<DeleteOutlined/>}
/>
</Space>
</Space>
) : (
<Space key='list'>
<Tag color={"green"}>成功:{successResults.length}</Tag>
<Input
size={"small"}
value={search}
onChange={(e) => {
setSearch(e.target.value)
}}
/>
{/*<Tag>当前请求结果数[{(content || []).length}]</Tag>*/}
<Button
size={"small"}
onClick={() => {
setContent([])
}}
>
清除数据
</Button>
</Space>
)}
</Space>
)
}
/>
)
})
const hotPatchTrigger = useMemoizedFn(() => {
let m = showModal({
title: "调试 / 插入热加载代码",
width: "60%",
content: (
<div>
<HTTPFuzzerHotPatch initialHotPatchCode={hotPatchCode || ""} onInsert={tag => {
if (reqEditor) monacoEditorWrite(reqEditor, tag);
m.destroy()
}} onSaveCode={code => {
setHotPatchCode(code)
saveValue(WEB_FUZZ_HOTPATCH_CODE, code)
}}/>
</div>
)
})
})
return (
<div style={{height: "100%", width: "100%", display: "flex", flexDirection: "column", overflow: "hidden"}}>
<Row gutter={8} style={{marginBottom: 8}}>
<Col span={24} style={{textAlign: "left", marginTop: 4}}>
<Space>
{loading ? (
<Button
style={{width: 150}}
onClick={() => {
cancelCurrentHTTPFuzzer()
}}
// size={"small"}
danger={true}
type={"primary"}
>
强制停止
</Button>
) : (
<Button
style={{width: 150}}
onClick={() => {
setContent([])
setRedirectedResponse(undefined)
sendFuzzerSettingInfo()
submitToHTTPFuzzer()
}}
// size={"small"}
type={"primary"}
>
发送数据包
</Button>
)}
<Space>
<Button
onClick={() => {
withdrawRequest()
}}
type={"link"}
icon={<LeftOutlined/>}
/>
<Button
onClick={() => {
forwardRequest()
}}
type={"link"}
icon={<RightOutlined/>}
/>
{history.length > 1 && (
<Dropdown
trigger={["click"]}
overlay={() => {
return (
<Menu>
{history.map((i, index) => {
return (
<Menu.Item
style={{width: 120}}
onClick={() => {
setRequest(i)
setCurrentHistoryIndex(index)
}}
>{`${index}`}</Menu.Item>
)
})}
</Menu>
)
}}
>
<Button size={"small"} type={"link"} onClick={(e) => e.preventDefault()}>
History <DownOutlined/>
</Button>
</Dropdown>
)}
</Space>
<Checkbox defaultChecked={isHttps} value={isHttps} onChange={() => setIsHttps(!isHttps)}>强制
HTTPS</Checkbox>
<SwitchItem
label={"高级配置"}
formItemStyle={{marginBottom: 0}}
value={advancedConfig}
setValue={setAdvancedConfig}
size={"small"}
/>
{droppedCount > 0 && <Tag color={"red"}>已丢弃[{droppedCount}]个响应</Tag>}
{onlyOneResponse && content[0].Ok && (
<Form.Item style={{marginBottom: 0}}>
<Button
onClick={() => {
setLoading(true)
ipcRenderer
.invoke("RedirectRequest", {
Request: request,
Response: new Buffer(content[0].ResponseRaw).toString("utf8"),
IsHttps: isHttps,
PerRequestTimeoutSeconds: timeout,
NoFixContentLength: noFixContentLength,
Proxy: proxy
})
.then((rsp: FuzzerResponse) => {
setRedirectedResponse(rsp)
})
.catch((e) => {
failed(`"ERROR in: ${e}"`)
})
.finally(() => {
setTimeout(() => setLoading(false), 300)
})
}}
>
跟随重定向
</Button>
</Form.Item>
)}
{loading && (
<Space>
<Spin size={"small"}/>
<div style={{color: "#3a8be3"}}>sending packets</div>
</Space>
)}
{proxy && <Tag>代理:{proxy}</Tag>}
{/*<Popover*/}
{/* trigger={"click"}*/}
{/* content={*/}
{/* }*/}
{/*>*/}
{/* <Button type={"link"} size={"small"}>*/}
{/* 配置请求包*/}
{/* </Button>*/}
{/*</Popover>*/}
{actualHost !== "" && <Tag color={"red"}>请求 Host:{actualHost}</Tag>}
</Space>
</Col>
{/*<Col span={12} style={{textAlign: "left"}}>*/}
{/*</Col>*/}
</Row>
{advancedConfig && (
<Row style={{marginBottom: 8}} gutter={8}>
<Col span={16}>
{/*高级配置*/}
<Card bordered={true} size={"small"} bodyStyle={{height: 106}}>
<Spin style={{width: "100%"}} spinning={!reqEditor}>
<Form
onSubmitCapture={(e) => e.preventDefault()}
// layout={"horizontal"}
size={"small"}
// labelCol={{span: 8}}
// wrapperCol={{span: 16}}
>
<Row gutter={8}>
<Col span={12} xl={8}>
<Form.Item
label={<OneLine width={68}>Intruder</OneLine>}
style={{marginBottom: 4}}
>
<Button
style={{backgroundColor: "#08a701"}}
size={"small"}
type={"primary"}
onClick={() => {
const m = showModal({
width: "70%",
content: (
<>
<StringFuzzer
advanced={true}
disableBasicMode={true}
insertCallback={(template: string) => {
if (!template) {
Modal.warn({
title: "Payload 为空 / Fuzz 模版为空"
})
} else {
if (reqEditor && template) {
reqEditor.trigger(
"keyboard",
"type",
{
text: template
}
)
} else {
Modal.error({
title: "BUG: 编辑器失效"
})
}
m.destroy()
}
}}
/>
</>
)
})
}}
>
插入 yak.fuzz 语法
</Button>
</Form.Item>
</Col>
<Col span={12} xl={8}>
<SwitchItem
label={<OneLine width={68}>渲染 fuzz</OneLine>}
setValue={(e) => {
if (!e) {
Modal.confirm({
title: "确认关闭 Fuzz 功能吗?关闭之后,所有的 Fuzz 标签将会失效",
onOk: () => {
setForceFuzz(e)
}
})
return
}
setForceFuzz(e)
}}
size={"small"}
value={forceFuzz}
formItemStyle={{marginBottom: 4}}
/>
</Col>
<Col span={12} xl={8}>
<InputInteger
label={<OneLine width={68}>并发线程</OneLine>}
size={"small"}
setValue={(e) => {
setConcurrent(e)
}}
formItemStyle={{marginBottom: 4}} // width={40}
width={50}
value={concurrent}
/>
</Col>
<Col span={12} xl={8}>
<SwitchItem
label={<OneLine width={68}>HTTPS</OneLine>}
setValue={(e) => {
setIsHttps(e)
}}
size={"small"}
value={isHttps}
formItemStyle={{marginBottom: 4}}
/>
</Col>
<Col span={12} xl={8}>
<SwitchItem
label={<OneLine width={70}>
<Tooltip title={"不修复 Content-Length: 常用发送多个数据包"}>
不修复长度
</Tooltip>
</OneLine>}
setValue={(e) => {
setNoFixContentLength(e)
}}
size={"small"}
value={noFixContentLength}
formItemStyle={{marginBottom: 4}}
/>
</Col>
<Col span={12} xl={8}>
<ItemSelects
item={{
style: {marginBottom: 4},
label: <OneLine width={68}>设置代理</OneLine>,
}}
select={{
style: {width: "100%"},
allowClear: true,
autoClearSearchValue: true,
maxTagTextLength: 8,
mode: "tags",
data: [
{text: "http://127.0.0.1:7890", value: "http://127.0.0.1:7890"},
{text: "http://127.0.0.1:8080", value: "http://127.0.0.1:8080"},
{text: "http://127.0.0.1:8082", value: "http://127.0.0.1:8082"}
],
value: proxy ? proxy.split(",") : [],
setValue: (value) => setProxy(value.join(",")),
maxTagCount: "responsive",
}}
></ItemSelects>
{/* <ManyMultiSelectForString
formItemStyle={{marginBottom: 4}}
label={<OneLine width={68}>设置代理</OneLine>}
data={[
"http://127.0.0.1:7890",
"http://127.0.0.1:8080",
"http://127.0.0.1:8082"
].map((i) => {
return {label: i, value: i}
})}
mode={"tags"}
defaultSep={","}
value={proxy}
setValue={(r) => {
setProxy(r.split(",").join(","))
}}
/> */}
</Col>
<Col span={12} xl={8}>
<InputItem
extraFormItemProps={{
style: {marginBottom: 0}
}}
label={<OneLine width={68}>请求 Host</OneLine>}
setValue={setActualHost}
value={actualHost}
/>
</Col>
<Col span={12} xl={8}>
<InputFloat
formItemStyle={{marginBottom: 4}}
size={"small"}
label={<OneLine width={68}>超时时间</OneLine>}
setValue={setParamTimeout}
value={timeout}
/>
</Col>
</Row>
</Form>
</Spin>
</Card>
</Col>
<Col span={8}>
<AutoCard title={<Tooltip title={"通过过滤匹配,丢弃无用数据包,保证界面性能!"}>
设置过滤器
</Tooltip>}
bordered={false} size={"small"} bodyStyle={{paddingTop: 4}}
style={{marginTop: 0, paddingTop: 0}}
>
<Form size={"small"} onSubmitCapture={e => e.preventDefault()}>
<Row gutter={20}>
<Col span={12}>
<InputItem
label={"状态码"} placeholder={"200,300-399"}
disable={loading}
value={getFilter().StatusCode.join(",")}
setValue={e => {
setFilter({...getFilter(), StatusCode: e.split(",").filter(i => !!i)})
}}
extraFormItemProps={{style: {marginBottom: 0}}}
/>
</Col>
<Col span={12}>
<InputItem
label={"关键字"} placeholder={"Login,登录成功"}
value={getFilter().Keywords.join(",")}
disable={loading}
setValue={e => {
setFilter({...getFilter(), Keywords: e.split(",").filter(i => !!i)})
}}
extraFormItemProps={{style: {marginBottom: 0}}}
/>
</Col>
<Col span={12}>
<InputItem
label={"正则"} placeholder={`Welcome\\s+\\w+!`}
value={getFilter().Regexps.join(",")}
disable={loading}
setValue={e => {
setFilter({...getFilter(), Regexps: e.split(",").filter(i => !!i)})
}}
extraFormItemProps={{style: {marginBottom: 0, marginTop: 2}}}
/>
</Col>
</Row>
</Form>
</AutoCard>
</Col>
</Row>
)}
{/*<Divider style={{marginTop: 6, marginBottom: 8, paddingTop: 0}}/>*/}
<ResizeBox
firstMinSize={350} secondMinSize={360}
style={{overflow: "hidden"}}
firstNode={<HTTPPacketEditor
system={props.system}
refreshTrigger={refreshTrigger}
hideSearch={true}
bordered={true}
originValue={new Buffer(request)}
actions={[
{
id: "packet-from-url",
label: "URL转数据包",
contextMenuGroupId: "1_urlPacket",
run: () => {
setUrlPacketShow(true)
}
},
{
id: "copy-as-url",
label: "复制为 URL",
contextMenuGroupId: "1_urlPacket",
run: () => {
copyAsUrl({Request: getRequest(), IsHTTPS: getIsHttps()})
}
},
{
id: "insert-intruder-tag",
label: "插入模糊测试字典标签",
contextMenuGroupId: "1_urlPacket",
run: (editor) => {
showDictsAndSelect(i => {
monacoEditorWrite(editor, i, editor.getSelection())
})
}
},
{
id: "insert-hotpatch-tag",
label: "插入热加载标签",
contextMenuGroupId: "1_urlPacket",
run: (editor) => {
hotPatchTrigger()
}
},
]}
onEditor={setReqEditor}
onChange={(i) => setRequest(new Buffer(i).toString("utf8"))}
extra={
<Space size={2}>
<Button
style={{marginRight: 1}}
size={"small"} type={"primary"}
onClick={() => {
hotPatchTrigger()
}}
>热加载标签</Button>
<Popover
trigger={"click"}
title={"从 URL 加载数据包"}
content={
<div style={{width: 400}}>
<Form
layout={"vertical"}
onSubmitCapture={(e) => {
e.preventDefault()
ipcRenderer
.invoke("Codec", {
Type: "packet-from-url",
Text: targetUrl
})
.then((e) => {
if (e?.Result) {
setRequest(e.Result)
refreshRequest()
}
})
.finally(() => {
})
}}
size={"small"}
>
<InputItem
label={"从 URL 构造请求"}
value={targetUrl}
setValue={setTargetUrl}
extraFormItemProps={{style: {marginBottom: 8}}}
></InputItem>
<Form.Item style={{marginBottom: 8}}>
<Button type={"primary"} htmlType={"submit"}>
构造请求
</Button>
</Form.Item>
</Form>
</div>
}
>
<Button size={"small"} type={"primary"}>
URL
</Button>
</Popover>
<Popover
trigger={"click"}
placement={"bottom"}
destroyTooltipOnHide={true}
content={
<div style={{width: 400}}>
<HTTPFuzzerHistorySelector onSelect={e => {
loadHistory(e)
}}/>
</div>
}
>
<Button size={"small"} type={"primary"} icon={<HistoryOutlined/>}>
历史
</Button>
</Popover>
</Space>
}
/>}
secondNode={<AutoSpin spinning={false}>
{onlyOneResponse ? (
<>{redirectedResponse ? responseViewer(redirectedResponse) : responseViewer(content[0])}</>
) : (
<>
{(content || []).length > 0 ? (
<HTTPFuzzerResultsCard
onSendToWebFuzzer={sendToFuzzer}
sendToPlugin={sendToPlugin}
setRequest={(r) => {
setRequest(r)
refreshRequest()
}}
extra={
<div>
<Input
value={keyword}
style={{maxWidth: 200}}
allowClear
placeholder="输入字符串或正则表达式"
onChange={e => setKeyword(e.target.value)}
addonAfter={
<DownloadOutlined style={{cursor: "pointer"}}
onClick={downloadContent}/>
}></Input>
</div>
}
failedResponses={failedResults}
successResponses={filterContent.length !== 0 ? filterContent : keyword ? [] : successResults}
/>
) : (
<Result
status={"info"}
title={"请在左边编辑并发送一个 HTTP 请求/模糊测试"}
subTitle={
"本栏结果针对模糊测试的多个 HTTP 请求结果展示做了优化,可以自动识别单个/多个请求的展示"
}
/>
)}
</>
)}
</AutoSpin>}/>
<Modal
visible={urlPacketShow}
title='从 URL 加载数据包'
onCancel={() => setUrlPacketShow(false)}
footer={null}
>
<Form
layout={"vertical"}
onSubmitCapture={(e) => {
e.preventDefault()
ipcRenderer
.invoke("Codec", {
Type: "packet-from-url",
Text: targetUrl
})
.then((e) => {
if (e?.Result) {
setRequest(e.Result)
refreshRequest()
setUrlPacketShow(false)
}
})
.finally(() => {
})
}}
size={"small"}
>
<InputItem
label={"从 URL 构造请求"}
value={targetUrl}
setValue={setTargetUrl}
extraFormItemProps={{style: {marginBottom: 8}}}
></InputItem>
<Form.Item style={{marginBottom: 8}}>
<Button type={"primary"} htmlType={"submit"}>
构造请求
</Button>
</Form.Item>
</Form>
</Modal>
</div>
)
}
Example #18
Source File: SelectBox.tsx From posthog-foss with MIT License | 4 votes |
export function SelectUnit({
dropdownLogic,
items,
disablePopover = false,
}: {
dropdownLogic: selectBoxLogicType & BuiltLogic
items: Record<string, SelectBoxItem>
disablePopover?: boolean // Disable PropertyKeyInfo popover
}): JSX.Element {
const { setSelectedItem, clickSelectedItem } = useActions(dropdownLogic)
const { selectedItem, search, blockMouseOver } = useValues(dropdownLogic)
const [hiddenData, setHiddenData] = useState<Record<string, SelectedItem[]>>({})
const [data, setData] = useState<Record<string, SelectedItem[]>>({})
const [flattenedData, setFlattenedData] = useState<SelectedItem[]>([])
const [groupTypes, setGroupTypes] = useState<string[]>([])
let lengthOfData = 0
Object.values(items).forEach((entry) => {
lengthOfData += entry?.dataSource?.length || 0
})
useEffect(() => {
const formattedData: Record<string, SelectedItem[]> = {}
const _hiddenData: Record<string, SelectedItem[]> = {}
const _groupTypes: string[] = []
const currHidden = Object.keys(hiddenData)
Object.keys(items).forEach((groupName) => {
if (!currHidden.includes(groupName)) {
formattedData[groupName] = items[groupName].dataSource
} else {
formattedData[groupName] = []
_hiddenData[groupName] = items[groupName].dataSource
}
_groupTypes.push(groupName)
})
setGroupTypes(_groupTypes)
setData(formattedData)
setHiddenData(_hiddenData)
}, [lengthOfData])
useEffect(() => {
const _flattenedData: SelectedItem[] = []
Object.keys(data).forEach((key) => {
_flattenedData.push({
key: key,
name: key,
groupName: key,
})
_flattenedData.push(...data[key].map((selectItem) => ({ ...selectItem, groupName: key })))
})
setFlattenedData(_flattenedData)
}, [data])
const hideKey = (key: string): void => {
const { [`${key}`]: hideItem } = data
const copy = {
...data,
}
copy[key] = []
setData(copy)
setHiddenData({
...hiddenData,
[`${key}`]: hideItem,
})
}
const showKey = (key: string): void => {
const { [`${key}`]: showItem, ...restOfData } = hiddenData
setHiddenData(restOfData)
const copy = {
...data,
}
copy[key] = showItem
setData(copy)
}
const renderItem: ListRowRenderer = ({ index, style }: ListRowProps) => {
const item = flattenedData[index]
if (!item.groupName) {
return null
}
const group = items[item.groupName]
if (groupTypes.includes(item.key)) {
return (
<div style={style} key={item.key}>
<span onClick={() => (hiddenData?.[item.key] ? showKey(item.key) : hideKey(item.key))}>
<h4 style={{ cursor: 'pointer', userSelect: 'none', padding: '4px 12px', marginBottom: 0 }}>
{hiddenData?.[item.key] || !data[item.key].length ? <RightOutlined /> : <DownOutlined />}{' '}
{group.header(item.key)}
<span
style={{
float: 'right',
fontWeight: search && flattenedData.length > 0 ? 700 : 'normal',
}}
className="text-small"
>
{data?.[item.key]?.length || hiddenData?.[item.key]?.length || '0'}{' '}
{flattenedData.length === 1 ? 'entry' : 'entries'}
</span>
</h4>
</span>
</div>
)
} else {
return (
<List.Item
className={selectedItem?.key === item.key ? 'selected' : undefined}
key={item.key}
onClick={() => clickSelectedItem(item, group)}
style={style}
onMouseOver={() =>
!blockMouseOver && setSelectedItem({ ...item, key: item.key, category: group.type })
}
data-attr={`select-item-${index}`}
>
<PropertyKeyInfo value={item.name} disablePopover={disablePopover} />
</List.Item>
)
}
}
return (
<>
{flattenedData.length > 0 && (
<div style={{ height: '100%' }}>
{
<AutoSizer>
{({ height, width }: { height: number; width: number }) => {
return (
<VirtualizedList
height={height}
overscanRowCount={0}
rowCount={flattenedData.length}
rowHeight={35}
rowRenderer={renderItem}
width={width}
tabIndex={-1}
/>
)
}}
</AutoSizer>
}
</div>
)}
</>
)
}
Example #19
Source File: ShareTicketModal.tsx From condo with MIT License | 4 votes |
ShareTicketModal: React.FC<IShareTicketModalProps> = (props) => {
const intl = useIntl()
const SendTicketToEmailMessage = intl.formatMessage({ id: 'SendTicketToEmail' })
const ToEmployeesEmailMessage = intl.formatMessage({ id: 'ToEmployeesEmail' })
const EmployeesNameMessage = intl.formatMessage({ id: 'EmployeesName' })
const ServerErrorMessage = intl.formatMessage({ id: 'ServerError' })
const WhatsappMessage = intl.formatMessage({ id: 'WhatsApp' })
const TelegramMessage = intl.formatMessage({ id: 'Telegram' })
const ShareHeaderMessage = intl.formatMessage({ id: 'ticket.shareHeader' })
const ShareButtonMessage = intl.formatMessage({ id: 'ticket.shareButton' })
const OKMessage = intl.formatMessage({ id: 'OK' })
const ShareSentMessage = intl.formatMessage({ id: 'ticket.shareSent' })
const ShareSentToEmailMessage = intl.formatMessage({ id: 'ticket.shareSentToEmail' })
const { isSmall } = useLayoutContext()
const { date, number, details, id, locale, organization } = props
const cipher = crypto.createCipher(ALGORITHM, SALT)
let cutDetails = details || ''
if (cutDetails.length >= 110) {
cutDetails = `${cutDetails.substr(0, 100)}…`
}
const stringifiedParams = JSON.stringify({ date, number, details: cutDetails, id })
const encryptedText = cipher.update(stringifiedParams, 'utf8', CRYPTOENCODING) + cipher.final(CRYPTOENCODING)
const { query } = useRouter()
const [shareTicket] = useMutation(SHARE_TICKET_MUTATION)
const {
publicRuntimeConfig: { serverUrl: origin },
} = getConfig()
const [chosenEmployees, setChosenEmployees] = useState([])
const [loading, setLoading] = useState(false)
const [shareVisible, setShareVisible] = useState(false)
const [okVisible, setOkVisible] = useState(false)
const [usersWithoutEmail, setUsersWithoutEmail] = useState([])
const parseSelectValue = (selectedEmployees) => {
try {
return selectedEmployees.map(JSON.parse)
} catch (error) {
console.error('Invalid format for employees in multiple select', selectedEmployees)
}
}
function handleSelect (value) {
const withoutEmails = parseSelectValue(value).filter(item => !get(item, 'value.hasEmail')).map(item => item.text)
setUsersWithoutEmail(withoutEmails)
setChosenEmployees(value)
}
async function handleClick () {
setLoading(true)
const sender = getClientSideSenderInfo()
const { data, error } = await shareTicket({
variables: {
data: {
sender,
employees: parseSelectValue(chosenEmployees).filter(employee => get(employee, 'value.hasEmail')).map(employee => employee.id),
ticketId: query.id,
},
},
})
if (data && data.obj) {
setChosenEmployees([])
setShareVisible(false)
setOkVisible(true)
setUsersWithoutEmail([])
}
if (error) {
console.error(error)
notification.error({
message: ServerErrorMessage,
description: error.message,
})
}
setLoading(false)
}
function handleCancel () {
setShareVisible(false)
}
function handleShow () {
setShareVisible(true)
}
function handleClickSecond () {
setOkVisible(false)
}
return (
<>
<Button
type={'sberDefaultGradient'}
icon={<ShareAltOutlined />}
secondary
onClick={handleShow}
css={sendButton}
>
{ShareButtonMessage}
</Button>
<Modal
style={{ top: 30 }}
visible={okVisible}
footer={<Button
type='sberPrimary'
size='large'
onClick={handleClickSecond}
>
{OKMessage}
</Button>}
onCancel={handleCancel}
title={ShareSentMessage}
>
{ShareSentToEmailMessage}
</Modal>
<Modal
style={{ top: 30 }}
visible={shareVisible}
footer={null}
onCancel={handleCancel}
title={<Typography.Title level={isSmall ? 5 : 3}>{ShareHeaderMessage}</Typography.Title>}
>
<Row gutter={[0, 16]}>
<Col span={24}>
<a
target='_blank'
rel='noreferrer'
href={`https://wa.me/?text=${encodeURIComponent(`${origin}/share?q=${encryptedText}&locale=${locale || EN_LOCALE}`)}`}
>
<ShareButton>
{WhatsappMessage}
<RightOutlined />
</ShareButton>
</a>
</Col>
<Col span={24}>
<a
target='_blank'
rel='noreferrer'
href={`https://t.me/share/url?url=${encodeURIComponent(`${origin}/share?q=${encryptedText}&locale=${locale || EN_LOCALE}`)}`}
>
<ShareButton>
{TelegramMessage}
<RightOutlined />
</ShareButton>
</a>
</Col>
<Col span={24}>
<Collapse expandIconPosition='right' css={collapse}>
<Collapse.Panel key='1' header={ToEmployeesEmailMessage}>
<GraphQlSearchInput
search={getEmployeeWithEmail(get(organization, 'id'))}
showArrow={false}
mode='multiple'
css={search}
onChange={handleSelect}
value={chosenEmployees}
placeholder={EmployeesNameMessage}
autoClearSearchValue={true}
/>
{
!isEmpty(usersWithoutEmail) &&
<Warning>
{usersWithoutEmail}
</Warning>
}
{
!isEmpty(chosenEmployees) &&
<Button
type='sberPrimary'
size='large'
onClick={handleClick}
style={{ marginTop: '20px' }}
disabled={loading}
>
{SendTicketToEmailMessage}
</Button>
}
</Collapse.Panel>
</Collapse>
</Col>
</Row>
</Modal>
</>
)
}
Example #20
Source File: Row.tsx From fe-v5 with Apache License 2.0 | 4 votes |
export default function Row(props: IProps) {
const { name, row, onToggle, onAddClick, onEditClick, onDeleteClick } = props;
const [editVisble, setEditVisble] = useState(false);
const [newName, setNewName] = useState<string>();
const [deleteVisible, setDeleteVisible] = useState(false);
return (
<div className='dashboards-panels-row'>
<div
className='dashboards-panels-row-name'
onClick={() => {
onToggle();
}}
>
{row.collapsed ? <DownOutlined /> : <RightOutlined />}
<span style={{ paddingLeft: 6 }}>{name}</span>
</div>
<Space>
<AddPanelIcon
onClick={() => {
onAddClick();
}}
/>
<SettingOutlined
onClick={() => {
setEditVisble(true);
}}
/>
<DeleteOutlined
onClick={() => {
setDeleteVisible(true);
}}
/>
{row.collapsed === false && <HolderOutlined className='dashboards-panels-item-drag-handle' />}
</Space>
<Modal
title='编辑分组'
visible={editVisble}
onCancel={() => {
setEditVisble(false);
}}
onOk={() => {
onEditClick({
...row,
name: newName,
});
setEditVisble(false);
}}
>
<div>
分组名称
<Input
value={newName}
onChange={(e) => {
setNewName(e.target.value);
}}
onPressEnter={() => {
onEditClick({
...row,
name: newName,
});
setEditVisble(false);
}}
/>
</div>
</Modal>
<Modal
closable={false}
visible={deleteVisible}
onCancel={() => {
setDeleteVisible(false);
}}
footer={[
<Button
key='cancel'
onClick={() => {
setDeleteVisible(false);
}}
>
取消
</Button>,
<Button
key='ok'
type='primary'
onClick={() => {
onDeleteClick('self');
setDeleteVisible(false);
}}
>
仅删除分组
</Button>,
<Button
key='all'
type='primary'
danger
onClick={() => {
onDeleteClick('withPanels');
setDeleteVisible(false);
}}
>
删除分组和图表
</Button>,
]}
>
<div>
<h3 style={{ fontSize: 16 }}>
<InfoCircleOutlined style={{ color: '#faad14', marginRight: 10, fontSize: 22, position: 'relative', top: 4 }} /> 删除分组
</h3>
<div style={{ marginLeft: 38, fontSize: 14 }}>确定删除该分组吗?</div>
</div>
</Modal>
</div>
);
}
Example #21
Source File: GeneralSignup.tsx From next-basics with GNU General Public License v3.0 | 4 votes |
export function GeneralSignup(props: GeneralSignupProps): React.ReactElement {
const [form] = Form.useForm();
const runtime = getRuntime();
const brand = runtime.getBrandSettings();
const enabledFeatures = runtime.getFeatureFlags();
const { t } = useTranslation(NS_GENERAL_AUTH);
const [, setForceUpdate] = useState<any>();
const passwordConfigMap = {
default: {
regex: /^.{6,20}$/,
description: "请输入6至20位密码",
},
strong: {
regex: /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[^a-zA-Z0-9]).{8,20}$/,
description: "请输入8至20位密码,且同时包含大小写字母、数字、特殊字符",
},
backend: {},
};
let passwordLevel: keyof typeof passwordConfigMap = "default"; //特性开关
useEffect(() => {
if (enabledFeatures["enable-backend-password-config"]) {
(async () => {
passwordLevel = "backend";
passwordConfigMap[passwordLevel] =
await UserAdminApi_getPasswordConfig();
})();
}
}, []);
const MIN_USERNAME_LENGTH = 3; //特性开关
const MAX_USERNAME_LENGTH = 32; //特性开关
const usernamePattern = new RegExp(
`^[A-Za-z0-9][A-Za-z0-9|_\\-\\.]{${MIN_USERNAME_LENGTH - 1},${
MAX_USERNAME_LENGTH - 1
}}$`
);
const iniviteCodePattern = /^[0-9a-zA-Z]{9}$/;
const hideInvite = iniviteCodePattern.test(getInviteCode());
const [isCommonSignup, setIsCommonSignup] = useState(true);
const [isTermsVisible, setIsTermsVisible] = useState(false);
function showTerms(): void {
setIsTermsVisible(true);
}
function hideTerms(): void {
setIsTermsVisible(false);
}
function agreeTerms(): void {
form.setFieldsValue({
terms: true,
});
hideTerms();
}
function disagreeTerms(): void {
form.setFieldsValue({
terms: false,
});
hideTerms();
}
const [imageHeight, setImageHeight] = useState(window.innerHeight);
const onWindowResized = () => {
if (imageHeight < window.innerHeight) {
setImageHeight(window.innerHeight);
}
};
useEffect(() => {
const handleWindowResized = debounce(onWindowResized, 500, {
leading: false,
});
window.addEventListener("resize", handleWindowResized);
return () => {
window.removeEventListener("resize", handleWindowResized);
};
}, []);
const timer = useRef<any>();
const count = useRef<number>(duration);
const [verifyBtnDisabled, setVerifyBtnDisabled] = useState(true);
const [content, setContent] = useState(t(K.GET_VERIFY_CODE));
const [messageId, setMessageId] = useState("");
const handleVerifyBtnClick = async (
e: React.MouseEvent<HTMLElement, MouseEvent>
) => {
if (timer.current) return;
count.current -= 1;
setContent(t(K.GET_VERIFY_CODE_TIPS, { count: count.current }));
setVerifyBtnDisabled(true);
timer.current = setInterval(() => {
count.current -= 1;
setContent(t(K.GET_VERIFY_CODE_TIPS, { count: count.current }));
if (count.current === 0) {
clearInterval(timer.current);
timer.current = null;
count.current = duration;
setVerifyBtnDisabled(false);
setContent(t(K.GET_VERIFY_CODE));
}
}, 1000);
const result = await CustomerApi_sendApplicationVerificationCode({
phone_number: form.getFieldValue("phone"),
});
result.message_id && setMessageId(result.message_id);
};
const redirect = async (result: Record<string, any>): Promise<void> => {
runtime.reloadSharedData();
await runtime.reloadMicroApps();
resetLegacyIframe();
authenticate({
org: result.org,
username: result.username,
userInstanceId: result.userInstanceId,
accessRule: result.accessRule,
});
const { state } = getHistory().location;
const from =
state && state.from
? state.from
: {
pathname: "/",
};
const redirect = createLocation(from);
getHistory().push(redirect);
};
const onFinish = async (values: Record<string, any>): Promise<void> => {
values.password = encryptValue(values.password);
try {
let result: Record<string, any>;
if (isCommonSignup && !hideInvite) {
result = await OrgApi_saaSOrgRegister(
assign(omit(values, ["terms", "password2"]), {
message_id: messageId,
}) as OrgApi_SaaSOrgRegisterRequestBody
);
} else {
result = await AuthApi_register(
assign(
omit(values, ["terms", "password2", "username"]),
hideInvite
? { invite: getInviteCode(), name: values["username"] }
: { name: values["username"] }
) as AuthApi_RegisterRequestBody
);
}
if (result.loggedIn) {
redirect(result);
}
message.success(t(K.REGISTER_SUCCESS));
} catch (error) {
Modal.error({
title: t(K.REGISTER_FAILED),
content:
isCommonSignup && !hideInvite
? t(K.WRONG_VERIFICATION_CODE)
: t(K.WRONG_INVITE_CODE),
});
}
};
return (
<>
<div className={styles.signupWrapper}>
<div className={styles.signupHeader}>
<div className={styles.logoBar}>
<Link to="/">
{brand.auth_logo_url ? (
<img
src={brand.auth_logo_url}
style={{ height: 32, verticalAlign: "middle" }}
/>
) : (
<Logo height={32} style={{ verticalAlign: "middle" }} />
)}
</Link>
</div>
</div>
<div className={styles.signupImg}>
<img src={loginPng} style={{ height: imageHeight }} />
</div>
<div className={styles.signupForm}>
<Card bordered={false}>
{!hideInvite &&
(isCommonSignup ? (
<a
onClick={() => {
setIsCommonSignup(false);
}}
style={{ alignSelf: "flex-end" }}
id="JumpToJoinFormLink"
>
{t(K.JOIN_THE_ORGANIZATION)} <RightOutlined />
</a>
) : (
<a
onClick={() => {
setIsCommonSignup(true);
}}
id="JumpToCommonFormLink"
>
<LeftOutlined /> {t(K.REGISTER_COMMONLY)}
</a>
))}
{!hideInvite && isCommonSignup ? (
<div className={styles.title}>{t(K.REGISTER_ACCOUNT)}</div>
) : (
<div className={styles.title}>{t(K.REGISTER_AND_JOIN)}</div>
)}
<Form name="signupForm" form={form} onFinish={onFinish}>
<Form.Item
validateFirst={true}
name="username"
rules={[
{
required: true,
message: t(K.USERNAME_TIPS, {
minLength: 3,
maxLength: 32,
}),
},
{
pattern: usernamePattern,
message: t(K.USERNAME_TIPS, {
minLength: 3,
maxLength: 32,
}),
},
{
validator: (
_: any,
value: any,
callback: (value?: string) => void
) =>
validateMap["airNameValidator"](
value,
callback,
setForceUpdate
),
},
]}
>
<Input
prefix={<UserOutlined className={styles.inputPrefixIcon} />}
placeholder={t(K.USERNAME)}
/>
</Form.Item>
{enabledFeatures["enable-nickname-config"] && hideInvite && (
<Form.Item validateFirst={false} name="nickname">
<Input
prefix={
<SolutionOutlined className={styles.inputPrefixIcon} />
}
placeholder={t(K.NICKNAME)}
/>
</Form.Item>
)}
<Form.Item
name="email"
validateFirst={true}
rules={[
{ required: true, message: t(K.PLEASE_ENTER_VALID_EMAIL) },
{ type: "email", message: t(K.PLEASE_ENTER_VALID_EMAIL) },
{
validator: (
_: any,
value: any,
callback: (value?: string) => void
) =>
validateMap["airEmailValidator"](
value,
callback,
setForceUpdate
),
},
]}
>
<Input
prefix={<MailOutlined className={styles.inputPrefixIcon} />}
type="email"
placeholder={t(K.EMAIL)}
/>
</Form.Item>
<Form.Item
validateFirst={true}
name="password"
rules={[
{ required: true, message: t(K.PLEASE_INPUT_PASSWORD) },
{
pattern: passwordConfigMap[passwordLevel].regex,
message: passwordConfigMap[passwordLevel].description,
},
]}
>
<Input
prefix={<LockOutlined className={styles.inputPrefixIcon} />}
type="password"
placeholder={t(K.PASSWORD)}
/>
</Form.Item>
<Form.Item
dependencies={["password"]}
name="password2"
rules={[
{ required: true, message: t(K.PLEASE_INPUT_PASSWORD) },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue("password") === value) {
return Promise.resolve();
}
return Promise.reject(
new Error(t(K.TWO_PASSWORDS_ARE_INCONSISTENT))
);
},
}),
]}
>
<Input
prefix={<LockOutlined className={styles.inputPrefixIcon} />}
type="password"
placeholder={t(K.PASSWORD_CONFIRM)}
/>
</Form.Item>
{!hideInvite &&
(isCommonSignup ? (
<>
<Form.Item
validateFirst={true}
rules={[
{
required: true,
message: t(K.PLEASE_FILL_IN_VALID_PHONE_NUMBER),
},
{
validator: (_, value) => {
if (
/^(?=\d{11}$)^1(?:3\d|4[57]|5[^4\D]|7[^249\D]|8\d)\d{8}$/.test(
value
)
) {
setVerifyBtnDisabled(false);
return Promise.resolve();
}
setVerifyBtnDisabled(true);
return Promise.reject(
new Error(t(K.PLEASE_FILL_IN_VALID_PHONE_NUMBER))
);
},
},
]}
name="phone"
>
<Input
prefix={
<PhoneOutlined
className={styles.inputPrefixIcon}
rotate={90}
/>
}
suffix={
<Button
disabled={verifyBtnDisabled}
type="text"
onClick={handleVerifyBtnClick}
id="verifyBtn"
>
{content}
</Button>
}
placeholder={t(K.PHONE)}
/>
</Form.Item>
<Form.Item
rules={[
{
required: true,
message: t(K.PLEASE_INPUT_PHRASE),
},
{
pattern: /^\d{6}$/,
message: t(K.PLEASE_INPUT_VALID_PHRASE),
},
]}
name="verification_code"
>
<Input
prefix={
<SafetyOutlined className={styles.inputPrefixIcon} />
}
placeholder={t(K.VERIFY_CODE)}
></Input>
</Form.Item>
</>
) : (
<Form.Item
validateFirst={true}
name="invite"
rules={[
{
required: true,
message: t([K.PLEASE_FILL_IN_INVITE_CODE]),
},
{
pattern: iniviteCodePattern,
message: t([K.PLEASE_FILL_IN_INVITE_CODE]),
},
]}
>
<Input
prefix={
<GeneralIcon
icon={{
lib: "easyops",
icon: "release-management",
category: "menu",
color: "rgba(0,0,0,.25)",
}}
/>
}
type="text"
placeholder={t(K.INVITE_CODE)}
/>
</Form.Item>
))}
<Form.Item
name="terms"
valuePropName="checked"
rules={[
{
validator: (_, value) =>
value
? Promise.resolve()
: Promise.reject(new Error(t(K.AGREE_TERMS_TIPS))),
},
]}
>
<Checkbox>
{t(K.AGREE_TERMS)}
<a
onClick={() => {
showTerms();
}}
id="TermsLink"
>
{t(K.UWINTECH_TERMS)}
</a>
</Checkbox>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
style={{
width: "100%",
height: 34,
}}
id="submitBtn"
>
{t(K.REGISTER)}
</Button>
</Form.Item>
<Form.Item>
<div style={{ textAlign: "center" }}>
{t(K.ALREADY_HAVE_AN_ACCOUNT)}
<a
id="LogInLink"
onClick={() => {
getHistory().push(
createLocation({
pathname: props.loginUrl ?? "/auth/login",
})
);
}}
>
{t(K.LOGIN_IMMEDIATELY)}
</a>
</div>
</Form.Item>
</Form>
</Card>
<Modal
visible={isTermsVisible}
title={t(K.UWINTECH_TERMS)}
width={598}
okType="default"
cancelText={t(K.DISAGREE)}
okText={t(K.AGREE)}
closable={false}
onCancel={() => {
disagreeTerms();
}}
onOk={() => {
agreeTerms();
}}
>
<Terms />
</Modal>
</div>
</div>
</>
);
}
Example #22
Source File: SchemaItem.tsx From next-basics with GNU General Public License v3.0 | 4 votes |
export function SchemaItem({
style,
readonly,
className,
itemData,
traceId,
hideDeleteBtn,
hiddenRootNode,
disabledModelType,
isModelDefinitionRow,
parentsModel = [],
}: SchemaItemProps): React.ReactElement {
const { t } = useTranslation(NS_FLOW_BUILDER);
const editorContext = useContext(EditorContext);
const [hover, setHover] = useState(false);
const [expand, setExpand] = useState(false);
const {
modelDefinitionList = [],
onModal,
onRemove,
showModelDefinition,
hideModelDefinition,
} = editorContext;
useEffect(() => {
setExpand(!isEmpty(itemData.fields));
}, [itemData.fields]);
const openEditModal = (): void => {
onModal?.({ ...itemData }, true, traceId);
};
const openCreateModal = (): void => {
onModal?.({} as SchemaItemProperty, false, traceId);
};
const displayName = useMemo(
() => itemData.name || itemData.ref,
[itemData.name, itemData.ref]
);
const offsetPadding = useMemo(() => {
return 20 * (traceId.split("-").length - 1);
}, [traceId]);
const modelDefinition = useMemo(() => {
return modelDefinitionList.find(
(item) => item.name === calcModelDefinition(itemData)
);
}, [modelDefinitionList, itemData]);
const isSelfRef = useMemo(
() =>
isModelDefinition(itemData) &&
parentsModel.includes(calcModelDefinition(itemData)),
[itemData, parentsModel]
);
const handleExpand = (): void => {
setExpand(true);
if (itemData.ref) {
showModelDefinition(
getModelRefData(itemData.ref, modelDefinition, modelDefinitionList),
traceId
);
} else {
showModelDefinition(modelDefinition, traceId);
}
};
const handleFold = (): void => {
setExpand(false);
hideModelDefinition(traceId);
};
const handleClick = (): void => {
expand ? handleFold() : handleExpand();
};
return (
<div
className={classNames({ [styles.highlight]: modelDefinition && expand })}
>
<div
style={style}
className={className}
hidden={traceId === rootTraceId && hiddenRootNode}
>
<div
title={displayName}
className={classNames(styles.textEllipsis, {
[styles.modelDefinitionText]: isModelDefinitionRow,
})}
style={{
paddingLeft: offsetPadding,
...(hover ? { color: "var(--color-brand)" } : {}),
}}
>
{modelDefinition && !isSelfRef ? (
<span onClick={handleClick} style={{ cursor: "pointer" }}>
{expand ? (
<DownOutlined className={styles.caret} />
) : (
<RightOutlined className={styles.caret} />
)}
{displayName}
</span>
) : (
displayName
)}
</div>
<div>
<Checkbox checked={itemData.required} disabled />
</div>
<div className={styles.type}>
<Tooltip
title={
itemData.type ? t(K.SCHEMA_ITEM_NORMAL) : t(K.SCHEMA_ITEM_REF)
}
>
<Tag
className={classNames({
[styles.typeTag]: itemData.type,
[styles.refTag]: itemData.ref,
[styles.modelDefinitionTag]: isModelDefinitionRow,
})}
>
{itemData.type || itemData.ref}
</Tag>
</Tooltip>
{!readonly && modelDefinition?.updated && (
<Tooltip title={t(K.MODEL_DEFINITION_UPDATE_MESSAGE)}>
<Badge color="orange" />
</Tooltip>
)}
</div>
<div
className={classNames(styles.textEllipsis, {
[styles.modelDefinitionText]: isModelDefinitionRow,
})}
title={itemData.description}
>
{itemData.description}
</div>
{!readonly && (
<div hidden={isModelDefinitionRow}>
<Button
type="link"
className={editorStyles.iconBtn}
style={{ marginRight: 8 }}
onClick={openEditModal}
>
<SettingOutlined />
</Button>
{!hideDeleteBtn && (
<Button
type="link"
className={editorStyles.deleteBtn}
onClick={() => onRemove?.(traceId)}
>
<DeleteOutlined />
</Button>
)}
</div>
)}
</div>
{itemData.fields?.map((item, index) => (
<SchemaItem
parentsModel={
isModelDefinition(itemData)
? [...parentsModel, calcModelDefinition(itemData)]
: [...parentsModel]
}
className={editorStyles.schemaItem}
isModelDefinitionRow={isModelDefinitionRow || !!modelDefinition}
readonly={readonly}
style={{
gridTemplateColumns: getGridTemplateColumns(
filterTitleList(titleList, readonly)
),
}}
key={index}
traceId={`${traceId}-${index}`}
itemData={item}
disabledModelType={disabledModelType}
/>
))}
{!readonly && allowExpandFields(itemData.type) && (
<div
style={{ paddingLeft: 20 + offsetPadding }}
hidden={isModelDefinitionRow}
>
<Button
className={editorStyles.iconBtn}
type="link"
title={t(K.ADD_FIELD_PARAMS_TIPS, { name: displayName })}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
onClick={openCreateModal}
>
<PlusCircleOutlined />
{t(K.FIELD_PARAMS)}
</Button>
</div>
)}
</div>
);
}
Example #23
Source File: DesktopSlider.tsx From next-basics with GNU General Public License v3.0 | 4 votes |
export function DesktopSlider(props: DesktopSliderProps): React.ReactElement {
const enableMyDesktop = true;
const [desktopCursor, setDesktopCursor] = React.useState(
getRememberedDesktopCursor()
);
const [appCursor, setAppCursor] = React.useState(-1);
const { columns, rows } = useLaunchpadSettingsContext();
const { setDesktopDir } = useDesktopDirContext();
const slideDuration = 400;
useEffect(() => {
enableMyDesktop && launchpadService.setMaxVisitorLength(8);
}, [columns, enableMyDesktop]);
const mapItemToDesktop = (apps: appItem[]): DesktopData => ({
name: "-",
items: apps.map((app) => {
if (app.type === "custom") {
return {
id: app.id,
name: app.name,
url: app.homepage,
type: "custom",
app,
};
}
return {
type: "app",
id: app.id,
app,
};
}),
});
const transformCustomItem = (item: DesktopItemCustom): appItem => ({
id: item.id,
localeName: item.name,
name: item.name,
homepage: item.url,
...item,
type: "custom",
});
let desktops: DesktopData[];
let validItems = props.microApps;
if (props.desktops && props.desktops.length > 0) {
validItems = [];
const id2app = props.microApps.reduce((acc, app) => {
acc.set(app.id, app);
return acc;
}, new Map<string, appItem>());
desktops = props.desktops
.map((desktop) => ({
name: desktop.name,
items: desktop.items
.map((item) => {
if (item.type === "app") {
if (id2app.has(item.id)) {
const app = id2app.get(item.id);
validItems.push(app);
id2app.delete(item.id);
return {
type: item.type,
id: item.id,
app,
};
}
// ignore not found apps
} else if (item.type === "dir") {
const items = item.items
.map((item) => {
if (item.type === "app") {
if (id2app.has(item.id)) {
const app = id2app.get(item.id);
validItems.push(app);
id2app.delete(item.id);
return {
type: item.type,
id: item.id,
app,
};
}
} else if (item.type === "custom") {
validItems.push(transformCustomItem(item));
return item;
}
})
.filter(Boolean);
// ignore empty dirs
if (items.length > 0) {
return {
type: item.type,
id: item.id,
name: item.name,
items,
};
}
} else if (item.type === "custom") {
validItems.push(transformCustomItem(item));
return item;
}
})
.filter(Boolean)
.slice(0, columns * rows),
}))
// ignore empty desktops
.filter((desktop) => desktop.items.length > 0);
} else {
// 如果没有定义桌面列表(例如本地开发模式),则自动按数量切割。
desktops = chunk(props.microApps, columns * rows).map(mapItemToDesktop);
}
let filteredDesktop: DesktopData;
if (props.q) {
const lowerQ = props.q.toLowerCase();
filteredDesktop = mapItemToDesktop(
validItems
.filter(
(app) =>
app.localeName.toLowerCase().includes(lowerQ) ||
app.name.toLowerCase().includes(lowerQ) ||
app.id.toLowerCase().includes(lowerQ)
)
.slice(0, columns * rows)
);
}
// When sliding desktop, reset app cursor.
React.useEffect(() => {
setAppCursor(-1);
}, [desktopCursor]);
// When making search, set app cursor to the first app.
React.useEffect(() => {
setAppCursor(
props.q && filteredDesktop && filteredDesktop.items.length > 0 ? 0 : -1
);
}, [props.q]);
const lockRef = React.useRef(false);
const throttledSetDesktopCursor = (index: number): void => {
if (lockRef.current) {
return;
}
setRememberedDesktopCursor(index);
setDesktopCursor(index);
// 一次滑动一个屏幕,锁定期间内,不能继续滑动屏幕。
lockRef.current = true;
setTimeout(() => {
lockRef.current = false;
}, slideDuration);
};
const slideLeft = React.useCallback((): void => {
if (desktopCursor > 0) {
throttledSetDesktopCursor(desktopCursor - 1);
}
}, [desktopCursor]);
const slideRight = React.useCallback((): void => {
const length = desktops.length;
if (desktopCursor < length) {
throttledSetDesktopCursor(desktopCursor + 1);
}
}, [desktopCursor, desktops.length]);
const handleSlideLeft = (e: React.MouseEvent): void => {
e.stopPropagation();
slideLeft();
};
const handleSlideRight = (e: React.MouseEvent): void => {
e.stopPropagation();
slideRight();
};
const handleSlideTo = (e: React.MouseEvent, index: number): void => {
e.stopPropagation();
if (desktopCursor !== index) {
throttledSetDesktopCursor(index);
}
};
// Press arrow key to select an app.
React.useEffect(() => {
const onKeydown = (event: KeyboardEvent): void => {
// 第一栏为我的面板,须过滤掉(PS: 但是搜索时 desktopCursor 为0是可以的)
if (enableMyDesktop && desktopCursor === 0 && !props.q) return;
const key =
event.key ||
/* istanbul ignore next: compatibility */ event.keyCode ||
/* istanbul ignore next: compatibility */ event.which;
const currentDesktop = props.q
? filteredDesktop
: desktops[desktopCursor - 1];
if (key === "Enter" || key === 13) {
event.preventDefault();
if (appCursor >= 0 && appCursor < currentDesktop.items.length) {
const cell = currentDesktop.items[appCursor];
if (cell.type === "app") {
launchpadService.pushVisitor("app", cell.app);
getRuntime().resetWorkspaceStack();
getHistory().push(cell.app.homepage);
} else if (cell.type === "custom") {
launchpadService.pushVisitor("custom", cell);
window.open(cell.url);
} else if (cell.type === "dir") {
// Calculate the approximate coordinates of a dir.
const x = appCursor % columns;
const y = Math.floor(appCursor / columns);
setDesktopDir({
activeIndex: 0,
dir: {
name: cell.name,
items: cell.items,
},
coordinates: {
x: (window.innerWidth * (x + 1)) / (columns + 1),
y: (window.innerHeight * (y + 1)) / (rows + 1),
},
});
}
}
} else {
let offset = 0;
if (key === "ArrowRight" || key === 39) {
offset = 1;
} else if (key === "ArrowLeft" || key === 37) {
offset = appCursor === -1 ? currentDesktop.items.length : -1;
} else if (key === "ArrowDown" || key === 40) {
offset = appCursor === -1 ? 1 : columns;
} else if (key === "ArrowUp" || key === 38) {
offset = appCursor === -1 ? currentDesktop.items.length : -columns;
}
if (offset !== 0) {
event.preventDefault();
const next = appCursor + offset;
if (next >= 0 && next < currentDesktop.items.length) {
setAppCursor(next);
}
}
}
};
window.addEventListener("keydown", onKeydown);
return () => {
window.removeEventListener("keydown", onKeydown);
};
}, [desktopCursor, appCursor, props.q, columns, setDesktopDir]);
const deltaXRef = React.useRef(0);
const deltaYRef = React.useRef(0);
const responsibleRef = React.useRef(true);
const tryToSlideByWheel = (): void => {
// Mac 的 trackpad,部分鼠标滚轮会有“拖尾效应”,拖尾期间,不再响应滚轮事件。
if (!responsibleRef.current) {
return;
}
// 取绝对值较大的方向
const axisRef =
Math.abs(deltaYRef.current) > Math.abs(deltaXRef.current)
? deltaYRef
: deltaXRef;
// 经测试,滚轮纵轴一次位移 4,横轴一次位移 40。
const threshold = axisRef === deltaYRef ? 4 : 40;
if (axisRef.current >= threshold) {
slideRight();
} else if (axisRef.current <= -threshold) {
slideLeft();
} else {
return;
}
// 触发滑动后,重设 delta,拖尾期间,不再响应滚轮事件。
deltaXRef.current = 0;
deltaYRef.current = 0;
responsibleRef.current = false;
};
const resetDeltaTimeoutRef = React.useRef<any>();
const handleWheel = (e: React.WheelEvent): void => {
deltaXRef.current += e.deltaX;
deltaYRef.current += e.deltaY;
tryToSlideByWheel();
if (resetDeltaTimeoutRef.current) {
clearTimeout(resetDeltaTimeoutRef.current);
}
// 间隔 50ms 内的连续滚轮事件被认作一次滚动。
resetDeltaTimeoutRef.current = setTimeout(() => {
deltaXRef.current = 0;
deltaYRef.current = 0;
responsibleRef.current = true;
}, 50);
};
const sliderChildrenLength = desktops.length + 1;
return (
<div
onWheel={handleWheel}
className={classNames(styles.desktopSlider, {
[styles.filtered]: props.q,
})}
>
<div className={styles.desktopSelector}>
{[...[{ name: <HomeFilled /> }], ...desktops].map((desktop, index) => (
<React.Fragment key={index}>
{index !== 0 && <span className={styles.selectorSeparator} />}
<a
className={classNames(styles.desktopName, {
[styles.active]: desktopCursor === index,
})}
onClick={(e) => handleSlideTo(e, index)}
role="button"
>
{desktop.name}
</a>
</React.Fragment>
))}
</div>
<div className={styles.scrollContainer}>
<div
className={styles.desktopList}
style={{
width: `${sliderChildrenLength * 100}%`,
marginLeft: `${desktopCursor * -100}%`,
transition: `margin-left ${slideDuration}ms ease-out`,
}}
>
{enableMyDesktop && (
<MyDesktop
desktopCount={desktops.length}
arrowWidthPercent={props.arrowWidthPercent}
/>
)}
{desktops.map((desktop, index) => (
<Desktop
key={index}
desktop={desktop}
desktopCount={desktops.length}
arrowWidthPercent={props.arrowWidthPercent}
activeIndex={desktopCursor - 1 === index ? appCursor : -1}
/>
))}
</div>
{
// Show filtered apps as a single desktop.
props.q && (
<div className={styles.filteredList}>
<Desktop
desktop={filteredDesktop}
desktopCount={1}
arrowWidthPercent={props.arrowWidthPercent}
activeIndex={appCursor}
/>
</div>
)
}
</div>
<a
className={classNames(styles.arrowLeft, {
[styles.available]: desktopCursor > 0,
})}
style={{ width: `${props.arrowWidthPercent}%` }}
onClick={handleSlideLeft}
role="button"
>
<span className={styles.arrowButton}>
<LeftOutlined />
</span>
</a>
<a
className={classNames(styles.arrowRight, {
[styles.available]: desktopCursor < sliderChildrenLength - 1,
})}
style={{ width: `${props.arrowWidthPercent}%` }}
onClick={handleSlideRight}
role="button"
>
<span className={styles.arrowButton}>
<RightOutlined />
</span>
</a>
</div>
);
}
Example #24
Source File: DendronTreeMenu.tsx From dendron with GNU Affero General Public License v3.0 | 4 votes |
function MenuView({
roots,
expandKeys,
onSelect,
onExpand,
collapsed,
activeNote,
noteIndex,
}: {
roots: DataNode[];
expandKeys: string[];
onSelect: (noteId: string) => void;
onExpand: (noteId: string) => void;
collapsed: boolean;
activeNote: string | undefined;
} & Partial<NoteData>) {
const ExpandIcon = useCallback(
({ isOpen, ...rest }: { isOpen: boolean }) => {
const UncollapsedIcon = isOpen ? UpOutlined : DownOutlined;
const Icon = collapsed ? RightOutlined : UncollapsedIcon;
return (
<i data-expandedicon="true">
<Icon
style={{
pointerEvents: "none", // only allow custom element to be gesture target
margin: 0,
}}
/>
</i>
);
},
[collapsed]
);
const createMenu = (menu: DataNode) => {
if (menu.children && menu.children.length > 0) {
return (
<SubMenu
icon={menu.icon}
className={
menu.key === activeNote ? "dendron-ant-menu-submenu-selected" : ""
}
key={menu.key}
title={<MenuItemTitle menu={menu} noteIndex={noteIndex!} />}
onTitleClick={(event) => {
const target = event.domEvent.target as HTMLElement;
const isArrow = target.dataset.expandedicon;
if (!isArrow) {
onSelect(event.key);
} else {
onExpand(event.key);
}
}}
>
{menu.children.map((childMenu: DataNode) => {
return createMenu(childMenu);
})}
</SubMenu>
);
}
return (
<MenuItem key={menu.key} icon={menu.icon}>
<MenuItemTitle menu={menu} noteIndex={noteIndex!} />
</MenuItem>
);
};
if (activeNote) {
expandKeys.push(activeNote);
}
return (
<Menu
key={String(collapsed)}
className="dendron-tree-menu"
mode="inline"
{...(!collapsed && {
openKeys: expandKeys,
selectedKeys: expandKeys,
})}
inlineIndent={DENDRON_STYLE_CONSTANTS.SIDER.INDENT}
expandIcon={ExpandIcon}
inlineCollapsed={collapsed}
// results in gray box otherwise when nav bar is too short for display
style={{ height: "100%" }}
>
{roots.map((menu) => {
return createMenu(menu);
})}
</Menu>
);
}
Example #25
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 #26
Source File: SessionRecordingsTable.tsx From posthog-foss with MIT License | 4 votes |
export function SessionRecordingsTable({ personUUID, isPersonPage = false }: SessionRecordingsTableProps): JSX.Element {
const sessionRecordingsTableLogicInstance = sessionRecordingsTableLogic({ personUUID })
const {
sessionRecordings,
sessionRecordingsResponseLoading,
sessionRecordingId,
entityFilters,
propertyFilters,
hasNext,
hasPrev,
fromDate,
toDate,
durationFilter,
showFilters,
} = useValues(sessionRecordingsTableLogicInstance)
const {
openSessionPlayer,
closeSessionPlayer,
setEntityFilters,
setPropertyFilters,
loadNext,
loadPrev,
setDateRange,
setDurationFilter,
enableFilter,
} = useActions(sessionRecordingsTableLogicInstance)
const { preflight } = useValues(preflightLogic)
const columns: LemonTableColumns<SessionRecordingType> = [
{
title: 'Start time',
render: function RenderStartTime(_: any, sessionRecording: SessionRecordingType) {
return <TZLabel time={sessionRecording.start_time} formatString="MMMM DD, YYYY h:mm" />
},
},
{
title: 'Duration',
render: function RenderDuration(_: any, sessionRecording: SessionRecordingType) {
return <span>{humanFriendlyDuration(sessionRecording.recording_duration)}</span>
},
},
{
title: 'Person',
key: 'person',
render: function RenderPersonLink(_: any, sessionRecording: SessionRecordingType) {
return <PersonHeader withIcon person={sessionRecording.person} />
},
},
{
render: function RenderPlayButton(_: any, sessionRecording: SessionRecordingType) {
return (
<div className="play-button-container">
<Button
className={sessionRecording.viewed ? 'play-button viewed' : 'play-button'}
data-attr="session-recordings-button"
icon={<PlayCircleOutlined />}
>
Watch recording
</Button>
</div>
)
},
},
]
return (
<div className="session-recordings-table" data-attr="session-recordings-table">
<Row className="filter-row">
<div className="filter-container" style={{ display: showFilters ? undefined : 'none' }}>
<div>
<Typography.Text strong>
{`Filter by events and actions `}
<Tooltip title="Show recordings where all of the events or actions listed below happen.">
<InfoCircleOutlined className="info-icon" />
</Tooltip>
</Typography.Text>
<ActionFilter
fullWidth={true}
filters={entityFilters}
setFilters={(payload) => {
setEntityFilters(payload)
}}
typeKey={isPersonPage ? `person-${personUUID}` : 'session-recordings'}
hideMathSelector={true}
buttonCopy="Add another filter"
horizontalUI
stripeActionRow={false}
propertyFilterWrapperClassName="session-recording-action-property-filter"
customRowPrefix=""
hideRename
showOr
renderRow={(props) => <FilterRow {...props} />}
showNestedArrow={false}
actionsTaxonomicGroupTypes={[
TaxonomicFilterGroupType.Actions,
TaxonomicFilterGroupType.Events,
]}
propertiesTaxonomicGroupTypes={[
TaxonomicFilterGroupType.EventProperties,
TaxonomicFilterGroupType.Elements,
]}
/>
</div>
{!isPersonPage && preflight?.is_clickhouse_enabled && (
<div className="mt-2">
<Typography.Text strong>
{`Filter by persons and cohorts `}
<Tooltip title="Show recordings by persons who match the set criteria">
<InfoCircleOutlined className="info-icon" />
</Tooltip>
</Typography.Text>
<PropertyFilters
popoverPlacement="bottomRight"
pageKey={isPersonPage ? `person-${personUUID}` : 'session-recordings'}
taxonomicGroupTypes={[
TaxonomicFilterGroupType.PersonProperties,
TaxonomicFilterGroupType.Cohorts,
]}
propertyFilters={propertyFilters}
onChange={(properties) => {
setPropertyFilters(properties)
}}
/>
</div>
)}
</div>
<Button
style={{ display: showFilters ? 'none' : undefined }}
onClick={() => {
enableFilter()
if (isPersonPage) {
const entityFilterButtons = document.querySelectorAll('.entity-filter-row button')
if (entityFilterButtons.length > 0) {
;(entityFilterButtons[0] as HTMLElement).click()
}
}
}}
>
<FilterOutlined /> Filter recordings
</Button>
<Row className="time-filter-row">
<Row className="time-filter">
<DateFilter
makeLabel={(key) => (
<>
<CalendarOutlined />
<span> {key}</span>
</>
)}
defaultValue="Last 7 days"
bordered={true}
dateFrom={fromDate ?? undefined}
dateTo={toDate ?? undefined}
onChange={(changedDateFrom, changedDateTo) => {
setDateRange(changedDateFrom, changedDateTo)
}}
dateOptions={{
Custom: { values: [] },
'Last 24 hours': { values: ['-24h'] },
'Last 7 days': { values: ['-7d'] },
'Last 21 days': { values: ['-21d'] },
}}
/>
</Row>
<Row className="time-filter">
<Typography.Text className="filter-label">Duration</Typography.Text>
<DurationFilter
onChange={(newFilter) => {
setDurationFilter(newFilter)
}}
initialFilter={durationFilter}
pageKey={isPersonPage ? `person-${personUUID}` : 'session-recordings'}
/>
</Row>
</Row>
</Row>
<LemonTable
dataSource={sessionRecordings}
columns={columns}
loading={sessionRecordingsResponseLoading}
onRow={(sessionRecording) => ({
onClick: (e) => {
// Lets the link to the person open the person's page and not the session recording
if (!(e.target as HTMLElement).closest('a')) {
openSessionPlayer(sessionRecording.id, RecordingWatchedSource.RecordingsList)
}
},
})}
rowClassName="cursor-pointer"
data-attr="session-recording-table"
/>
{(hasPrev || hasNext) && (
<Row className="pagination-control">
<Button
type="link"
disabled={!hasPrev}
onClick={() => {
loadPrev()
window.scrollTo(0, 0)
}}
>
<LeftOutlined /> Previous
</Button>
<Button
type="link"
disabled={!hasNext}
onClick={() => {
loadNext()
window.scrollTo(0, 0)
}}
>
Next <RightOutlined />
</Button>
</Row>
)}
<div style={{ marginBottom: 64 }} />
{!!sessionRecordingId && <SessionPlayerDrawer isPersonPage={isPersonPage} onClose={closeSessionPlayer} />}
</div>
)
}