@ant-design/icons#EditOutlined TypeScript Examples
The following examples show how to use
@ant-design/icons#EditOutlined.
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: UnControlledTableHeaderPanel.tsx From datart with Apache License 2.0 | 6 votes |
EditableLabel: FC<{
label: string;
editable?: Boolean;
onChange: (value: string) => void;
}> = memo(({ label, editable = true, onChange }) => {
const [isEditing, setIsEditing] = useState(false);
const render = () => {
if (!editable) {
return <span>{label}</span>;
}
return isEditing ? (
<Search
enterButton={<CheckOutlined />}
placeholder={label}
size="small"
onSearch={value => {
if (!!value) {
setIsEditing(false);
onChange(value);
}
}}
/>
) : (
<>
<span>{label}</span>
<Button
type="text"
size="small"
icon={<EditOutlined />}
onClick={() => setIsEditing(true)}
></Button>
</>
);
};
return <StyledEditableLabel>{render()}</StyledEditableLabel>;
})
Example #2
Source File: CommandInput.tsx From posthog-foss with MIT License | 6 votes |
export function CommandInput(): JSX.Element {
const { input, isSqueak, activeFlow } = useValues(commandPaletteLogic)
const { setInput } = useActions(commandPaletteLogic)
return (
<div className="palette__row">
{isSqueak ? (
<img src={PostHogIcon} className="palette__icon" />
) : activeFlow ? (
<activeFlow.icon className="palette__icon" /> ?? <EditOutlined className="palette__icon" />
) : (
<SearchOutlined className="palette__icon" />
)}
<input
className="palette__display palette__input ph-no-capture"
autoFocus
value={input}
onChange={(event) => {
setInput(event.target.value)
}}
placeholder={activeFlow?.instruction ?? 'What would you like to do? Try some suggestions…'}
data-attr="command-palette-input"
/>
</div>
)
}
Example #3
Source File: BasicUnControlledTabPanel.tsx From datart with Apache License 2.0 | 6 votes |
EditableTabHeader: FC<{
label: string;
rowKey: string;
editable?: Boolean;
onChange: (key: string, value: string) => void;
}> = memo(({ label, rowKey, editable = true, onChange }) => {
const [isEditing, setIsEditing] = useState(false);
const render = () => {
if (!editable) {
return <span>{label}</span>;
}
return isEditing ? (
<Search
enterButton={<CheckOutlined />}
size="small"
onSearch={value => {
if (!!value) {
setIsEditing(false);
onChange(rowKey, value);
}
}}
/>
) : (
<Space>
<Button
block
type="text"
icon={<EditOutlined />}
onClick={() => setIsEditing(true)}
></Button>
<span>{label}</span>
</Space>
);
};
return render();
})
Example #4
Source File: MoreOption.spec.tsx From next-basics with GNU General Public License v3.0 | 6 votes |
describe("MoreOption", () => {
it("should work", () => {
const wrapper = shallow(<MoreOption />);
expect(wrapper.find("span").text()).toEqual("20 ");
});
it("should work with props", () => {
const mockClickFn = jest.fn();
const wrapper = mount(
<MoreOption itemsCount={10} onChange={mockClickFn} />
);
wrapper.find(EditOutlined).simulate("click");
wrapper.find(InputNumber).invoke("onChange")(100);
wrapper.find(InputNumber).invoke("onBlur")(null);
expect(mockClickFn).toBeCalledWith(100);
});
});
Example #5
Source File: index.tsx From fe-v5 with Apache License 2.0 | 5 votes |
TagFilter: React.ForwardRefRenderFunction<any, ITagFilterProps> = ({ isOpen = false, value, onChange, editable = true, cluster, range, id, onOpenFire }, ref) => {
const { t } = useTranslation();
const [editing, setEditing] = useState<boolean>(isOpen);
const [varsMap, setVarsMap] = useState<{ string?: string | string[] | undefined }>({});
const [data, setData] = useState<FormType>();
const handleEditClose = (v: FormType) => {
if (v) {
onChange(v, true);
setData(v);
}
setEditing(false);
};
useEffect(() => {
value && setData(value);
}, [value]);
const handleVariableChange = (index: number, v: string | string[], options) => {
const newData = data ? { var: _.cloneDeep(data.var) } : { var: [] };
const newDataWithOptions = data ? { var: _.cloneDeep(data.var) } : { var: [] };
// newData.var[index].selected = v;
setVaraiableSelected(newData.var[index].name, v, id);
setVarsMap((varsMap) => ({ ...varsMap, [`$${newData.var[index].name}`]: v }));
options && (newDataWithOptions.var[index].options = options);
setData(newData);
onChange(newData, false, newDataWithOptions);
};
return (
<div className='tag-area'>
<div className={classNames('tag-content', 'tag-content-close')}>
{data?.var && data?.var.length > 0 && (
<>
{data.var.map((expression, index) => (
<DisplayItem
expression={expression}
index={index}
data={data.var}
onChange={handleVariableChange}
cluster={cluster}
range={range}
key={index}
id={id}
varsMap={varsMap}
></DisplayItem>
))}
{editable && (
<EditOutlined
className='icon'
onClick={() => {
setEditing(true);
onOpenFire && onOpenFire();
}}
></EditOutlined>
)}
</>
)}
{(data ? data?.var.length === 0 : true) && editable && (
<div
className='add-variable-tips'
onClick={() => {
setEditing(true);
onOpenFire && onOpenFire();
}}
>
{t('添加大盘变量')}
</div>
)}
</div>
<EditItem visible={editing} onChange={handleEditClose} value={data} range={range} id={id} />
</div>
);
}
Example #6
Source File: PluginField.tsx From posthog-foss with MIT License | 5 votes |
export function PluginField({
value,
onChange,
fieldConfig,
}: {
value?: any
onChange?: (value: any) => void
fieldConfig: PluginConfigSchema
}): JSX.Element {
const [editingSecret, setEditingSecret] = useState(false)
if (
fieldConfig.secret &&
!editingSecret &&
value &&
(value === SECRET_FIELD_VALUE || value.name === SECRET_FIELD_VALUE)
) {
return (
<Button
icon={<EditOutlined />}
onClick={() => {
onChange?.(fieldConfig.default || '')
setEditingSecret(true)
}}
>
Reset secret {fieldConfig.type === 'attachment' ? 'attachment' : 'field'}
</Button>
)
}
return fieldConfig.type === 'attachment' ? (
<UploadField value={value} onChange={onChange} />
) : fieldConfig.type === 'string' ? (
<Input value={value} onChange={onChange} autoFocus={editingSecret} className="ph-no-capture" />
) : fieldConfig.type === 'choice' ? (
<Select dropdownMatchSelectWidth={false} value={value} className="ph-no-capture" onChange={onChange} showSearch>
{fieldConfig.choices.map((choice) => (
<Select.Option value={choice} key={choice}>
{choice}
</Select.Option>
))}
</Select>
) : (
<strong style={{ color: 'var(--danger)' }}>
Unknown field type "<code>{fieldConfig.type}</code>".
<br />
You may need to upgrade PostHog!
</strong>
)
}
Example #7
Source File: Tile.tsx From ant-extensions with MIT License | 5 votes |
Tile: React.FC<ITileConfig> = React.memo((item) => {
const { isEditing, editWidget, renderWidget, findWidget } = useContext(Context);
const style = useMemo(
() => ({
color: item.color || "inherit"
}),
[item.color]
);
const [expanded, setExpanded] = useState(false);
const widget = useMemo(() => findWidget(item.widgetId), [findWidget, item.widgetId]);
return (
<Item item={item} expanded={!isEditing && expanded}>
<div className="ant-ext-pm__tileHead">
<span style={style}>{item.iconCls && <i className={item.iconCls} />}</span>
<label style={style}>{item.title}</label>
<div>
{item.info && (
<Tooltip
overlay={<pre dangerouslySetInnerHTML={{ __html: item.info }} />}
overlayClassName="ant-ext-pm__tileInfo"
>
<button>
<InfoCircleOutlined />
</button>
</Tooltip>
)}
{!isEditing && item.expandable && (
<button className="ant-ext-pm__tileExpander" onClick={() => setExpanded(!expanded)}>
{expanded ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</button>
)}
{isEditing && (
<button onClick={() => editWidget(item.widgetId)}>
<EditOutlined />
</button>
)}
</div>
</div>
<div className="ant-ext-pm__tileBody">
{!isEditing && renderWidget(item.widgetId)}
{isEditing && widget && (
<div style={{ placeSelf: "center", textAlign: "center" }}>
{widget.icon}
<div>{widget.title}</div>
</div>
)}
</div>
</Item>
);
})
Example #8
Source File: InputConfirm.tsx From iot-center-v2 with MIT License | 5 votes |
InputConfirm: React.FC<TInputConfirmProps> = (props) => {
const {value, onValueChange, tooltip} = props
const [newValue, setNewValue] = useState('')
const [isEditing, setIsEditing] = useState(false)
const [loading, setLoading] = useState(false)
useEffect(() => {
setNewValue(value ?? '')
}, [value])
return (
<Row>
<Col flex="auto">
{!isEditing ? (
value ?? ''
) : (
<Tooltip title={tooltip ?? ''}>
<Input
value={newValue}
onChange={(e) => setNewValue(e.target.value)}
style={{width: '100%'}}
/>
</Tooltip>
)}
</Col>
<Col>
{!isEditing ? (
<Tooltip title={'Edit'} key="edit">
<Button
size="small"
type="text"
icon={<EditOutlined />}
onClick={() => setIsEditing(true)}
></Button>
</Tooltip>
) : (
<>
<Tooltip title={loading ? '' : 'Cancel'} color="red" key="cancel">
<Button
size="small"
type="text"
disabled={loading}
icon={<CloseOutlined />}
onClick={() => {
setIsEditing(false)
}}
danger
></Button>
</Tooltip>
<Tooltip title={loading ? '' : 'Save'} color="green">
<Button
size="small"
type="text"
disabled={loading}
loading={loading}
style={{color: 'green'}}
icon={<CheckOutlined />}
onClick={async () => {
try {
setLoading(true)
await (onValueChange ?? (() => undefined))(newValue)
} finally {
setIsEditing(false)
setLoading(false)
}
}}
></Button>
</Tooltip>
</>
)}
</Col>
</Row>
)
}
Example #9
Source File: node.tsx From imove with MIT License | 5 votes |
nodeMenuConfig = [
{
key: 'copy',
title: '复制',
icon: <CopyOutlined />,
handler: shortcuts.copy.handler,
},
{
key: 'delete',
title: '删除',
icon: <DeleteOutlined />,
handler: shortcuts.delete.handler,
},
{
key: 'rename',
title: '编辑文本',
icon: <EditOutlined />,
showDividerBehind: true,
handler() {
// TODO
},
},
{
key: 'bringToTop',
title: '置于顶层',
icon: <XIcon type={'icon-bring-to-top'} />,
handler: shortcuts.bringToTop.handler,
},
{
key: 'bringToBack',
title: '置于底层',
icon: <XIcon type={'icon-bring-to-bottom'} />,
showDividerBehind: true,
handler: shortcuts.bringToBack.handler,
},
{
key: 'editCode',
title: '编辑代码',
icon: <FormOutlined />,
disabled(flowChart: Graph) {
return getSelectedNodes(flowChart).length !== 1;
},
handler(flowChart: Graph) {
flowChart.trigger('graph:editCode');
},
},
{
key: 'executeCode',
title: '执行代码',
icon: <CodeOutlined />,
disabled(flowChart: Graph) {
return getSelectedNodes(flowChart).length !== 1;
},
handler(flowChart: Graph) {
flowChart.trigger('graph:runCode');
},
},
]
Example #10
Source File: index.tsx From jetlinks-ui-antd with MIT License | 5 votes |
GridLayout: React.FC<Props> = props => {
const { layout, edit } = props;
return (
<>
<ReactGridLayout
onLayoutChange={(item: any) => {
// layoutChange(item)
}}
// cols={{ md: 12 }}
// isResizable={edit}
// isDraggable={edit}
onDragStop={() => {
// setLayout([...layout])
}}
onResizeStop={() => {
// setLayout([...layout])
}}
className="layout"
// layout={layout}
rowHeight={30}
>
{layout.map((item: any) => (
<Card
style={{ overflow: "hidden" }}
key={item.i}
id={item.i}
>
<div style={{ position: 'absolute', right: 15, top: 5, }}>
<div style={{ float: 'right' }}>
<Fragment>
{edit && (
<>
<Tooltip title="删除">
<CloseCircleOutlined onClick={() => {
// removeCard(item)
}} />
</Tooltip>
<Divider type="vertical" />
<Tooltip title="编辑">
<EditOutlined onClick={() => {
// setAddItem(true);
// setCurrent(item)
}} />
</Tooltip>
</>)}
{item.doReady &&
<>
<Divider type="vertical" />
<Tooltip title="刷新">
<SyncOutlined onClick={() => { item.doReady() }} />
</Tooltip>
</>}
</Fragment>
</div>
</div>
<GridCard
{...item}
productId={props.productId}
deviceId={props.target} />
</Card>))}
</ReactGridLayout>
</>
)
}
Example #11
Source File: Children.tsx From ui-visualization with MIT License | 5 votes |
Children = (props: Props) => {
const childLayout = [
{ i: 'd', x: 6, y: 0, w: 3, h: 2, minW: 2, maxW: 4 },
{ i: 'e', x: 8, y: 0, w: 1, h: 2 }
]
const [edit, setEdit] = useState<boolean>(false);
const [layout, setLayout] = useState<any[]>(childLayout);
return (
<Fragment>
<GridLayout
className="layout"
layout={layout}
cols={12}
rowHeight={30}
isDraggable={edit}
isResizable={edit}
width={1200}>
{
layout.map(i => <div key={i.i} style={{ backgroundColor: '#69E2D4' }}>{i.i}</div>)
}
</GridLayout>
<div className={styles.layoutOption}>
{edit ?
<div style={{ float: 'right' }}>
<Tooltip title="新增" style={{ float: 'right' }}>
<Button
type="danger"
shape="circle"
size="large"
onClick={() => {
layout.push({ i: Math.random(), x: 8, y: 7, w: 4, h: 4 })
setLayout([...layout]);
}}
>
<PlusOutlined />
</Button>
</Tooltip>
<div style={{ float: 'right', marginLeft: 10 }}>
<Tooltip title="保存" >
<Button
type="primary"
shape="circle"
size="large"
onClick={() => {
setEdit(false)
}}
>
<SaveOutlined />
</Button>
</Tooltip>
</div>
</div> :
<div style={{ float: 'right', textAlign: 'center' }}>
<Tooltip title="编辑" >
<Button
type="danger"
shape="circle"
size="large"
onClick={() => {
setEdit(true)
props.edit()
}}
>
<EditOutlined />
</Button>
</Tooltip>
</div>
}
</div>
</Fragment >
)
}
Example #12
Source File: index.tsx From fe-v5 with Apache License 2.0 | 5 votes |
export default function index(props: IProps) {
const { editable = true, value } = props;
return (
<Space align='baseline'>
<Dropdown
overlay={
<Menu>
{_.isEmpty(value) ? (
<div style={{ textAlign: 'center' }}>暂无数据</div>
) : (
_.map(value, (item, idx) => {
return (
<Menu.Item key={idx}>
<a href={item.url} target={item.targetBlank ? '_blank' : '_self'}>
{item.title}
</a>
</Menu.Item>
);
})
)}
</Menu>
}
>
<Button>
大盘链接 <DownOutlined />
</Button>
</Dropdown>
{editable && (
<EditOutlined
style={{ fontSize: 18 }}
className='icon'
onClick={() => {
Edit({
initialValues: value,
onOk: (newValue) => {
props.onChange(newValue);
},
});
}}
/>
)}
</Space>
);
}
Example #13
Source File: index.tsx From visual-layout with MIT License | 5 votes |
Project: React.FC<{
project: ProjectObject;
appService: AppService;
setVisible: (visible: boolean) => void;
}> = ({ project, appService, setVisible }) => {
const operation = [
{
key: 'EditOutlined',
icon: (
<div
className={styles.item}
onClick={() => {
appService.set(project.id);
setVisible(false);
}}
>
<EditOutlined />
</div>
),
},
{
key: 'ArrowDownOutlined',
icon: (
<div
className={styles.item}
onClick={() => {
exportCode(appService.project);
}}
>
<ArrowDownOutlined />
</div>
),
},
{
key: 'DeleteOutlined',
icon: (
<Popconfirm
title="确定删除项目"
onConfirm={() => appService.delete(project.id)}
onCancel={() => {}}
okText="是"
cancelText="否"
>
<div className={styles.item}>
<DeleteOutlined style={{ color: 'red' }} />
</div>
</Popconfirm>
),
},
];
const isSelect = appService.project.id === project.id;
return (
<div className={styles.container}>
<div className={styles.operation}>
<span className={isSelect ? styles.select : ''} />
{operation.map(({ key, icon }) => (
<div key={key}>{icon}</div>
))}
</div>
<div className={styles.info}>
<div>
<span>项目名: </span>
<span>{project.name ? project.name : '--'}</span>
</div>
<div>
<span>项目描述: </span>
<span>{project.description ? project.description : '--'}</span>
</div>
</div>
</div>
);
}
Example #14
Source File: CommentOptions.tsx From foodie with MIT License | 5 votes |
CommentOptions: React.FC<IProps> = (props) => {
const [isOpen, setIsOpen] = useState(false);
const isOpenRef = useRef(isOpen);
const dispatch = useDispatch();
useEffect(() => {
document.addEventListener('click', handleClickOutside);
return () => {
document.removeEventListener('click', handleClickOutside);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
isOpenRef.current = isOpen;
}, [isOpen]);
const handleClickOutside = (e: Event) => {
const option = (e.target as HTMLDivElement).closest(`#comment_${props.comment.id}`);
if (!option && isOpenRef.current) {
setIsOpen(false);
}
}
const toggleOpen = () => {
setIsOpen(!isOpen);
}
const onClickDelete = () => {
dispatch(setTargetComment(props.comment));
props.openDeleteModal();
}
const onClickEdit = () => {
setIsOpen(false);
props.onClickEdit();
dispatch(setTargetComment(props.comment));
}
return (
<div className="relative z-10" id={`comment_${props.comment.id}`}>
<div
className="p-2 rounded-full flex items-center justify-center cursor-pointer hover:bg-gray-200 dark:text-white dark:hover:bg-indigo-1100"
onClick={toggleOpen}
>
<EllipsisOutlined style={{ fontSize: '20px' }} />
</div>
{isOpen && (
<div className=" w-56 flex flex-col bg-white dark:bg-indigo-1000 rounded-md shadow-lg overflow-hidden absolute top-8 right-3 border border-gray-200 dark:border-gray-800 divide-y divide-gray-100 dark:divide-gray-800">
{props.comment.isOwnComment && (
<h4
className="p-4 flex items-center hover:bg-indigo-700 hover:text-white cursor-pointer dark:text-white"
onClick={onClickEdit}
>
<EditOutlined className="mr-4" />
Edit Comment
</h4>
)}
<h4
className="p-4 flex items-center hover:bg-indigo-700 hover:text-white cursor-pointer dark:text-white"
onClick={onClickDelete}
>
<DeleteOutlined className="mr-4" />
Delete Comment
</h4>
</div>
)}
</div>
);
}
Example #15
Source File: TriggerData.tsx From leek with Apache License 2.0 | 5 votes |
function TriggerData(props) { return [ { title: "ID", dataIndex: "id", key: "id", render: (id) => { return ( <Space direction="horizontal"> <SlackOutlined /> <Text strong>{id}</Text> </Space> ); }, }, { title: "Status", dataIndex: "enabled", key: "ENABLED", render: (enabled) => { return enabled ? ( <Tag color="green">Enabled</Tag> ) : ( <Tag color="red">Disabled</Tag> ); }, }, { title: "States", dataIndex: "states", key: "states", render: (states) => { return states && states.length > 0 ? ( states.map((state, key) => <TaskState state={state} key={key} />) ) : ( <Tag>{"All STATES"}</Tag> ); }, }, { title: "Environments", dataIndex: "envs", key: "envs", render: (envs) => { return envs && envs.length > 0 ? ( envs.map((env, key) => <Tag key={key}>{env}</Tag>) ) : ( <Tag>{"All ENVS"}</Tag> ); }, }, { title: "Actions", dataIndex: "id", key: "id", render: (id, record) => { return ( <Space direction="horizontal"> <Button onClick={() => props.handleEditTrigger(id, record)} size="small" loading={props.triggersModifying} icon={<EditOutlined />} /> <Button onClick={() => props.handleDeleteTrigger(id)} size="small" type="primary" ghost danger loading={props.triggersModifying} icon={<DeleteOutlined />} /> </Space> ); }, }, ]; }
Example #16
Source File: ApiKeyCard.tsx From jitsu with MIT License | 5 votes |
export function ApiKeyCard({ apiKey: key, showDocumentation }: ApiKeyCardProps) {
const services = useServices()
const [loading, setLoading] = useState(false)
const rotateKey = async (key: ApiKey, type: "jsAuth" | "serverAuth"): Promise<string> => {
let newKey = apiKeysStore.generateApiToken(type === "jsAuth" ? "js" : "s2s")
await flowResult(apiKeysStore.patch(key.uid, { [type]: newKey }))
actionNotification.info("New key has been generated and saved")
return newKey
}
let deleteAction = async () => {
confirmDelete({
entityName: "api key",
action: async () => {
setLoading(true)
try {
await flowResult(apiKeysStore.delete(key.uid))
} catch (error) {
handleError(error, "Unable to delete API key at this moment, please try later.")
} finally {
setLoading(false)
}
},
})
}
let editLink = generatePath(apiKeysRoutes.editExact, {
projectId: services.activeProject.id,
id: key.uid.replace(".", "-"),
})
return (
<ConnectionCard
loading={loading}
title={APIKeyUtil.getDisplayName(key)}
icon={apiKeysReferenceMap.js.icon}
deleteAction={deleteAction}
editAction={editLink}
menuOverlay={
<Menu>
<Menu.Item icon={<EditOutlined />}>
<NavLink to={editLink}>Edit</NavLink>
</Menu.Item>
<Menu.Item icon={<DeleteOutlined />} onClick={deleteAction}>
Delete
</Menu.Item>
</Menu>
}
rename={async newName => {
await flowResult(apiKeysStore.patch(key.uid, { comment: newName }))
}}
subtitle={<a onClick={showDocumentation}>Show connection instructions→</a>}
status={
<>
<div className="text-xs">
<div className="flex flex-nowrap items-center">
<span className="inline-block whitespace-nowrap w-16 text-xxs">Server Key</span>{" "}
<SecretKey rotateKey={() => rotateKey(key, "serverAuth")}>{key.serverAuth}</SecretKey>
</div>
<div className="flex flex-nowrap items-center pt-2">
<span className="inline-block whitespace-nowrap w-16 text-xxs">JS Key</span>
<SecretKey rotateKey={() => rotateKey(key, "jsAuth")}>{key.jsAuth}</SecretKey>
</div>
</div>
</>
}
/>
)
}
Example #17
Source File: NewActionButton.tsx From posthog-foss with MIT License | 5 votes |
export function NewActionButton(): JSX.Element {
const [visible, setVisible] = useState(false)
const [appUrlsVisible, setAppUrlsVisible] = useState(false)
return (
<>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setVisible(true)} data-attr="create-action">
New Action
</Button>
<Modal
visible={visible}
style={{ cursor: 'pointer' }}
onCancel={() => {
setVisible(false)
setAppUrlsVisible(false)
}}
title="Create new action"
footer={[
appUrlsVisible && (
<Button key="back-button" onClick={() => setAppUrlsVisible(false)}>
Back
</Button>
),
<Button
key="cancel-button"
onClick={() => {
setVisible(false)
setAppUrlsVisible(false)
}}
>
Cancel
</Button>,
]}
>
{!appUrlsVisible && (
<Row gutter={2} justify="space-between">
<Col xs={11}>
<Card
title="Inspect element on your site"
onClick={() => setAppUrlsVisible(true)}
size="small"
>
<div style={{ textAlign: 'center', fontSize: 40 }}>
<SearchOutlined />
</div>
</Card>
</Col>
<Col xs={11}>
<Card
title="From event or pageview"
onClick={() => {
router.actions.push(urls.createAction())
}}
size="small"
>
<div style={{ textAlign: 'center', fontSize: 40 }} data-attr="new-action-pageview">
<EditOutlined />
</div>
</Card>
</Col>
</Row>
)}
{appUrlsVisible && <AuthorizedUrlsTable />}
</Modal>
</>
)
}
Example #18
Source File: CourseAdminPanel.tsx From office-hours with GNU General Public License v3.0 | 5 votes |
export default function CourseAdminPanel({
defaultPage,
courseId,
}: CourseAdminPageProps): ReactElement {
const profile = useProfile();
const [currentSettings, setCurrentSettings] = useState(
defaultPage || CourseAdminOptions.CHECK_IN
);
const router = useRouter();
return (
<Row>
<Col span={4} style={{ textAlign: "center" }}>
<SettingsPanelAvatar avatarSize={20} />
<CenteredText>
Welcome back
<br />
{profile?.firstName} {profile?.lastName}
{!profile?.photoURL && (
<Tooltip
title={
"You should consider uploading a profile picture to make yourself more recognizable to students"
}
>
<span>
<QuestionCircleOutlined
style={{ marginLeft: "5px" }}
onClick={() => {
router.push(`/settings?cid=${courseId}`);
}}
/>
</span>
</Tooltip>
)}
</CenteredText>
<Menu
defaultSelectedKeys={[currentSettings]}
onClick={(e) => setCurrentSettings(e.key as CourseAdminOptions)}
style={{ background: "#f8f9fb", paddingTop: "20px" }}
>
<Menu.Item key={CourseAdminOptions.CHECK_IN} icon={<EditOutlined />}>
TA Check In/Out Times
</Menu.Item>
<Menu.Item key={CourseAdminOptions.OVERRIDES} icon={<BellOutlined />}>
Course Overrides
</Menu.Item>
</Menu>
</Col>
<VerticalDivider />
<Space direction="vertical" size={40} style={{ flexGrow: 1 }}>
<Col span={20}>
{currentSettings === CourseAdminOptions.CHECK_IN && (
<TACheckInCheckOutTimes courseId={courseId} />
)}
{currentSettings === CourseAdminOptions.OVERRIDES && (
<CourseOverrideSettings courseId={courseId} />
)}
</Col>
</Space>
</Row>
);
}
Example #19
Source File: FlowNameView.tsx From Protoman with MIT License | 5 votes |
FlowNameView: React.FunctionComponent<Props> = ({}) => {
const dispatch = useDispatch();
const [draftName, setDraftName] = React.useState('');
const [isEditingName, setIsEditingName] = React.useState(false);
const startEditing = (): void => setIsEditingName(true);
const stopEditing = (): void => setIsEditingName(false);
const [isInvalidName, setIsInvalidName] = React.useState(false);
const collection = useSelector(selectCurrentCol);
const flowName = useSelector((s: AppState) => s.currentFlow);
React.useEffect(() => {
if (flowName) {
setDraftName(flowName);
}
}, [flowName]);
function checkName(newName: string): boolean {
return newName === flowName || !collection?.flows?.map(([n]): string => n)?.includes(newName);
}
return (
<TitleWrapper>
{isEditingName ? (
<Form.Item
validateStatus={isInvalidName ? 'error' : ''}
style={{ margin: 0 }}
help={isInvalidName ? 'Invalid Name' : ''}
>
<TitleInput
value={draftName}
onChange={(e): void => {
setIsInvalidName(!checkName(e.target.value));
setDraftName(e.target.value);
}}
onKeyDown={(e): void => {
switch (e.keyCode) {
case 27: // esc
setDraftName(name);
stopEditing();
break;
case 13: // enter
if (!isInvalidName) {
dispatch(changeFlowName(draftName));
stopEditing();
}
}
}}
/>
</Form.Item>
) : (
<>
<Title>{draftName}</Title>
<Button shape="circle" size="small" onClick={startEditing} style={{ marginLeft: 4 }}>
<EditOutlined />
</Button>
</>
)}
</TitleWrapper>
);
}
Example #20
Source File: detail.tsx From fe-v5 with Apache License 2.0 | 4 votes |
export default function DashboardDetail() {
const refreshRef = useRef<{ closeRefresh: Function }>();
const { t } = useTranslation();
const { id, busiId } = useParams<URLParam>();
const [groupForm] = Form.useForm();
const history = useHistory();
const Ref = useRef<any>(null);
const { clusters } = useSelector<CommonRootState, CommonStoreState>((state) => state.common);
const localCluster = localStorage.getItem('curCluster');
const [curCluster, setCurCluster] = useState<string>(localCluster || clusters[0]);
if (!localCluster && clusters.length > 0) {
setCurCluster(clusters[0]);
localStorage.setItem('curCluster', clusters[0]);
}
const [dashboard, setDashboard] = useState<Dashboard>({
create_by: '',
favorite: 0,
id: 0,
name: '',
tags: '',
update_at: 0,
update_by: '',
});
const [step, setStep] = useState<number | null>(null);
const [titleEditing, setTitleEditing] = useState(false);
const [chartGroup, setChartGroup] = useState<Group[]>([]);
const [variableConfig, setVariableConfig] = useState<VariableType>();
const [variableConfigWithOptions, setVariableConfigWithOptions] = useState<VariableType>();
const [dashboardLinks, setDashboardLinks] = useState<ILink[]>();
const [groupModalVisible, setGroupModalVisible] = useState(false);
const [chartModalVisible, setChartModalVisible] = useState(false);
const [chartModalInitValue, setChartModalInitValue] = useState<Chart | null>();
const [range, setRange] = useState<Range>({
start: 0,
end: 0,
});
const [refreshFlag, setRefreshFlag] = useState(_.uniqueId('refreshFlag_'));
const { run } = useThrottleFn(
() => {
if ('start' in range && range.start && range.end) {
const diff = range.end - range.start;
const now = moment().unix();
setRange({
end: now,
start: now - diff,
});
} else if ('unit' in range && range.unit) {
const newRefreshFlag = _.uniqueId('refreshFlag_');
setRange({
...range,
refreshFlag: newRefreshFlag,
});
setRefreshFlag(newRefreshFlag);
}
init(false);
},
{ wait: 1000 },
);
useEffect(() => {
init();
}, []);
const init = (needUpdateVariable = true) => {
getSingleDashboard(busiId, id).then((res) => {
setDashboard(res.dat);
if (res.dat.configs) {
const configs = JSON.parse(res.dat.configs);
setVariableConfig(configs);
setVariableConfigWithOptions(configs);
setDashboardLinks(configs.links);
}
});
getChartGroup(busiId, id).then((res) => {
let arr = res.dat || [];
setChartGroup(
arr
.sort((a, b) => a - b)
.map((item) => {
item.updateTime = Date.now(); // 前端拓展一个更新时间字段,用来主动刷新ChartGroup
return item;
}),
);
});
};
const handleDateChange = (e) => {
setRange(e);
};
const handleEdit = () => {
setTitleEditing(!titleEditing);
};
const handleModifyTitle = async (e) => {
await updateSingleDashboard(busiId, id, { ...dashboard, name: e.target.value });
// await init();
setDashboard({ ...dashboard, name: e.target.value });
setTitleEditing(false);
};
const handleAddChart = (gid: number) => {
groupId = gid;
editor({
visible: true,
variableConfig: variableConfigWithOptions,
cluster: curCluster,
busiId,
groupId,
id,
initialValues: {
type: 'timeseries',
targets: [
{
refId: 'A',
expr: '',
},
],
},
onOK: () => {
handleChartConfigVisibleChange(true);
},
});
// setChartModalVisible(true);
}; //group是为了让detail组件知道当前需要刷新的是哪个chartGroup,item是为了获取待编辑的信息
const handleUpdateChart = (group: Group, item: Chart) => {
groupId = group.id;
setChartModalInitValue(item);
if (semver.valid(item.configs.version)) {
editor({
visible: true,
variableConfig,
cluster: curCluster,
busiId,
groupId,
id,
initialValues: {
...item.configs,
id: item.id,
},
onOK: () => {
handleChartConfigVisibleChange(true);
},
});
} else {
setChartModalVisible(true);
}
};
const handleDelChart = async (group: Group, item: Chart) => {
groupId = group.id;
await removeChart(busiId, item.id as any);
refreshUpdateTimeByChartGroupId();
};
const handleCloneChart = async (group: Group, item: Chart) => {
groupId = group.id;
const configsClone = _.cloneDeep(item.configs);
configsClone.layout = {
w: configsClone.layout.w,
h: configsClone.layout.h,
};
await createChart(busiId, {
configs: JSON.stringify(configsClone),
weight: 0,
group_id: groupId,
});
refreshUpdateTimeByChartGroupId();
};
const handleShareChart = async (group: Group, item: any) => {
const serielData = {
dataProps: {
...item.configs,
targets: _.map(item.configs.targets, (target) => {
const realExpr = variableConfigWithOptions ? replaceExpressionVars(target.expr, variableConfigWithOptions, variableConfigWithOptions.var.length, id) : target.expr;
return {
...target,
expr: realExpr,
};
}),
step,
range,
},
curCluster: localStorage.getItem('curCluster'),
};
SetTmpChartData([
{
configs: JSON.stringify(serielData),
},
]).then((res) => {
const ids = res.dat;
window.open('/chart/' + ids);
});
};
const handleAddOrUpdateChartGroup = async () => {
await groupForm.validateFields();
let obj = groupForm.getFieldsValue();
if (isAddGroup) {
let weightArr = chartGroup.map((item) => item.weight);
let weight = Math.max(...weightArr) + 1;
await createChartGroup(busiId, { ...obj, weight, dashboard_id: Number(id) });
} else {
let group = chartGroup.find((item) => item.id === groupId);
await updateChartGroup(busiId, [{ dashboard_id: Number(id), ...group, ...obj }]);
}
init();
isAddGroup = true;
setGroupModalVisible(false);
};
const handleUpdateChartGroup = (group: Group) => {
groupId = group.id;
isAddGroup = false;
groupForm.setFieldsValue({ name: group.name });
setGroupModalVisible(true);
};
const handleMoveUpChartGroup = async (group: Group) => {
const { weight } = group;
let lessWeightGroup = chartGroup.find((item) => item.weight === weight - 1);
if (!lessWeightGroup) return;
lessWeightGroup.weight = weight;
group.weight = weight - 1;
await updateChartGroup(busiId, [lessWeightGroup, group]);
init();
};
const handleMoveDownChartGroup = async (group: Group) => {
const { weight } = group;
let lessWeightGroup = chartGroup.find((item) => item.weight === weight + 1);
if (!lessWeightGroup) return;
lessWeightGroup.weight = weight;
group.weight = weight + 1;
await updateChartGroup(busiId, [lessWeightGroup, group]);
init();
};
const handleDelChartGroup = async (id: number) => {
await delChartGroup(busiId, id);
message.success(t('删除分组成功'));
init();
setGroupModalVisible(false);
};
const refreshUpdateTimeByChartGroupId = () => {
let groupIndex = chartGroup.findIndex((item) => item.id === groupId);
if (groupIndex < 0) return;
let newChartGroup = [...chartGroup];
newChartGroup[groupIndex].updateTime = Date.now();
setChartGroup(newChartGroup);
};
const handleChartConfigVisibleChange = (b) => {
setChartModalVisible(false);
setChartModalInitValue(null);
b && refreshUpdateTimeByChartGroupId();
};
const handleVariableChange = (value, b, valueWithOptions) => {
let dashboardConfigs: any = {};
try {
if (dashboard.configs) {
dashboardConfigs = JSON.parse(dashboard.configs);
}
} catch (e) {
console.error(e);
}
dashboardConfigs.var = value.var;
b && updateSingleDashboard(busiId, id, { ...dashboard, configs: JSON.stringify(dashboardConfigs) });
setVariableConfig(dashboardConfigs);
valueWithOptions && setVariableConfigWithOptions(valueWithOptions);
};
const stopAutoRefresh = () => {
refreshRef.current?.closeRefresh();
};
const clusterMenu = (
<Menu selectedKeys={[curCluster]}>
{clusters.map((cluster) => (
<Menu.Item
key={cluster}
onClick={(_) => {
setCurCluster(cluster);
localStorage.setItem('curCluster', cluster);
init();
}}
>
{cluster}
</Menu.Item>
))}
</Menu>
);
return (
<PageLayout
customArea={
<div className='dashboard-detail-header'>
<div className='dashboard-detail-header-left'>
<RollbackOutlined className='back' onClick={() => history.push('/dashboards')} />
{titleEditing ? <Input ref={Ref} defaultValue={dashboard.name} onPressEnter={handleModifyTitle} /> : <div className='title'>{dashboard.name}</div>}
{!titleEditing ? (
<EditOutlined className='edit' onClick={handleEdit} />
) : (
<>
<Button size='small' style={{ marginRight: 5, marginLeft: 5 }} onClick={() => setTitleEditing(false)}>
取消
</Button>
<Button
size='small'
type='primary'
onClick={() => {
handleModifyTitle({ target: { value: Ref.current.state.value } });
}}
>
保存
</Button>
</>
)}
</div>
<div className='dashboard-detail-header-right'>
<Space>
<div style={{ display: 'flex', alignItems: 'center' }}>
集群:
<Dropdown overlay={clusterMenu}>
<Button>
{curCluster} <DownOutlined />
</Button>
</Dropdown>
</div>
<DateRangePicker onChange={handleDateChange} />
<Resolution onChange={(v) => setStep(v)} initialValue={step} />
<Refresh onRefresh={run} ref={refreshRef} />
</Space>
</div>
</div>
}
>
<div className='dashboard-detail-content'>
<div className='dashboard-detail-content-header'>
<div className='variable-area'>
<VariableConfig onChange={handleVariableChange} value={variableConfig} cluster={curCluster} range={range} id={id} onOpenFire={stopAutoRefresh} />
</div>
<DashboardLinks
value={dashboardLinks}
onChange={(v) => {
let dashboardConfigs: any = {};
try {
if (dashboard.configs) {
dashboardConfigs = JSON.parse(dashboard.configs);
}
} catch (e) {
console.error(e);
}
dashboardConfigs.links = v;
updateSingleDashboard(busiId, id, {
...dashboard,
configs: JSON.stringify(dashboardConfigs),
});
setDashboardLinks(v);
}}
/>
</div>
<div className='charts'>
{chartGroup.map((item, i) => (
<ChartGroup
id={id}
cluster={curCluster}
busiId={busiId}
key={i}
step={step}
groupInfo={item}
onAddChart={handleAddChart}
onUpdateChart={handleUpdateChart}
onCloneChart={handleCloneChart}
onShareChart={handleShareChart}
onUpdateChartGroup={handleUpdateChartGroup}
onMoveUpChartGroup={handleMoveUpChartGroup}
onMoveDownChartGroup={handleMoveDownChartGroup}
onDelChart={handleDelChart}
onDelChartGroup={handleDelChartGroup}
range={range}
refreshFlag={refreshFlag}
variableConfig={variableConfigWithOptions!}
moveUpEnable={i > 0}
moveDownEnable={i < chartGroup.length - 1}
/>
))}
<Button
block
icon={<PlusOutlined />}
style={{
paddingRight: 0,
}}
onClick={() => {
groupForm.setFieldsValue({ name: '' });
setGroupModalVisible(true);
}}
>
{t('新增图表分组')}
</Button>
</div>
</div>
<Modal
title={isAddGroup ? t('新建分组') : t('更新分组名称')}
visible={groupModalVisible}
onOk={handleAddOrUpdateChartGroup}
onCancel={() => {
setGroupModalVisible(false);
}}
>
<Form {...layout} form={groupForm}>
<Form.Item
label={t('分组名称')}
name='name'
rules={[
{
required: true,
message: t('请输入名称'),
},
]}
>
<Input />
</Form.Item>
</Form>
</Modal>
{chartModalVisible && (
<ChartConfigModal
id={id}
cluster={curCluster}
busiId={busiId}
initialValue={chartModalInitValue}
groupId={groupId}
show={chartModalVisible}
onVisibleChange={handleChartConfigVisibleChange}
variableConfig={variableConfigWithOptions}
/>
)}
</PageLayout>
);
}
Example #21
Source File: StudentBanner.tsx From office-hours with GNU General Public License v3.0 | 4 votes |
export default function StudentBanner({
queueId,
editQuestion,
leaveQueue,
}: StudentBannerProps): ReactElement {
const { studentQuestion, studentQuestionIndex } = useStudentQuestion(queueId);
const isQueueOnline = useQueue(queueId).queue?.room.startsWith("Online");
switch (studentQuestion?.status) {
case "Drafting":
return (
<Banner
titleColor="#faad14"
contentColor="#ffd666"
title="Please finish writing your question"
content="Your spot in queue has been temporarily reserved. Please finish describing your question to receive help and finish joining the queue."
buttons={
<>
<Tooltip title="Delete Draft">
<BannerButton
icon={<DeleteRowOutlined />}
onClick={leaveQueue}
/>
</Tooltip>
<Tooltip title="Finish Draft">
<BannerButton
data-cy="edit-question"
icon={<EditOutlined />}
onClick={async () => {
editQuestion();
}}
/>
</Tooltip>
</>
}
/>
);
case "Queued":
return (
<Banner
titleColor="#3684C6"
contentColor="#ABD4F3"
title={
<span>
You are{" "}
<BoldNumber>{toOrdinal(studentQuestionIndex + 1)}</BoldNumber> in
queue
</span>
}
buttons={
<>
<LeaveQueueButton leaveQueue={leaveQueue} />
<Tooltip title="Edit Question">
<BannerButton
data-cy="edit-question"
icon={<EditOutlined />}
onClick={editQuestion}
/>
</Tooltip>
</>
}
content={<QuestionDetailRow studentQuestion={studentQuestion} />}
/>
);
case "Helping":
return (
<Banner
titleColor="#66BB6A"
contentColor="#82C985"
title={
<span>
<BoldNumber>{studentQuestion.taHelped.name}</BoldNumber> is coming
to help you
</span>
}
buttons={
<>
<LeaveQueueButton leaveQueue={leaveQueue} />
{isQueueOnline && (
<Tooltip title="Open Teams DM">
<BannerButton
icon={<TeamOutlined />}
onClick={() => {
window.open(
`https://teams.microsoft.com/l/chat/0/0?users=${studentQuestion.taHelped.email}`
);
}}
/>
</Tooltip>
)}
</>
}
content={
<Bullets>
<li>Please be dressed appropriately</li>
<li>Be respectful of the TA’s time</li>
<li>Come prepared with your question!</li>
</Bullets>
}
/>
);
case "ReQueueing":
return (
<Banner
titleColor="#66BB6A"
contentColor="#82C985"
title={<span>Are you ready to re-join the queue?</span>}
buttons={
<>
<LeaveQueueButton leaveQueue={leaveQueue} />
<Tooltip title="Rejoin Queue">
<Button
shape="circle"
style={{
marginLeft: "16px",
border: 0,
}}
icon={<UndoOutlined />}
onClick={async () => {
await API.questions.update(studentQuestion.id, {
status: OpenQuestionStatus.PriorityQueued,
});
}}
type="primary"
data-cy="re-join-queue"
size="large"
/>
</Tooltip>
</>
}
content={
<Bullets>
<li>Have you finished doing what the TA has told you?</li>
<li>
Once you hit requeue, you will be placed at the top of the queue
</li>
</Bullets>
}
/>
);
case "PriorityQueued":
return (
<Banner
titleColor="#3684C6"
contentColor="#ABD4F3"
title={
<PriorityQueuedBanner>
You are now in a priority queue, you will be helped soon. <br />
<span style={{ fontSize: 16 }}>
You were last helped by{" "}
<span style={{ fontWeight: "bold" }}>
{studentQuestion.taHelped.name}
</span>
.
</span>
</PriorityQueuedBanner>
}
buttons={
<>
<LeaveQueueButton leaveQueue={leaveQueue} />
<Tooltip title="Edit Question">
<BannerButton
data-cy="edit-question"
icon={<EditOutlined />}
onClick={editQuestion}
/>
</Tooltip>
</>
}
content={<QuestionDetailRow studentQuestion={studentQuestion} />}
/>
);
default:
return <div />;
}
}
Example #22
Source File: List.tsx From fe-v5 with Apache License 2.0 | 4 votes |
export default function List(props: IProps) {
const [list, setList] = useState([]);
const [active, setActive] = useState<number>();
const [search, setSearch] = useState('');
const [refreshFlag, setRefreshFlag] = useState(_.uniqueId('refreshFlag_'));
const { profile } = useSelector<AccountRootState, accountStoreState>((state) => state.account);
useEffect(() => {
const defaultMetricViewId = localStorage.getItem('metric-view-id') !== null ? Number(localStorage.getItem('metric-view-id')) : null;
getList().then((res) => {
setList(res);
let curId;
if (!defaultMetricViewId || !_.find(res, { id: defaultMetricViewId })) {
curId = _.get(_.head(res), 'id');
} else {
curId = defaultMetricViewId;
}
if (curId) {
setActive(curId);
const curItem = _.find(res, { id: curId });
let configs = {} as IMatch;
try {
configs = JSON.parse(curItem.configs);
configs.id = curId;
configs.refreshFlag = refreshFlag;
} catch (e) {
console.error(e);
}
props.onSelect({
...configs,
});
}
});
}, [refreshFlag]);
return (
<div className='n9e-metric-views-list'>
<div className='n9e-metric-views-list-header'>
<div className='metric-page-title'>快捷视图列表</div>
<a>
<PlusSquareOutlined
onClick={() => {
Form({
admin: profile.admin,
action: 'add',
visible: true,
range: props.range,
onOk: (record) => {
localStorage.setItem('metric-view-id', record.id);
setRefreshFlag(_.uniqueId('refreshFlag_'));
},
});
}}
/>
</a>
</div>
<Input
prefix={<SearchOutlined />}
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
<div className='n9e-metric-views-list-content'>
{_.isEmpty(list)
? '暂无数据'
: _.map(
_.filter(list, (item) => {
if (search) {
let result = true;
try {
const reg = new RegExp(search, 'gi');
result = reg.test(item.name);
} catch (e) {
console.log(e);
}
return result;
}
return true;
}),
(item) => {
return (
<div
className={classNames({
'n9e-metric-views-list-content-item': true,
active: item.id === active,
})}
key={item.id}
onClick={() => {
setActive(item.id);
localStorage.setItem('metric-view-id', item.id);
const curItem = _.find(list, { id: item.id });
let configs = {} as IMatch;
try {
configs = JSON.parse(curItem.configs);
configs.id = item.id;
} catch (e) {
console.error(e);
}
props.onSelect({
...configs,
});
}}
>
<span className='name'>{item.name}</span>
{item.cate === 1 || profile.admin ? (
<span>
{item.cate === 0 && (
<span className='n9e-metric-views-list-content-item-cate' style={{ color: '#ccc' }}>
公开
</span>
)}
<div className='n9e-metric-views-list-content-item-opes'>
<EditOutlined
onClick={(e) => {
e.stopPropagation();
let configs = {} as any;
try {
configs = JSON.parse(item.configs);
configs.dynamicLabels = _.map(configs.dynamicLabels, 'label');
configs.dimensionLabels = _.map(configs.dimensionLabels, 'label');
} catch (e) {
console.error(e);
}
const initialValues = {
id: item.id,
name: item.name,
cate: item.cate === 0,
...configs,
};
Form({
admin: profile.admin,
action: 'edit',
visible: true,
range: props.range,
initialValues,
onOk: () => {
localStorage.setItem('metric-view-id', item.id);
setRefreshFlag(_.uniqueId('refreshFlag_'));
},
});
}}
/>
<DeleteOutlined
onClick={(e) => {
e.stopPropagation();
Modal.confirm({
title: '是否要删除?',
onOk: () => {
deleteMetricView({
ids: [item.id],
}).then(() => {
message.success('删除成功');
setRefreshFlag(_.uniqueId('refreshFlag_'));
});
},
});
}}
/>
<Tooltip title='导出配置' placement='right'>
<ExportOutlined
onClick={() => {
Export({
visible: true,
data: item.configs,
});
}}
/>
</Tooltip>
</div>
</span>
) : (
<span style={{ color: '#ccc' }}>公开</span>
)}
</div>
);
},
)}
</div>
</div>
);
}
Example #23
Source File: CollectionCell.tsx From Protoman with MIT License | 4 votes |
CollectionCell: React.FunctionComponent<Props> = ({ collectionName }) => {
const dispatch = useDispatch();
const collection = useSelector((s: AppState) => getByKey(s.collections, collectionName));
const collectionNames = useSelector(selectColNames);
const [menuVisible, setMenuVisible] = React.useState(false);
const [isEditingName, setIsEditingName] = React.useState(false);
const [isInvalidName, setIsInvalidName] = React.useState(false);
const [draftName, setDraftName] = React.useState(collectionName);
React.useEffect(() => {
setDraftName(collectionName);
}, [collectionName]);
function showMenu(): void {
setMenuVisible(true);
}
function hideMenu(): void {
setMenuVisible(false);
}
function startEditing(): void {
setIsEditingName(true);
hideMenu();
}
function stopEditing(): void {
setIsEditingName(false);
}
const collectionSize = Object.keys(collection?.flows || {}).length;
function handleDelete(): void {
if (collectionNames.length > 1) {
dispatch(deleteCollection(collectionName));
} else {
message.error("Can't delete the last collection");
}
hideMenu();
}
function checkName(newName: string): boolean {
return validateCollectionName(newName, collectionName, collectionNames);
}
function handleNameChange(newName: string): void {
if (checkName(newName)) {
dispatch(changeCollectionName(collectionName, newName));
}
}
function handleOpenFM(): void {
dispatch(openFM(collectionName));
hideMenu();
}
function validateFlowName(flowName: string): boolean {
return !collection?.flows?.map(([n]) => n)?.includes(flowName);
}
function handleCreate(): void {
const tmpName = 'Request';
let tmpNameIdx = 1;
while (!validateFlowName(`${tmpName}${tmpNameIdx}`)) tmpNameIdx++;
dispatch(createFlow(collectionName, `${tmpName}${tmpNameIdx}`));
hideMenu();
}
function handleExport(): void {
if (collection) {
// error display is done in index.ts
exportCollection(collectionName, collection);
}
}
const menu = (
<>
<Button type="link" onClick={prevent(handleOpenFM)}>
<FilePptOutlined />
Manage .proto files
</Button>
<Separator />
<Button type="link" onClick={prevent(handleCreate)}>
<PlusOutlined />
New Request
</Button>
<Separator />
<Button type="link" onClick={prevent(startEditing)}>
<EditOutlined />
Edit Name
</Button>
<Separator />
<Button type="link" onClick={prevent(handleExport)}>
<ExportOutlined />
Export Collection
</Button>
<Separator />
<Button type="link" danger onClick={prevent(handleDelete)}>
<DeleteOutlined />
Delete Collection
</Button>
</>
);
return (
<Popover
placement="rightTop"
content={menu}
visible={menuVisible}
trigger="contextMenu"
onVisibleChange={setMenuVisible}
>
<TableData onContextMenu={prevent(showMenu)}>
{isEditingName ? (
<Form.Item
validateStatus={isInvalidName ? 'error' : ''}
style={{ margin: 0 }}
help={isInvalidName ? 'Invalid Name' : ''}
>
<TitleInput
value={draftName}
onChange={(e): void => {
setIsInvalidName(!checkName(e.target.value));
setDraftName(e.target.value);
}}
onKeyDown={(e): void => {
switch (e.keyCode) {
case 27: // esc
setDraftName(collectionName);
stopEditing();
break;
case 13: // enter
if (!isInvalidName) {
handleNameChange(draftName);
stopEditing();
}
}
}}
onClick={prevent(e => e)}
/>
</Form.Item>
) : (
<Title>{draftName}</Title>
)}
<Description>
{collectionSize} {collectionSize === 1 ? 'entry' : 'entries'}
</Description>
</TableData>
</Popover>
);
}
Example #24
Source File: FolderTree.tsx From datart with Apache License 2.0 | 4 votes |
export function FolderTree({
selectedId,
treeData,
i18nPrefix,
}: FolderTreeProps) {
const tg = useI18NPrefix('global');
const dispatch = useDispatch();
const history = useHistory();
const orgId = useSelector(selectOrgId);
const loading = useSelector(selectVizListLoading);
const vizsData = useSelector(selectVizs);
const { showSaveForm } = useContext(SaveFormContext);
const saveAsViz = useSaveAsViz();
useEffect(() => {
dispatch(getFolders(orgId));
}, [dispatch, orgId]);
const redirect = useCallback(
tabKey => {
if (tabKey) {
history.push(`/organizations/${orgId}/vizs/${tabKey}`);
} else {
history.push(`/organizations/${orgId}/vizs`);
}
},
[history, orgId],
);
const menuSelect = useCallback(
(_, { node }) => {
if (node.relType !== 'FOLDER') {
history.push(`/organizations/${orgId}/vizs/${node.relId}`);
}
},
[history, orgId],
);
const archiveViz = useCallback(
({ id: folderId, relId, relType }) =>
() => {
let id = folderId;
let archive = false;
let msg = tg('operation.deleteSuccess');
if (['DASHBOARD', 'DATACHART'].includes(relType)) {
id = relId;
archive = true;
msg = tg('operation.archiveSuccess');
}
dispatch(
deleteViz({
params: { id, archive },
type: relType,
resolve: () => {
message.success(msg);
dispatch(removeTab({ id, resolve: redirect }));
},
}),
);
},
[dispatch, redirect, tg],
);
const moreMenuClick = useCallback(
node =>
({ key, domEvent }) => {
domEvent.stopPropagation();
switch (key) {
case 'info':
showSaveForm({
vizType: node.relType,
type: CommonFormTypes.Edit,
visible: true,
initialValues: { ...node, parentId: node.parentId || void 0 },
onSave: (values, onClose) => {
let index = node.index;
if (isParentIdEqual(node.parentId, values.parentId)) {
index = getInsertedNodeIndex(values, vizsData);
}
dispatch(
editFolder({
folder: {
...values,
parentId: values.parentId || null,
index,
},
resolve: onClose,
}),
);
},
});
break;
case 'delete':
break;
case 'saveAs':
saveAsViz(node.relId, node.relType);
break;
default:
break;
}
},
[dispatch, showSaveForm, vizsData, saveAsViz],
);
const renderTreeTitle = useCallback(
node => {
const { isFolder, title, path, relType } = node;
return (
<TreeTitle>
<h4>{`${title}`}</h4>
<CascadeAccess
module={ResourceTypes.Viz}
path={path}
level={PermissionLevels.Manage}
>
<Popup
trigger={['click']}
placement="bottom"
content={
<Menu
prefixCls="ant-dropdown-menu"
selectable={false}
onClick={moreMenuClick(node)}
>
<MenuListItem
key="info"
prefix={<EditOutlined className="icon" />}
>
{tg('button.info')}
</MenuListItem>
{!isFolder && (
<MenuListItem
key="saveAs"
prefix={<CopyFilled className="icon" />}
>
{tg('button.saveAs')}
</MenuListItem>
)}
<MenuListItem
key="delete"
prefix={<DeleteOutlined className="icon" />}
>
<Popconfirm
title={`${
relType === 'FOLDER'
? tg('operation.deleteConfirm')
: tg('operation.archiveConfirm')
}`}
onConfirm={archiveViz(node)}
>
{relType === 'FOLDER'
? tg('button.delete')
: tg('button.archive')}
</Popconfirm>
</MenuListItem>
</Menu>
}
>
<span className="action" onClick={stopPPG}>
<MoreOutlined />
</span>
</Popup>
</CascadeAccess>
</TreeTitle>
);
},
[moreMenuClick, archiveViz, tg],
);
const onDrop = info => {
onDropTreeFn({
info,
treeData,
callback: (id, parentId, index) => {
dispatch(
editFolder({
folder: {
id,
parentId,
index: index,
},
resolve: () => {},
}),
);
},
});
};
return (
<Tree
loading={loading}
treeData={treeData}
titleRender={renderTreeTitle}
onSelect={menuSelect}
onDrop={onDrop}
{...(selectedId && { selectedKeys: [selectedId] })}
defaultExpandAll
draggable
/>
);
}
Example #25
Source File: index.tsx From amiya with MIT License | 4 votes |
export default function Demo() {
return (
<AySearchTable
api={listApi}
rowKey="id"
title="员工列表"
dialogFormExtend={{ addApi, updateApi }}
deleteApi={deleteApi}
ctrl={ctrl}
>
<AyFields>
<AyField
title="头像"
key="avatar"
width={70}
renderType="image"
props={{
width: 60,
height: 60
}}
dialog={{
title: '选择头像',
type: 'card-group',
tooltip: '选择一个喜欢的头像',
required: true,
cardStyle: {
padding: 2
},
options: [
require('../images/avatar1.jpg'),
require('../images/avatar2.jpg'),
require('../images/avatar3.jpg'),
require('../images/avatar4.jpg'),
require('../images/avatar5.jpg')
].map(src => {
return {
label: <img src={src} width="50" height="50" />,
value: src
}
})
}}
/>
<AyField title="用户名称" key="nickname" search dialog={{ required: true }} />
<AyField title="登录账号" key="username" search dialog={{ required: true }} />
<AyField
title="所属角色"
key="character"
type="select"
search
renderType="group"
split="\"
size={0}
tooltip="当前绑定的角色"
after={
<AyButton tooltip="编辑角色" type="link" icon={<EditOutlined />} onClick={() => message.info('未实现')} />
}
options={[
{ label: '超级管理员', value: 1 },
{ label: '财务', value: 2 },
{ label: '运营', value: 3 }
]}
/>
<AyField title="邮箱地址" key="email" dialog />
<AyField
title="第三方绑定"
key="linkAccount"
renderType="group"
after={
<div>
<a onClick={() => message.info('未实现')}>添加绑定</a>
</div>
}
options={[
{ label: <WeiboCircleOutlined style={{ color: '#d52c2b', fontSize: 20 }} />, value: 'weibo' },
{ label: <QqOutlined style={{ color: '#333', fontSize: 20 }} />, value: 'qq' },
{ label: <WechatOutlined style={{ color: '#03dc6c', fontSize: 20 }} />, value: 'wechat' },
{ label: <TwitterOutlined style={{ color: '#1d9bf0', fontSize: 20 }} />, value: 'twitter' }
]}
/>
<AyField
title="在职状态"
key="status"
type="select"
search
renderType="status"
dialog={{
type: 'radio-group',
required: true,
defaultValue: 1,
optionType: 'button'
}}
options={[
{ label: '在职', value: 1, color: 'green' },
{ label: '离职', value: 2, color: 'red' },
{ label: '退休', value: 3, color: 'cyan' },
{ label: '停薪', value: 4, color: 'purple' }
]}
/>
</AyFields>
<Space size="large">
<Text type="secondary">初始登录密码:111111</Text>
<AyAction action="add">新增员工</AyAction>
</Space>
</AySearchTable>
)
}
Example #26
Source File: MainOperator.tsx From yakit with GNU Affero General Public License v3.0 | 4 votes |
Main: React.FC<MainProp> = (props) => {
const [engineStatus, setEngineStatus] = useState<"ok" | "error">("ok")
const [status, setStatus] = useState<{ addr: string; isTLS: boolean }>()
const [collapsed, setCollapsed] = useState(false)
const [hideMenu, setHideMenu] = useState(false)
const [loading, setLoading] = useState(false)
const [menuItems, setMenuItems] = useState<MenuItemGroup[]>([])
const [routeMenuData, setRouteMenuData] = useState<MenuDataProps[]>(RouteMenuData)
const [notification, setNotification] = useState("")
const [pageCache, setPageCache] = useState<PageCache[]>([
{
verbose: "MITM",
route: Route.HTTPHacker,
singleNode: ContentByRoute(Route.HTTPHacker),
multipleNode: []
}
])
const [currentTabKey, setCurrentTabKey] = useState<string>(Route.HTTPHacker)
// 系统类型
const [system, setSystem] = useState<string>("")
useEffect(() => {
ipcRenderer.invoke('fetch-system-name').then((res) => setSystem(res))
}, [])
// yakit页面关闭是否二次确认提示
const [winCloseFlag, setWinCloseFlag] = useState<boolean>(true)
const [winCloseShow, setWinCloseShow] = useState<boolean>(false)
useEffect(() => {
ipcRenderer
.invoke("get-value", WindowsCloseFlag)
.then((flag: any) => setWinCloseFlag(flag === undefined ? true : flag))
}, [])
// 获取自定义菜单
const updateMenuItems = () => {
setLoading(true)
// Fetch User Defined Plugins
ipcRenderer
.invoke("GetAllMenuItem", {})
.then((data: { Groups: MenuItemGroup[] }) => {
setMenuItems(data.Groups)
})
.catch((e: any) => failed("Update Menu Item Failed"))
.finally(() => setTimeout(() => setLoading(false), 300))
// Fetch Official General Plugins
ipcRenderer
.invoke("QueryYakScript", {
Pagination: genDefaultPagination(1000),
IsGeneralModule: true,
Type: "yak"
} as QueryYakScriptRequest)
.then((data: QueryYakScriptsResponse) => {
const tabList: MenuDataProps[] = cloneDeep(RouteMenuData)
for (let item of tabList) {
if (item.subMenuData) {
if (item.key === Route.GeneralModule) {
const extraMenus: MenuDataProps[] = data.Data.map((i) => {
return {
icon: <EllipsisOutlined/>,
key: `plugin:${i.Id}`,
label: i.ScriptName,
} as unknown as MenuDataProps
})
item.subMenuData.push(...extraMenus)
}
item.subMenuData.sort((a, b) => a.label.localeCompare(b.label))
}
}
setRouteMenuData(tabList)
})
}
useEffect(() => {
updateMenuItems()
ipcRenderer.on("fetch-new-main-menu", (e) => {
updateMenuItems()
})
return () => {
ipcRenderer.removeAllListeners("fetch-new-main-menu")
}
}, [])
useEffect(() => {
if (engineStatus === "error") props.onErrorConfirmed && props.onErrorConfirmed()
}, [engineStatus])
// 整合路由对应名称
const pluginKey = (item: PluginMenuItem) => `plugin:${item.Group}:${item.YakScriptId}`;
const routeKeyToLabel = new Map<string, string>();
routeMenuData.forEach(k => {
(k.subMenuData || []).forEach(subKey => {
routeKeyToLabel.set(`${subKey.key}`, subKey.label)
})
routeKeyToLabel.set(`${k.key}`, k.label)
})
menuItems.forEach((k) => {
k.Items.forEach((value) => {
routeKeyToLabel.set(pluginKey(value), value.Verbose)
})
})
// Tabs Bar Operation Function
const getCacheIndex = (route: string) => {
const targets = pageCache.filter((i) => i.route === route)
return targets.length > 0 ? pageCache.indexOf(targets[0]) : -1
}
const addTabPage = useMemoizedFn(
(route: Route, nodeParams?: { time?: string; node: ReactNode; isRecord?: boolean }) => {
const filterPage = pageCache.filter((i) => i.route === route)
const filterPageLength = filterPage.length
if (singletonRoute.includes(route)) {
if (filterPageLength > 0) {
setCurrentTabKey(route)
} else {
const tabName = routeKeyToLabel.get(route) || `${route}`
setPageCache([
...pageCache,
{
verbose: tabName,
route: route,
singleNode: ContentByRoute(route),
multipleNode: []
}
])
setCurrentTabKey(route)
}
} else {
if (filterPageLength > 0) {
const tabName = routeKeyToLabel.get(route) || `${route}`
const tabId = `${route}-[${randomString(49)}]`
const time = new Date().getTime().toString()
const node: multipleNodeInfo = {
id: tabId,
verbose: `${tabName}-[${filterPage[0].multipleNode.length + 1}]`,
node: nodeParams && nodeParams.node ? nodeParams?.node || <></> : ContentByRoute(route),
time: nodeParams && nodeParams.node ? nodeParams?.time || time : time
}
const pages = pageCache.map((item) => {
if (item.route === route) {
item.multipleNode.push(node)
item.multipleCurrentKey = tabId
return item
}
return item
})
setPageCache([...pages])
setCurrentTabKey(route)
if (nodeParams && !!nodeParams.isRecord) addFuzzerList(nodeParams?.time || time)
} else {
const tabName = routeKeyToLabel.get(route) || `${route}`
const tabId = `${route}-[${randomString(49)}]`
const time = new Date().getTime().toString()
const node: multipleNodeInfo = {
id: tabId,
verbose: `${tabName}-[1]`,
node: nodeParams && nodeParams.node ? nodeParams?.node || <></> : ContentByRoute(route),
time: nodeParams && nodeParams.node ? nodeParams?.time || time : time
}
setPageCache([
...pageCache,
{
verbose: tabName,
route: route,
singleNode: undefined,
multipleNode: [node],
multipleCurrentKey: tabId
}
])
setCurrentTabKey(route)
if (nodeParams && !!nodeParams.isRecord) addFuzzerList(nodeParams?.time || time)
}
}
}
)
const menuAddPage = useMemoizedFn((route: Route) => {
if (route === "ignore") return
if (route === Route.HTTPFuzzer) {
const time = new Date().getTime().toString()
addTabPage(Route.HTTPFuzzer, {
time: time,
node: ContentByRoute(Route.HTTPFuzzer, undefined, {
system: system,
order: time
}),
isRecord: true
})
} else addTabPage(route as Route)
})
const removePage = (route: string) => {
const targetIndex = getCacheIndex(route)
if (targetIndex > 0 && pageCache[targetIndex - 1]) {
const targetCache = pageCache[targetIndex - 1]
setCurrentTabKey(targetCache.route)
}
if (targetIndex === 0 && pageCache[targetIndex + 1]) {
const targetCache = pageCache[targetIndex + 1]
setCurrentTabKey(targetCache.route)
}
if (targetIndex === 0 && pageCache.length === 1) setCurrentTabKey("")
setPageCache(pageCache.filter((i) => i.route !== route))
if (route === Route.HTTPFuzzer) delFuzzerList(1)
}
const updateCacheVerbose = (id: string, verbose: string) => {
const index = getCacheIndex(id)
if (index < 0) return
pageCache[index].verbose = verbose
setPageCache([...pageCache])
}
const setMultipleCurrentKey = useMemoizedFn((key: string, type: Route) => {
const arr = pageCache.map(item => {
if (item.route === type) {
item.multipleCurrentKey = key
return item
}
return item
})
setPageCache([...arr])
})
const removeMultipleNodePage = useMemoizedFn((key: string, type: Route) => {
const removeArr: multipleNodeInfo[] = pageCache.filter(item => item.route === type)[0]?.multipleNode || []
if (removeArr.length === 0) return
const nodes = removeArr.filter(item => item.id === key)
const time = nodes[0].time
let index = 0
for (let i in removeArr) {
if (removeArr[i].id === key) {
index = +i
break
}
}
if (index === 0 && removeArr.length === 1) {
removePage(type)
return
}
let current = ""
let filterArr: multipleNodeInfo[] = []
if (index > 0 && removeArr[index - 1]) {
current = removeArr[index - 1].id
filterArr = removeArr.filter(item => item.id !== key)
}
if (index === 0 && removeArr[index + 1]) {
current = removeArr[index + 1].id
filterArr = removeArr.filter(item => item.id !== key)
}
if (current) {
const arr = pageCache.map(item => {
if (item.route === type) {
item.multipleNode = [...filterArr]
item.multipleCurrentKey = current
return item
}
return item
})
setPageCache([...arr])
if (type === Route.HTTPFuzzer) delFuzzerList(2, time)
}
})
const removeOtherMultipleNodePage = useMemoizedFn((key: string, type: Route) => {
const removeArr: multipleNodeInfo[] = pageCache.filter(item => item.route === type)[0]?.multipleNode || []
if (removeArr.length === 0) return
const nodes = removeArr.filter(item => item.id === key)
const time = nodes[0].time
const arr = pageCache.map(item => {
if (item.route === type) {
item.multipleNode = [...nodes]
item.multipleCurrentKey = key
return item
}
return item
})
setPageCache([...arr])
if (type === Route.HTTPFuzzer) delFuzzerList(3, time)
})
// 全局记录鼠标坐标位置(为右键菜单提供定位)
const coordinateTimer = useRef<any>(null)
useEffect(() => {
document.onmousemove = (e) => {
const {screenX, screenY, clientX, clientY, pageX, pageY} = e
if (coordinateTimer.current) {
clearTimeout(coordinateTimer.current)
coordinateTimer.current = null
}
coordinateTimer.current = setTimeout(() => {
coordinate.screenX = screenX
coordinate.screenY = screenY
coordinate.clientX = clientX
coordinate.clientY = clientY
coordinate.pageX = pageX
coordinate.pageY = pageY
}, 50);
}
}, [])
// 全局注册快捷键功能
const documentKeyDown = useMemoizedFn((e: any) => {
// ctrl + w 关闭tab页面
if (e.code === "KeyW" && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
if (pageCache.length === 0) return
setLoading(true)
removePage(currentTabKey)
setTimeout(() => setLoading(false), 300);
return
}
})
useEffect(() => {
document.onkeydown = documentKeyDown
}, [])
// fuzzer本地缓存
const fuzzerList = useRef<Map<string, fuzzerInfoProp>>(new Map<string, fuzzerInfoProp>())
const saveFuzzerList = debounce(() => {
const historys: fuzzerInfoProp[] = []
fuzzerList.current.forEach((value) => historys.push(value))
historys.sort((a, b) => +a.time - +b.time)
const filters = historys.filter(item => (item.request || "").length < 1000000 && (item.request || "").length > 0)
ipcRenderer.invoke("set-value", FuzzerCache, JSON.stringify(filters.slice(-5)))
}, 500)
const fetchFuzzerList = useMemoizedFn(() => {
setLoading(true)
fuzzerList.current.clear()
ipcRenderer
.invoke("get-value", FuzzerCache)
.then((res: any) => {
const cache = JSON.parse(res || "[]")
for (let item of cache) {
const time = new Date().getTime().toString()
fuzzerList.current.set(time, {...item, time: time})
addTabPage(Route.HTTPFuzzer, {
time: time,
node:
ContentByRoute(
Route.HTTPFuzzer,
undefined,
{
isHttps: item.isHttps || false,
request: item.request || "",
fuzzerParams: item,
system: system,
order: time
}
)
})
}
})
.catch((e) => console.info(e))
.finally(() => setTimeout(() => setLoading(false), 300))
})
const addFuzzerList = (key: string, request?: string, isHttps?: boolean) => {
fuzzerList.current.set(key, {request, isHttps, time: key})
}
const delFuzzerList = (type: number, key?: string) => {
if (type === 1) fuzzerList.current.clear()
if (type === 2 && key) if (fuzzerList.current.has(key)) fuzzerList.current.delete(key)
if (type === 3 && key) {
const info = fuzzerList.current.get(key)
if (info) {
fuzzerList.current.clear()
fuzzerList.current.set(key, info)
}
}
saveFuzzerList()
}
const updateFuzzerList = (key: string, param: fuzzerInfoProp) => {
fuzzerList.current.set(key, param)
saveFuzzerList()
}
useEffect(() => {
ipcRenderer.on("fetch-fuzzer-setting-data", (e, res: any) => updateFuzzerList(res.key, JSON.parse(res.param)))
// 开发环境不展示fuzzer缓存
ipcRenderer.invoke("is-dev").then((flag) => {
if (!flag) fetchFuzzerList()
// fetchFuzzerList()
})
return () => ipcRenderer.removeAllListeners("fetch-fuzzer-setting-data")
}, [])
// 加载补全
useEffect(() => {
ipcRenderer.invoke("GetYakitCompletionRaw").then((data: { RawJson: Uint8Array }) => {
const completionJson = Buffer.from(data.RawJson).toString("utf8")
setCompletions(JSON.parse(completionJson) as CompletionTotal)
// success("加载 Yak 语言自动补全成功 / Load Yak IDE Auto Completion Finished")
})
}, [])
useEffect(() => {
ipcRenderer.invoke("yakit-connect-status").then((data) => {
setStatus(data)
})
ipcRenderer.on("client-engine-status-ok", (e, reason) => {
if (engineStatus !== "ok") setEngineStatus("ok")
})
ipcRenderer.on("client-engine-status-error", (e, reason) => {
if (engineStatus === "ok") setEngineStatus("error")
})
const updateEngineStatus = () => {
ipcRenderer
.invoke("engine-status")
.catch((e: any) => {
setEngineStatus("error")
})
.finally(() => {
})
}
let id = setInterval(updateEngineStatus, 3000)
return () => {
ipcRenderer.removeAllListeners("client-engine-status-error")
ipcRenderer.removeAllListeners("client-engine-status-ok")
clearInterval(id)
}
}, [])
useHotkeys("Ctrl+Alt+T", () => {
})
useEffect(() => {
ipcRenderer.invoke("query-latest-notification").then((e: string) => {
setNotification(e)
if (e) {
success(
<>
<Space direction={"vertical"}>
<span>来自于 yaklang.io 的通知</span>
<Button
type={"link"}
onClick={() => {
showModal({
title: "Notification",
content: (
<>
<MDEditor.Markdown source={e}/>
</>
)
})
}}
>
点击查看
</Button>
</Space>
</>
)
}
})
}, [])
// 新增数据对比页面
useEffect(() => {
ipcRenderer.on("main-container-add-compare", (e, params) => {
const newTabId = `${Route.DataCompare}-[${randomString(49)}]`;
const verboseNameRaw = routeKeyToLabel.get(Route.DataCompare) || `${Route.DataCompare}`;
addTabPage(Route.DataCompare, {node: ContentByRoute(Route.DataCompare, undefined, {system: system})})
// 区分新建对比页面还是别的页面请求对比的情况
ipcRenderer.invoke("created-data-compare")
})
return () => {
ipcRenderer.removeAllListeners("main-container-add-compare")
}
}, [pageCache])
// Global Sending Function(全局发送功能|通过发送新增功能页面)
const addFuzzer = useMemoizedFn((res: any) => {
const {isHttps, request} = res || {}
if (request) {
const time = new Date().getTime().toString()
addTabPage(Route.HTTPFuzzer, {
time: time,
node:
ContentByRoute(
Route.HTTPFuzzer,
undefined,
{
isHttps: isHttps || false,
request: request || "",
system: system,
order: time
}
)
})
addFuzzerList(time, request || "", isHttps || false)
}
})
const addScanPort = useMemoizedFn((res: any) => {
const {URL = ""} = res || {}
if (URL) {
addTabPage(Route.Mod_ScanPort, {
node: ContentByRoute(Route.Mod_ScanPort, undefined, {scanportParams: URL})
})
}
})
const addBrute = useMemoizedFn((res: any) => {
const {URL = ""} = res || {}
if (URL) {
addTabPage(Route.Mod_Brute, {
node: ContentByRoute(Route.Mod_Brute, undefined, {bruteParams: URL})
})
}
})
// 发送到专项漏洞检测modal-show变量
const [bugTestShow, setBugTestShow] = useState<boolean>(false)
const [bugList, setBugList] = useState<BugInfoProps[]>([])
const [bugTestValue, setBugTestValue] = useState<BugInfoProps[]>([])
const [bugUrl, setBugUrl] = useState<string>("")
const addBugTest = useMemoizedFn((type: number, res?: any) => {
const {URL = ""} = res || {}
if (type === 1 && URL) {
setBugUrl(URL)
ipcRenderer.invoke("get-value", CustomBugList)
.then((res: any) => {
setBugList(res ? JSON.parse(res) : [])
setBugTestShow(true)
})
.catch(() => {
})
}
if (type === 2) {
const filter = pageCache.filter(item => item.route === Route.PoC)
if (filter.length === 0) {
addTabPage(Route.PoC)
setTimeout(() => {
ipcRenderer.invoke("send-to-bug-test", {type: bugTestValue, data: bugUrl})
setBugTestValue([])
setBugUrl("")
}, 300);
} else {
ipcRenderer.invoke("send-to-bug-test", {type: bugTestValue, data: bugUrl})
setCurrentTabKey(Route.PoC)
setBugTestValue([])
setBugUrl("")
}
}
})
const addYakRunning = useMemoizedFn((res: any) => {
const {name = "", code = ""} = res || {}
const filter = pageCache.filter(item => item.route === Route.YakScript)
if (!name || !code) return false
if ((filter || []).length === 0) {
addTabPage(Route.YakScript)
setTimeout(() => {
ipcRenderer.invoke("send-to-yak-running", {name, code})
}, 300);
} else {
ipcRenderer.invoke("send-to-yak-running", {name, code})
setCurrentTabKey(Route.YakScript)
}
})
useEffect(() => {
ipcRenderer.on("fetch-send-to-tab", (e, res: any) => {
const {type, data = {}} = res
if (type === "fuzzer") addFuzzer(data)
if (type === "scan-port") addScanPort(data)
if (type === "brute") addBrute(data)
if (type === "bug-test") addBugTest(1, data)
if (type === "plugin-store") addYakRunning(data)
})
return () => {
ipcRenderer.removeAllListeners("fetch-send-to-tab")
}
}, [])
// Tabs Bar 组件
const closeAllCache = useMemoizedFn(() => {
Modal.confirm({
title: "确定要关闭所有 Tabs?",
content: "这样将会关闭所有进行中的进程",
onOk: () => {
delFuzzerList(1)
setPageCache([])
}
})
})
const closeOtherCache = useMemoizedFn((route: string) => {
Modal.confirm({
title: "确定要关闭除此之外所有 Tabs?",
content: "这样将会关闭所有进行中的进程",
onOk: () => {
const arr = pageCache.filter((i) => i.route === route)
setPageCache(arr)
if (route === Route.HTTPFuzzer) delFuzzerList(1)
}
})
})
const bars = (props: any, TabBarDefault: any) => {
return (
<TabBarDefault
{...props}
children={(barNode: React.ReactElement) => {
return (
<DropdownMenu
menu={{
data: [
{key: "all", title: "关闭所有Tabs"},
{key: "other", title: "关闭其他Tabs"}
]
}}
dropdown={{trigger: ["contextMenu"]}}
onClick={(key) => {
switch (key) {
case "all":
closeAllCache()
break
case "other":
closeOtherCache(barNode.key as Route)
break
default:
break
}
}}
>
{barNode}
</DropdownMenu>
)
}}
/>
)
}
return (
<Layout className="yakit-main-layout">
<AutoSpin spinning={loading}>
<Header className="main-laytou-header">
<Row>
<Col span={8}>
<Space>
<div style={{marginLeft: 18, textAlign: "center", height: 60}}>
<Image src={YakLogoBanner} preview={false} width={130} style={{marginTop: 6}}/>
</div>
<Divider type={"vertical"}/>
<YakVersion/>
<YakitVersion/>
{!hideMenu && (
<Button
style={{marginLeft: 4, color: "#207ee8"}}
type={"ghost"}
ghost={true}
onClick={(e) => {
setCollapsed(!collapsed)
}}
icon={collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
/>
)}
<Button
style={{marginLeft: 4, color: "#207ee8"}}
type={"ghost"}
ghost={true}
onClick={(e) => {
updateMenuItems()
}}
icon={<ReloadOutlined/>}
/>
</Space>
</Col>
<Col span={16} style={{textAlign: "right", paddingRight: 28}}>
<PerformanceDisplay/>
<RiskStatsTag professionalMode={true}/>
<Space>
{/* {status?.isTLS ? <Tag color={"green"}>TLS:通信已加密</Tag> : <Tag color={"red"}>通信未加密</Tag>} */}
{status?.addr && <Tag color={"geekblue"}>{status?.addr}</Tag>}
{/* <Tag color={engineStatus === "ok" ? "green" : "red"}>Yak 引擎状态:{engineStatus}</Tag> */}
<ReversePlatformStatus/>
<Dropdown forceRender={true} overlay={<Menu>
<Menu.Item key={"update"}>
<AutoUpdateYakModuleButton/>
</Menu.Item>
<Menu.Item key={"reverse-global"}>
<ConfigGlobalReverseButton/>
</Menu.Item>
</Menu>} trigger={["click"]}>
<Button icon={<SettingOutlined/>}>
配置
</Button>
</Dropdown>
<Button type={"link"} danger={true} icon={<PoweroffOutlined/>} onClick={() => {
if (winCloseFlag) setWinCloseShow(true)
else {
success("退出当前 Yak 服务器成功")
setEngineStatus("error")
}
}}/>
</Space>
</Col>
</Row>
</Header>
<Content
style={{
margin: 12,
backgroundColor: "#fff",
overflow: "auto"
}}
>
<Layout style={{height: "100%", overflow: "hidden"}}>
{!hideMenu && (
<Sider
style={{backgroundColor: "#fff", overflow: "auto"}}
collapsed={collapsed}
>
<Spin spinning={loading}>
<Space
direction={"vertical"}
style={{
width: "100%"
}}
>
<Menu
theme={"light"}
style={{}}
selectedKeys={[]}
mode={"inline"}
onSelect={(e) => {
if (e.key === "ignore") return
const flag = pageCache.filter(item => item.route === (e.key as Route)).length === 0
if (flag) menuAddPage(e.key as Route)
else setCurrentTabKey(e.key)
}}
>
{menuItems.map((i) => {
if (i.Group === "UserDefined") {
i.Group = "社区插件"
}
return (
<Menu.SubMenu icon={<EllipsisOutlined/>} key={i.Group}
title={i.Group}>
{i.Items.map((item) => {
if (item.YakScriptId > 0) {
return (
<MenuItem icon={<EllipsisOutlined/>}
key={`plugin:${item.Group}:${item.YakScriptId}`}>
<Text
ellipsis={{tooltip: true}}>{item.Verbose}</Text>
</MenuItem>
)
}
return (
<MenuItem icon={<EllipsisOutlined/>}
key={`batch:${item.Group}:${item.Verbose}:${item.MenuItemId}`}>
<Text
ellipsis={{tooltip: true}}>{item.Verbose}</Text>
</MenuItem>
)
})}
</Menu.SubMenu>
)
})}
{(routeMenuData || []).map((i) => {
if (i.subMenuData) {
return (
<Menu.SubMenu icon={i.icon} key={i.key} title={i.label}>
{(i.subMenuData || []).map((subMenu) => {
return (
<MenuItem icon={subMenu.icon} key={subMenu.key}
disabled={subMenu.disabled}>
<Text
ellipsis={{tooltip: true}}>{subMenu.label}</Text>
</MenuItem>
)
})}
</Menu.SubMenu>
)
}
return (
<MenuItem icon={i.icon} key={i.key} disabled={i.disabled}>
{i.label}
</MenuItem>
)
})}
</Menu>
</Space>
</Spin>
</Sider>
)}
<Content style={{
overflow: "hidden",
backgroundColor: "#fff",
marginLeft: 12,
height: "100%",
display: "flex",
flexFlow: "column"
}}>
<div style={{
padding: 12,
paddingTop: 8,
overflow: "hidden",
flex: "1",
display: "flex",
flexFlow: "column"
}}>
{pageCache.length > 0 ? (
<Tabs
style={{display: "flex", flex: "1"}}
tabBarStyle={{marginBottom: 8}}
className='main-content-tabs yakit-layout-tabs'
activeKey={currentTabKey}
onChange={setCurrentTabKey}
size={"small"}
type={"editable-card"}
renderTabBar={(props, TabBarDefault) => {
return bars(props, TabBarDefault)
}}
hideAdd={true}
onTabClick={(key, e) => {
const divExisted = document.getElementById("yakit-cursor-menu")
if (divExisted) {
const div: HTMLDivElement = divExisted as HTMLDivElement
const unmountResult = ReactDOM.unmountComponentAtNode(div)
if (unmountResult && div.parentNode) {
div.parentNode.removeChild(div)
}
}
}}
>
{pageCache.map((i) => {
return (
<Tabs.TabPane
forceRender={true}
key={i.route}
tab={i.verbose}
closeIcon={
<Space>
<Popover
trigger={"click"}
title={"修改名称"}
content={
<>
<Input
size={"small"}
defaultValue={i.verbose}
onBlur={(e) => updateCacheVerbose(i.route, e.target.value)}
/>
</>
}
>
<EditOutlined className='main-container-cion'/>
</Popover>
<CloseOutlined
className='main-container-cion'
onClick={() => removePage(i.route)}
/>
</Space>
}
>
<div
style={{
overflowY: NoScrollRoutes.includes(i.route)
? "hidden"
: "auto",
overflowX: "hidden",
height: "100%",
maxHeight: "100%"
}}
>
{i.singleNode ? (
i.singleNode
) : (
<MainTabs
currentTabKey={currentTabKey}
tabType={i.route}
pages={i.multipleNode}
currentKey={i.multipleCurrentKey || ""}
isShowAdd={true}
setCurrentKey={(key, type) => {
setMultipleCurrentKey(key, type as Route)
}}
removePage={(key, type) => {
removeMultipleNodePage(key, type as Route)
}}
removeOtherPage={(key, type) => {
removeOtherMultipleNodePage(key, type as Route)
}}
onAddTab={() => menuAddPage(i.route)}
></MainTabs>
)}
</div>
</Tabs.TabPane>
)
})}
</Tabs>
) : (
<></>
)}
</div>
</Content>
</Layout>
</Content>
</AutoSpin>
<Modal
visible={winCloseShow}
onCancel={() => setWinCloseShow(false)}
footer={[
<Button key='link' onClick={() => setWinCloseShow(false)}>
取消
</Button>,
<Button key='back' type='primary' onClick={() => {
success("退出当前 Yak 服务器成功")
setEngineStatus("error")
}}>
退出
</Button>
]}
>
<div style={{height: 40}}>
<ExclamationCircleOutlined style={{fontSize: 22, color: "#faad14"}}/>
<span style={{fontSize: 18, marginLeft: 15}}>提示</span>
</div>
<p style={{fontSize: 15, marginLeft: 37}}>是否要退出yakit操作界面,一旦退出,界面内打开内容除fuzzer页外都会销毁</p>
<div style={{marginLeft: 37}}>
<Checkbox
defaultChecked={!winCloseFlag}
value={!winCloseFlag}
onChange={() => {
setWinCloseFlag(!winCloseFlag)
ipcRenderer.invoke("set-value", WindowsCloseFlag, false)
}}
></Checkbox>
<span style={{marginLeft: 8}}>不再出现该提示信息</span>
</div>
</Modal>
<Modal
visible={bugTestShow}
onCancel={() => setBugTestShow(false)}
footer={[
<Button key='link' onClick={() => setBugTestShow(false)}>
取消
</Button>,
<Button key='back' type='primary' onClick={() => {
if ((bugTestValue || []).length === 0) return failed("请选择类型后再次提交")
addBugTest(2)
setBugTestShow(false)
}}>
确定
</Button>
]}
>
<ItemSelects
item={{
label: "专项漏洞类型",
style: {marginTop: 20}
}}
select={{
allowClear: true,
data: BugList.concat(bugList) || [],
optText: "title",
optValue: "key",
value: (bugTestValue || [])[0]?.key,
onChange: (value, option: any) => setBugTestValue(value ? [{
key: option?.key,
title: option?.title
}] : [])
}}
></ItemSelects>
</Modal>
</Layout>
)
}
Example #27
Source File: DestinationStatistics.tsx From jitsu with MIT License | 4 votes |
DestinationStatistics: React.FC<CommonDestinationPageProps> = () => {
const history = useHistory()
const services = useServices()
const params = useParams<StatisticsPageParams>()
const destination = destinationsStore.list.find(d => d._id === params.id)
const destinationUid = destination?._uid
const destinationReference = destinationsStore.getDestinationReferenceById(destinationUid)
const statisticsService = useMemo<IStatisticsService>(
() => new StatisticsService(services.backendApiClient, services.activeProject.id, true),
[]
)
const isSelfHosted = services.features.environment !== "jitsu_cloud"
// Events last 30 days
const lastMonthPushEvents = useLoaderAsObject<CombinedStatisticsDatePoint[]>(
monthlyDataLoader(destinationUid, destinationReference, "push", statisticsService),
[destinationUid]
)
const lastMonthPullEvents = useLoaderAsObject<CombinedStatisticsDatePoint[]>(
monthlyDataLoader(destinationUid, destinationReference, "pull", statisticsService),
[destinationUid]
)
// Last 24 hours
const lastDayPushEvents = useLoaderAsObject<CombinedStatisticsDatePoint[]>(
hourlyDataLoader(destinationUid, destinationReference, "push", statisticsService),
[destinationUid]
)
const lastDayPullEvents = useLoaderAsObject<CombinedStatisticsDatePoint[]>(
hourlyDataLoader(destinationUid, destinationReference, "pull", statisticsService),
[destinationUid]
)
const somethingIsLoading =
lastMonthPushEvents.isLoading ||
lastMonthPullEvents.isLoading ||
lastDayPushEvents.isLoading ||
lastDayPullEvents.isLoading
useEffect(() => {
currentPageHeaderStore.setBreadcrumbs(
{ title: "Destinations", link: projectRoute(destinationPageRoutes.root) },
{
title: (
<PageHeader
title={destinationReference ? params.id : "Destination Not Found"}
icon={destinationReference?.ui.icon}
mode={destinationReference ? "statistics" : null}
/>
),
}
)
}, [])
return destinationReference ? (
<>
<div className="flex flex-row space-x-2 justify-end mb-4">
<Button
type="ghost"
icon={<EditOutlined />}
size="large"
onClick={() =>
history.push(
projectRoute(destinationPageRoutes.editExact, {
id: params.id,
})
)
}
>
{"Edit Destination"}
</Button>
<Button
type="ghost"
icon={<UnorderedListOutlined />}
size="large"
onClick={() => history.push(projectRoute(destinationPageRoutes.root))}
>
{"Destinations List"}
</Button>
</div>
{isSelfHosted && (
<Row>
<span className={`text-secondaryText mb-4`}>
Jitsu 1.37 brought an update that enables for serving more fine-grained statistics data. The new charts will
not show the events processed by the previous versions of Jitsu.
</span>
</Row>
)}
<Row gutter={16} className={"mb-4"}>
<Col span={12}>
<Card title="Incoming events (last 30 days)" bordered={false} className="w-full" loading={somethingIsLoading}>
<StatisticsChart data={lastMonthPushEvents.data || []} granularity={"day"} />
</Card>
</Col>
<Col span={12}>
<Card
title="Incoming events (last 24 hours)"
bordered={false}
className="w-full"
loading={somethingIsLoading}
>
<StatisticsChart data={lastDayPushEvents.data || []} granularity={"hour"} />
</Card>
</Col>
</Row>
{destinationReference.syncFromSourcesStatus === "supported" && (
<Row gutter={16}>
<Col span={12}>
<Card
title="Rows synchronized from sources (last 30 days)"
bordered={false}
className="w-full"
loading={somethingIsLoading}
>
<StatisticsChart
data={lastMonthPullEvents.data || []}
granularity={"day"}
dataToDisplay={["success", "skip"]}
/>
</Card>
</Col>
<Col span={12}>
<Card
title="Rows synchronized from sources (last 24 hours)"
bordered={false}
className="w-full"
loading={somethingIsLoading}
>
<StatisticsChart
data={lastDayPullEvents.data || []}
granularity={"hour"}
dataToDisplay={["success", "skip"]}
/>
</Card>
</Col>
</Row>
)}
</>
) : (
<DestinationNotFound destinationId={params.id} />
)
}
Example #28
Source File: CarBrowserCards.tsx From jmix-frontend with Apache License 2.0 | 4 votes |
CarBrowserCards = observer((props: EntityListProps<Car>) => {
const {
entityList,
onEntityListChange,
onSelectEntity,
disabled: readOnlyMode
} = props;
const onOpenScreenError = useOpenScreenErrorCallback();
const onEntityDelete = useEntityDeleteCallback();
const {
items,
count,
executeListQuery,
listQueryResult: { loading, error },
handleDeleteBtnClick,
handleCreateBtnClick,
handleEditBtnClick,
handlePaginationChange,
goToParentScreen,
entityListState
} = useEntityList<Car>({
listQuery: SCR_CAR_LIST,
entityName: ENTITY_NAME,
routingPath: ROUTING_PATH,
entityList,
onEntityListChange,
onPagination: saveHistory,
onEntityDelete,
onOpenScreenError
});
const getEntityCardsActions = useMemo(() => {
if (readOnlyMode) {
return () => [];
}
return onSelectEntity
? (e: EntityInstance<Car>) => [
<Button
htmlType="button"
type="primary"
onClick={() => {
onSelectEntity(e);
goToParentScreen();
}}
>
<span>
<FormattedMessage id="common.selectEntity" />
</span>
</Button>
]
: (e: EntityInstance<Car>) => [
<EntityPermAccessControl entityName={ENTITY_NAME} operation="delete">
<DeleteOutlined
role={"button"}
key="delete"
onClick={(event?: React.MouseEvent) =>
handleDeleteBtnClick(event, e.id)
}
/>
</EntityPermAccessControl>,
<EntityPermAccessControl entityName={ENTITY_NAME} operation="update">
<EditOutlined
role={"button"}
key="edit"
onClick={(event?: React.MouseEvent) =>
handleEditBtnClick(event, e.id)
}
/>
</EntityPermAccessControl>
];
}, [
onSelectEntity,
handleDeleteBtnClick,
handleEditBtnClick,
goToParentScreen,
readOnlyMode
]);
if (error != null) {
console.error(error);
return <RetryDialog onRetry={executeListQuery} />;
}
if (loading || items == null) {
return <Spinner />;
}
return (
<div className={styles.narrowLayout}>
<div style={{ marginBottom: "12px" }}>
{(entityList != null || onSelectEntity != null) && (
<Tooltip title={<FormattedMessage id="common.back" />}>
<Button
htmlType="button"
style={{ margin: "0 12px 12px 0" }}
icon={<LeftOutlined />}
onClick={goToParentScreen}
key="back"
type="default"
shape="circle"
/>
</Tooltip>
)}
{onSelectEntity == null && !readOnlyMode && (
<EntityPermAccessControl entityName={ENTITY_NAME} operation="create">
<span>
<Button
htmlType="button"
type="primary"
icon={<PlusOutlined />}
onClick={handleCreateBtnClick}
>
<span>
<FormattedMessage id="common.create" />
</span>
</Button>
</span>
</EntityPermAccessControl>
)}
</div>
{items == null || items.length === 0 ? (
<p>
<FormattedMessage id="management.browser.noItems" />
</p>
) : null}
{items.map((e: EntityInstance<Car>) => (
<Card
title={e._instanceName}
key={e.id ? toIdString(e.id) : undefined}
style={{ marginBottom: "12px" }}
actions={getEntityCardsActions(e)}
>
{getFields(e).map(p => (
<EntityProperty
entityName={ENTITY_NAME}
propertyName={p}
value={e[p]}
key={p}
/>
))}
</Card>
))}
<div style={{ margin: "12px 0 12px 0", float: "right" }}>
<Paging
paginationConfig={entityListState.pagination ?? {}}
onPagingChange={handlePaginationChange}
total={count}
/>
</div>
</div>
);
})
Example #29
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>
);
}