antd#Affix TypeScript Examples
The following examples show how to use
antd#Affix.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: index.tsx From leek with Apache License 2.0 | 6 votes |
export function AppLayout({ children }: any) {
return (
<Layout>
<Header />
<Content
style={{
padding: "0 50px",
marginTop: 64,
}}
>
{children}
<Affix style={{ position: "fixed", bottom: 13, right: 50 }}>
<Row>
<Space>
<Text code>Leek v{env.LEEK_VERSION}</Text>
<span>-</span>
<a
href="https://tryleek.com"
target="_blank"
rel="noopener noreferrer"
>
Docs
</a>
</Space>
</Row>
</Affix>
</Content>
</Layout>
);
}
Example #2
Source File: ActionBar.tsx From condo with MIT License | 6 votes |
ActionBar: React.FC<IActionBarProps> = (props) => {
const { children, hidden } = props
const barWidthStyle = { width: props.isFormActionBar ? '100%' : 'calc(100% + 48px)' }
if (hidden) {
return null
}
return (
<Affix offsetBottom={48} style={barWidthStyle}>
<Space wrap={true} size={[24, 24]} css={actionBar} style={barWidthStyle}>
{ children }
</Space>
</Affix>
)
}
Example #3
Source File: index.tsx From tinyhouse with MIT License | 5 votes |
function App() {
const [viewer, setViewer] = useState<Viewer>(initialViewer);
const [logIn, { error }] = useMutation<LogInData, LogInVariables>(LOG_IN, {
onCompleted: (data) => {
if (data && data.logIn) {
setViewer(data.logIn);
if (data.logIn.token) {
sessionStorage.setItem("token", data.logIn.token);
}
} else {
sessionStorage.removeItem("token");
}
},
});
const logInRef = useRef(logIn);
useEffect(() => {
logInRef.current();
}, []);
if (!viewer.didRequest && !error) {
return (
<Layout className="app-skeleton">
<AppHeaderSkeleton />
<div className="app-skeleton__spin-section">
<Spin size="large" tip="Launching TinyHouse" />
</div>
</Layout>
);
}
const logInErrorBannerElement = error ? (
<ErrorBanner description="We weren't able to verify if you were logged in. Please try again later!" />
) : null;
return (
<BrowserRouter>
<Layout id="app">
{logInErrorBannerElement}
<Affix offsetTop={0} className="app__affix-header">
<AppHeader viewer={viewer} setViewer={setViewer} />
</Affix>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/host" element={<Host />} />
<Route path="/listing/:listingId" element={<Listing />} />
<Route
path="/listings"
element={<Listings title="TinyHouse Listings" />}
>
<Route
path="/listings/:location"
element={<Listings title="TinyHouse Listings" />}
/>
</Route>
<Route
path="/login"
element={<Login setViewer={setViewer} />}
/>
<Route
path="/user/:userId"
element={<User viewer={viewer} />}
/>
<Route path="*" element={<NotFound />} />
</Routes>
</Layout>
</BrowserRouter>
);
}
Example #4
Source File: TopNotifications.tsx From condo with MIT License | 5 votes |
useTopNotificationsHook = (): ITopNotificationHookResult => {
const [topNotifications, setTopNotifications] = useState<ITopNotification[]>([])
const addNotification = (notification: ITopNotification) => {
if (!topNotifications.find(existedNotification => existedNotification.id === notification.id)) {
setTopNotifications([...topNotifications, notification])
}
}
const removeNotification = (notificationId) => {
setTopNotifications([...topNotifications.filter(notification => notification.id !== notificationId)])
}
const TopNotificationComponent: React.FC = () => {
const { isSmall } = useLayoutContext()
return (
<>
<Affix>{
topNotifications.map(notification => {
return (
<Alert
showIcon
icon={(<InfoCircleFilled />)}
message={notification.message}
type={notification.type}
key={notification.id}
css={notificationAlert({ isSmall })}
action={<Space size={20}>
{
notification.actions.map((action, idx) => {
return (
<Button
onClick={async () => {
await action.action()
removeNotification(notification.id)
}}
size={isSmall ? 'middle' : 'large'}
type={'sberPrimary'}
secondary={action.secondary}
key={idx}
>
{action.title}
</Button>
)
})}
</Space>}
/>
)
})
}
</Affix>
</>
)
}
return {
addNotification,
TopNotificationComponent,
}
}
Example #5
Source File: index.tsx From shippo with MIT License | 4 votes |
CreationLayout: React.FC = () => {
const [current, setCurrent] = useState('app1')
const handleClick: MenuClickEventHandler = (event) => {
console.log('click ', event)
setCurrent(event.key)
}
const onSearch = (value: string) => console.log(value)
const callback = (key: string) => {
console.log(key)
}
const data = [
{
icon: <FormOutlined />,
title: '投稿',
},
{
icon: <QuestionCircleOutlined />,
title: '帮助',
},
]
return (
<Layout>
<Header>
<div style={{ display: 'flex', backgroundColor: '#fff' }}>
<div style={{ width: '250px', fontSize: '25px', color: '#1890ff', textAlign: 'center' }}>
Shippo 创作中心
</div>
<div style={{ flex: '1 1 0%' }}>
<span style={{ fontSize: '16px', margin: '0 30px', color: '#757575' }}>
<CrownOutlined style={{ marginRight: '5px' }} />
主页
</span>
</div>
<div style={{ padding: '0 50px' }}>
<Dropdown
placement="bottomCenter"
overlay={
<Menu>
<Menu.Item>个人中心</Menu.Item>
<Menu.Item>投稿管理</Menu.Item>
<Menu.Divider />
<Menu.Item>退出登录</Menu.Item>
</Menu>
}
>
<Avatar size={40} icon={<UserOutlined />} />
</Dropdown>
</div>
</div>
</Header>
<Layout>
<Sider width="250px" theme="light" style={{ paddingTop: '20px' }}>
<Affix offsetTop={20} onChange={(affixed) => console.log(affixed)}>
<div style={{ overflow: 'auto', maxHeight: '100vh' }}>
<div style={{ padding: '10px 25px', textAlign: 'center' }}>
<Button type="primary" size="large" style={{ width: '120px' }}>
投稿
</Button>
</div>
<div style={{ padding: '0 25px' }}>
<StyledMenu
style={{ width: '200px', border: 0, backgroundColor: '#fff' }}
defaultSelectedKeys={['1']}
mode="inline"
>
<Menu.Item key="1" icon={<HomeOutlined />}>
首页
</Menu.Item>
<SubMenu key="sub1" icon={<FileTextOutlined />} title="内容管理">
<Menu.Item key="2">稿件管理</Menu.Item>
</SubMenu>
<Menu.Item key="5" icon={<TeamOutlined />}>
粉丝管理
</Menu.Item>
<SubMenu key="sub2" icon={<MessageOutlined />} title="互动管理">
<Menu.Item key="6">评论管理</Menu.Item>
</SubMenu>
<Menu.Item key="7" icon={<SettingOutlined />}>
创作设置
</Menu.Item>
</StyledMenu>
</div>
</div>
</Affix>
</Sider>
<Content>
<div style={{ padding: '30px 50px' }}>
<StyledTabs defaultActiveKey="1" style={{ backgroundColor: '#fff' }}>
<TabPane tab="文章管理" key="1">
<Tinymce></Tinymce>
</TabPane>
<TabPane tab="文章管理" key="2">
Content of Tab Pane 2
</TabPane>
<TabPane tab="文章管理" key="3">
Content of Tab Pane 3
</TabPane>
</StyledTabs>
</div>
</Content>
</Layout>
</Layout>
)
}
Example #6
Source File: index.tsx From shippo with MIT License | 4 votes |
ReadLayout: React.FC = () => {
const [current, setCurrent] = useState('app1')
const handleClick: MenuClickEventHandler = (event) => {
console.log('click ', event)
setCurrent(event.key)
}
const onSearch = (value: string) => console.log(value)
const callback = (key: string) => {
console.log(key)
}
const data = [
{
icon: <FormOutlined />,
title: '投稿',
},
{
icon: <QuestionCircleOutlined />,
title: '帮助',
},
]
return (
<Layout>
<Header>
<div style={{ display: 'flex' }}>
<div>
<Menu
onClick={handleClick}
selectedKeys={[current]}
mode="horizontal"
style={{ borderBottom: '1px solid #fff' }}
>
<Menu.Item key="index" icon={<img width="40px" src={avatar} alt="" />}>
Shippo
</Menu.Item>
<Menu.Item key="app1">导航1</Menu.Item>
<Menu.Item key="app2">导航2</Menu.Item>
<Menu.Item key="app3">导航3</Menu.Item>
<Menu.Item key="app4">导航4</Menu.Item>
</Menu>
</div>
<div style={{ flex: '1 1 0%', backgroundColor: '#fff' }}>
<Search
placeholder=""
allowClear
onSearch={onSearch}
style={{ width: '100%', maxWidth: '500px', padding: '12px 10px 0 50px' }}
size="large"
/>
</div>
<div style={{ backgroundColor: '#fff', padding: '0 20px' }}>
<Space size={30}>
<Dropdown
placement="bottomCenter"
overlay={
<Menu>
<Menu.Item>个人中心</Menu.Item>
<Menu.Item>投稿管理</Menu.Item>
<Menu.Divider />
<Menu.Item>退出登录</Menu.Item>
</Menu>
}
>
<Avatar size={40} icon={<UserOutlined />} />
</Dropdown>
<Button type="primary">投稿</Button>
</Space>
</div>
</div>
</Header>
<Layout>
<Sider width="250px" theme="light" style={{ paddingTop: '20px' }}>
<Affix offsetTop={20} onChange={(affixed) => console.log(affixed)}>
<Menu
// onClick={handleClick}
style={{ width: '250px' }}
defaultSelectedKeys={['home']}
mode="inline"
>
<Menu.Item key="home" icon={<MailOutlined />}>
推荐
</Menu.Item>
<Menu.Item key="a" icon={<MailOutlined />}>
动画
</Menu.Item>
<Menu.Item key="c" icon={<MailOutlined />}>
漫画
</Menu.Item>
<Menu.Item key="g" icon={<MailOutlined />}>
游戏
</Menu.Item>
<Menu.Item key="n" icon={<MailOutlined />}>
轻小说
</Menu.Item>
</Menu>
</Affix>
</Sider>
<Content>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
<p style={{ height: '200px', padding: '30px' }}>内容</p>
</Content>
<Sider theme="light" width="300px" style={{ paddingTop: '20px' }}>
<Affix offsetTop={20} onChange={(affixed) => console.log(affixed)}>
<div style={{ overflow: 'auto', maxHeight: '100vh' }}>
<Search
placeholder="input search text"
allowClear
onSearch={onSearch}
style={{ width: '300px' }}
/>
<Card title="排行榜" bordered={false} style={{ width: '300px' }}>
<Tabs defaultActiveKey="1" onChange={callback}>
<TabPane tab="日榜" key="1">
日榜
</TabPane>
<TabPane tab="周榜" key="2">
周榜
</TabPane>
<TabPane tab="月榜" key="3">
月榜
</TabPane>
</Tabs>
</Card>
<Card title="更多" bordered={false} style={{ width: '300px' }}>
<List
itemLayout="horizontal"
dataSource={data}
split={false}
renderItem={(item) => (
<List.Item>
<List.Item.Meta
avatar={<Avatar shape="square" icon={item.icon} />}
title={<a href="https://ant.design">{item.title}</a>}
/>
</List.Item>
)}
/>
</Card>
</div>
</Affix>
</Sider>
</Layout>
</Layout>
)
}
Example #7
Source File: index.tsx From jetlinks-ui-antd with MIT License | 4 votes |
FieldAccess: React.FC<Props> = props => {
const initState: State = {
currentItem: props.data,
tempFieldAccess: [],
};
const [currentItem, setCurrentItem] = useState(initState.currentItem);
const [tempFieldAccess, setTempFieldAccess] = useState(initState.tempFieldAccess);
useEffect(() => {
setCurrentItem(props.data);
const tempKey: string[] = [];
((props.data.current || {}).dataAccesses || []).forEach(e =>
e.config.fields.forEach((i: string) => tempKey.push(e.action + '-' + i)),
);
const temp = (props.data.optionalFields || []).map(field => {
const actions = (props.data.actions || []).map(a => {
let key = a.action + '-' + field.name;
return { checked: tempKey.find(i => i === key) ? false : true, ...a };
});
return { actions, ...field } as TempField;
});
setTempFieldAccess(temp);
}, [props.data]);
const buildAccess = () => {
let tempAccess = new Map();
tempFieldAccess.forEach(item => {
item.actions.forEach(ac => {
if (ac.checked) return;
//获取不到就创建
let fieldAccess =
tempAccess.get(ac.action) ||
tempAccess
.set(ac.action, {
action: ac.action,
type: 'DENY_FILEDS',
config: {
fields: [],
},
})
.get(ac.action);
fieldAccess.config.fields.push(item.name);
});
});
currentItem.dataAccesses = [...tempAccess.values()];
props.save(currentItem);
message.success('保存成功');
};
const checkItem = (field: string, action: string) => {
const temp = tempFieldAccess.map(item => {
if (item.name === field) {
item.actions.map(ac => {
if (ac.action === action) {
ac.checked = !ac.checked;
}
return ac;
});
}
return item;
});
setTempFieldAccess(temp);
};
return (
<Modal
title={`设置${currentItem.name}字段权限`}
visible
width={800}
onCancel={() => {
props.close();
setCurrentItem({});
}}
onOk={() => {
buildAccess();
}}
>
<Alert
message='描述: 如果角色对当前对象设置了 "新建" 或 "导入" 权限,带*号的必填字段默认设置为“读写”,不可设置为“只读”或“不可见”,否则会造成新建/导入不成功'
type="info"
/>
<Divider type="horizontal" />
<Card bordered={false} id="field-access-card" style={{ height: 300, overflow: 'auto' }}>
<Col span={20}>
{tempFieldAccess.map(data => {
return (
<Row style={{ width: '100%', marginTop: 20 }} key={data.name} id={data.name}>
<Col span={4} style={{ fontWeight: 700 }}>
<span style={{ fontSize: '14px', color: '#f5222d' }}>* </span>
{data.describe}
</Col>
<Col span={20}>
<Checkbox.Group
style={{ width: '100%' }}
value={data.actions.filter(e => e.checked === true).map(e => e.action)}
>
{data.actions.map(item => (
<Col key={item.action} span={5}>
<Checkbox
value={item.action}
onClick={() => {
checkItem(data.name, item.action);
}}
>
{item.name}
</Checkbox>
</Col>
))}
</Checkbox.Group>
</Col>
</Row>
);
})}
</Col>
<Col span={3} push={1} style={{ height: 600, overflow: 'auto' }}>
<Affix>
<Anchor getContainer={() => document.getElementById('field-access-card') || window}>
{(props.data.optionalFields || []).map(data => (
<Anchor.Link href={'#' + data.name} title={data.describe} key={data.name} />
))}
</Anchor>
</Affix>
</Col>
</Card>
</Modal>
);
}
Example #8
Source File: index.tsx From jetlinks-ui-antd with MIT License | 4 votes |
AuthorizationSetting: React.FC<Props> = (props) => {
const initState: State = {
permissionList: [],
// currentList: [],
mergeData: [],
loading: true,
fieldAccessVisible: false,
checkedPermission: {},
}
const [permissionList, setPermissionList] = useState(initState.permissionList);
// const [currentList, setCurrentList] = useState(initState.currentList);
const [mergeData, setMergeData] = useState(initState.mergeData);
const [loading, setLoading] = useState(initState.loading);
const [fieldAccessVisible, setFieldAccessVisible] = useState(initState.fieldAccessVisible);
const [checkedPermission, setCheckedPermission] = useState(initState.checkedPermission);
useEffect(() => {
setLoading(true);
getPermissionList();
}, [props.dimension]);
const getPermissionList = () => {
apis.permission.listNoPaging().then(p => {
if (p.status === 200) {
if (props.dimension && props.dimension.id) {
const params = {
terms: { dimensionTarget: props.dimension.id, }
};
apis.dimensions.queryAutz(encodeQueryParam(params)).then(e => {
if (e.status === 200) {
const temp = (p.result).map((item: PermissionItem) => {
const autz = (e.result).find((i: DimensionsSetting) => i.permission === item.id);
//增加all选项
if (autz) {
(autz.actions || []).length >= (item.actions || []).length ? autz.actions.push('all') : autz;
}
return autz ? { 'current': autz, ...item } : item;
});
setPermissionList(temp);
}
}).catch(() => {
});
} else {
setPermissionList(p.result);
}
setLoading(false);
}
}).catch(() => {
});
}
const saveFieldsAccess = (permision: PermissionItem) => {
const index = permissionList.findIndex(p => p.id === permision.id);
permissionList[index] = permision;
}
/*
* * checkItem: 选择权限
* * permissionID :权限ID
* * item :权限
* */
const checkItem = (permissionId: string, item?: string) => {
const permission = permissionList.find(e => e.id === permissionId);
if (!permission) return;
const index = permissionList.findIndex(e => e.id === permissionId);
if (permission.current) {
let currentActions = permission.current.actions || [];
//存在current才会有删除的可能
if (item) {
const y = currentActions.find((e: string) => e === item);
if (y) {
//如果存在则删除
permission.current.actions = currentActions.filter((e: string) => e !== item && e !== 'all');
} else {
currentActions.push(item);
}
if (currentActions.length >= (permission.actions || []).length) {
currentActions.push('all');
}
} else {
if (currentActions.find((e: string) => e === 'all')) {
const { current, ...temp } = permission;
permissionList[index] = temp;
} else {
let current = {
actions: ['all', ...((permission.actions || []).map((e: any) => e.action))],
}
permission.current = current
permissionList[index] = permission;
}
}
} else {
if (item) {
let current = {
actions: [item],
}
permissionList[index] = { current, ...permission };
} else {
let current = {
actions: ['all', ...((permission.actions || []).map((e: any) => e.action))],
}
permissionList[index] = { current, ...permission };
}
}
setPermissionList(permissionList);
setMergeData([]);
}
const saveAutzSetting = () => {
//组装成接口所需格式
setLoading(true);
const data: any[] = [];
permissionList.forEach(p => {
let action = ((p.current || {}).actions || []).filter(e => e !== 'all');
data.push({
'id': (p.current || {}).id,
'permission': p.id,
'dimensionType': props.dimension.typeId,
'dimensionTypeName': props.dimension.typeName,
'dimensionTarget': props.dimension.id,
'dimensionTargetName': props.dimension.name,
'state': 1,
'actions': action,
'priority': 10,
'merge': true,
'dataAccesses': p.dataAccesses,
});
});
apis.dimensions.saveAutzSetting(data).then(e => {
if (e.status === 200) {
message.success('保存成功');
} else {
message.error('保存失败');
}
}).finally(() => {
setLoading(false);
setMergeData([]);
getPermissionList();
})
}
return (
<Spin spinning={loading}>
<Card
>
<Descriptions title={(props.dimension || {}).name}>
<Descriptions.Item>{(props.dimension || {}).describe}</Descriptions.Item>
</Descriptions>
<Col span={20}>
{
permissionList.map(permission => {
return (
<Card
id={permission.id}
key={permission.id}
title={permission.name}
bordered={false}
extra={
<span>
{
permission.optionalFields &&
<span>
<Button type="link" onClick={() => { setFieldAccessVisible(true); setCheckedPermission(permission) }}>字段权限</Button>
<Divider type="vertical" />
</span>
}
{/* <Button type="link" onClick={() => message.warn('开发中')}>数据权限</Button> */}
</span>
}
>
<Checkbox.Group style={{ width: '100%' }} value={(permission.current || {}).actions}>
<Col span={6} style={{ marginBottom: 10 }}>
<Checkbox
value="all"
onClick={() => checkItem(permission.id)}
indeterminate={
((permission.current || {}).actions || []).length > 0 &&
((permission.current || {}).actions || []).length < (permission.actions || []).length
}
>全选</Checkbox>
</Col>
{
(permission.actions || []).map((action: any) => {
return (
<Col span={6} style={{ marginBottom: 10 }} key={action.action}>
<Checkbox value={action.action} onClick={() => { checkItem(permission.id, action.action) }}>{action.name}</Checkbox>
</Col>
)
})
}
</Checkbox.Group>
</Card>
)
}
)
}
<Affix offsetBottom={20} style={{ float: "right" }}>
<Button
type="primary"
onClick={() => { saveAutzSetting() }}
>
保存
</Button>
</Affix>
</Col>
{/* <Col span={3} push={1} style={{ height: 600, overflow: 'auto' }}>
<Affix>
<Anchor>
{
permissionList.map(permission =>
<Anchor.Link href={'#' + permission.id} title={permission.name} key={permission.id} />
)
}
</Anchor>
</Affix>
</Col> */}
</Card>
{
fieldAccessVisible &&
<FieldAccess
close={() => setFieldAccessVisible(false)}
data={checkedPermission}
save={(item: PermissionItem) => saveFieldsAccess(item)}
/>
}
</Spin>
);
}
Example #9
Source File: index.tsx From leek with Apache License 2.0 | 4 votes |
IndexPage = () => {
const { currentApp, currentEnv } = useApplication();
const [stats, setStats] = useState<any>({});
const stats_widgets = StatsWidgets(stats);
// Metadata
const metricsService = new MetricsService();
const [seenWorkers, setSeenWorkers] = useState<
MetricsContextData["seenWorkers"]
>([]);
const [seenTasks, setSeenTasks] = useState<MetricsContextData["seenTasks"]>(
[]
);
const [processedEvents, setProcessedEvents] =
useState<MetricsContextData["processedEvents"]>(0);
const [processedTasks, setProcessedTasks] =
useState<MetricsContextData["processedTasks"]>(0);
const [seenStates, setSeenStates] = useState<
MetricsContextData["seenStates"]
>([]);
const [seenTaskStates, setSeenTaskStates] = useState<
MetricsContextData["seenStates"]
>([]);
const [seenRoutingKeys, setSeenRoutingKeys] = useState<
MetricsContextData["seenRoutingKeys"]
>([]);
const [seenQueues, setSeenQueues] = useState<
MetricsContextData["seenQueues"]
>([]);
const [searchDriftLoading, setSearchDriftLoading] = useState<boolean>(true);
const [searchDrift, setSearchDrift] = useState<any>(null);
const [timeFilters, setTimeFilters] = useState<any>({
timestamp_type: "timestamp",
interval_type: "past",
offset: 900000,
});
function getSearchDrift() {
if (!currentApp || !currentEnv) return;
setSearchDriftLoading(true);
metricsService
.getSearchDrift(currentApp, currentEnv)
.then(handleAPIResponse)
.then((result: any) => {
setSearchDrift(result);
}, handleAPIError)
.catch(handleAPIError)
.finally(() => setSearchDriftLoading(false));
}
function getMetrics() {
if (!currentApp) return;
metricsService
.getBasicMetrics(currentApp, currentEnv, timeFilters)
.then(handleAPIResponse)
.then((result: any) => {
setProcessedEvents(result.aggregations.processed_events.value);
const processed = result.aggregations.seen_states.buckets.reduce(
(result, item) => {
if (!workerStates.includes(item.key)) {
return result + item.doc_count;
}
return result;
},
0
);
setProcessedTasks(processed);
setSeenWorkers(result.aggregations.seen_workers.buckets);
setSeenTasks(result.aggregations.seen_tasks.buckets);
setSeenStates(result.aggregations.seen_states.buckets);
setSeenTaskStates(
tasksStatesDefaults
.map(
(obj) =>
result.aggregations.seen_states.buckets.find(
(o) => o.key === obj.key
) || obj
)
.filter((item) => !workerStates.includes(item.key))
);
setSeenRoutingKeys(result.aggregations.seen_routing_keys.buckets);
setSeenQueues(result.aggregations.seen_queues.buckets);
}, handleAPIError)
.catch(handleAPIError);
}
useEffect(() => {
let adapted = {
SEEN_TASKS: seenTasks.length,
SEEN_WORKERS: seenWorkers.length,
PROCESSED_EVENTS: processedEvents,
PROCESSED_TASKS: processedTasks,
TASKS: 0,
EVENTS: 0,
// Tasks
QUEUED: 0,
RECEIVED: 0,
STARTED: 0,
SUCCEEDED: 0,
FAILED: 0,
REJECTED: 0,
REVOKED: 0,
IGNORED: 0,
RETRY: 0,
RECOVERED: 0,
CRITICAL: 0,
// Worker
ONLINE: 0,
HEARTBEAT: 0,
OFFLINE: 0,
};
seenStates.map((task, _) => (adapted[task.key] = task.doc_count));
setStats(adapted);
}, [seenTasks, seenWorkers, seenStates]);
useEffect(() => {
getMetrics();
getSearchDrift();
return () => {
clearInterval(metricsInterval);
};
}, []);
useEffect(() => {
// Stop refreshing metadata
if (metricsInterval) clearInterval(metricsInterval);
// If no application specified, return
if (!currentApp) return;
// Else, get metrics every 10 seconds
getMetrics();
getSearchDrift();
metricsInterval = setInterval(() => {
getMetrics();
getSearchDrift();
}, 10000);
}, [currentApp, currentEnv, timeFilters]);
return (
<>
<Helmet>
<html lang="en" />
<title>Metrics</title>
<meta name="description" content="Events metrics" />
<meta name="keywords" content="celery, tasks" />
</Helmet>
<Row justify="space-between" align="middle" style={{ marginBottom: 16 }}>
<Statistic
loading={searchDriftLoading}
title={
<Tooltip title="The time of the latest event processed by leek.">
<span>Latest Event </span>
<InfoCircleOutlined />
</Tooltip>
}
value={
searchDrift && searchDrift.latest_event_timestamp
? moment(searchDrift.latest_event_timestamp).format(
"MMM D HH:mm:ss Z"
)
: ""
}
valueStyle={{ fontSize: 17.5 }}
prefix={<FieldTimeOutlined />}
/>
<Affix
style={{
position: "fixed",
left: "50%",
transform: "translate(-50%, 0)",
}}
>
<Row>
<TimeFilter
timeFilter={timeFilters}
onTimeFilterChange={setTimeFilters}
/>
</Row>
</Affix>
<Statistic
loading={searchDriftLoading}
title={
<Tooltip title="How many events in the queue waiting to be indexed.">
<span>Current Drift </span>
<InfoCircleOutlined />
</Tooltip>
}
value={
searchDrift && searchDrift.messages_count
? searchDrift.messages_count
: "0"
}
valueStyle={{ fontSize: 17.5 }}
prefix={<EyeInvisibleOutlined />}
/>
</Row>
<Row gutter={16} justify="center" align="middle">
{stats_widgets.map((widget, idx) => (
<Col
lg={12}
md={12}
sm={12}
xs={24}
key={idx}
style={{ marginBottom: "16px" }}
>
<StickerWidget
number={widget.number}
text={widget.text}
icon={widget.icon}
tooltip={widget.tooltip}
/>
</Col>
))}
</Row>
</>
);
}
Example #10
Source File: index.tsx From condo with MIT License | 4 votes |
TicketPageContent = ({ organization, employee, TicketContent }) => {
const intl = useIntl()
const ServerErrorMessage = intl.formatMessage({ id: 'ServerError' })
const UpdateMessage = intl.formatMessage({ id: 'Edit' })
const PrintMessage = intl.formatMessage({ id: 'Print' })
const SourceMessage = intl.formatMessage({ id: 'pages.condo.ticket.field.Source' })
const TicketAuthorMessage = intl.formatMessage({ id: 'Author' })
const EmergencyMessage = intl.formatMessage({ id: 'Emergency' })
const PaidMessage = intl.formatMessage({ id: 'Paid' })
const WarrantyMessage = intl.formatMessage({ id: 'Warranty' })
const ReturnedMessage = intl.formatMessage({ id: 'Returned' })
const ChangedMessage = intl.formatMessage({ id: 'Changed' })
const TimeHasPassedMessage = intl.formatMessage({ id: 'TimeHasPassed' })
const DaysShortMessage = intl.formatMessage({ id: 'DaysShort' })
const HoursShortMessage = intl.formatMessage({ id: 'HoursShort' })
const MinutesShortMessage = intl.formatMessage({ id: 'MinutesShort' })
const LessThanMinuteMessage = intl.formatMessage({ id: 'LessThanMinute' })
const ResidentCannotReadTicketMessage = intl.formatMessage({ id: 'pages.condo.ticket.title.ResidentCannotReadTicket' })
const router = useRouter()
const auth = useAuth() as { user: { id: string } }
const user = get(auth, 'user')
const { isSmall } = useLayoutContext()
// NOTE: cast `string | string[]` to `string`
const { query: { id } } = router as { query: { [key: string]: string } }
const { refetch: refetchTicket, loading, obj: ticket, error } = Ticket.useObject({
where: { id: id },
}, {
fetchPolicy: 'network-only',
})
// TODO(antonal): get rid of separate GraphQL query for TicketChanges
const ticketChangesResult = TicketChange.useObjects({
where: { ticket: { id } },
// TODO(antonal): fix "Module not found: Can't resolve '@condo/schema'"
// sortBy: [SortTicketChangesBy.CreatedAtDesc],
// @ts-ignore
sortBy: ['createdAt_DESC'],
}, {
fetchPolicy: 'network-only',
})
const { objs: comments, refetch: refetchComments } = TicketComment.useObjects({
where: { ticket: { id } },
// @ts-ignore
sortBy: ['createdAt_DESC'],
})
const commentsIds = useMemo(() => map(comments, 'id'), [comments])
const { objs: ticketCommentFiles, refetch: refetchCommentFiles } = TicketCommentFile.useObjects({
where: { ticketComment: { id_in: commentsIds } },
// @ts-ignore
sortBy: ['createdAt_DESC'],
})
const commentsWithFiles = useMemo(() => comments.map(comment => {
comment.files = ticketCommentFiles.filter(file => file.ticketComment.id === comment.id)
return comment
}), [comments, ticketCommentFiles])
const updateComment = TicketComment.useUpdate({}, () => {
refetchComments()
refetchCommentFiles()
})
const deleteComment = TicketComment.useSoftDelete({}, () => {
refetchComments()
})
const createCommentAction = TicketComment.useCreate({
ticket: id,
user: auth.user && auth.user.id,
}, () => Promise.resolve())
const { obj: ticketCommentsTime, refetch: refetchTicketCommentsTime } = TicketCommentsTime.useObject({
where: {
ticket: { id: id },
},
})
const {
obj: userTicketCommentReadTime, refetch: refetchUserTicketCommentReadTime, loading: loadingUserTicketCommentReadTime,
} = UserTicketCommentReadTime.useObject({
where: {
user: { id: user.id },
ticket: { id: id },
},
})
const createUserTicketCommentReadTime = UserTicketCommentReadTime.useCreate({
user: user.id,
ticket: id,
}, () => refetchUserTicketCommentReadTime())
const updateUserTicketCommentReadTime = UserTicketCommentReadTime.useUpdate({
user: user.id,
ticket: id,
}, () => refetchUserTicketCommentReadTime())
const canShareTickets = get(employee, 'role.canShareTickets')
const TicketTitleMessage = useMemo(() => getTicketTitleMessage(intl, ticket), [ticket])
const TicketCreationDate = useMemo(() => getTicketCreateMessage(intl, ticket), [ticket])
const refetchCommentsWithFiles = useCallback(async () => {
await refetchComments()
await refetchCommentFiles()
await refetchTicketCommentsTime()
await refetchUserTicketCommentReadTime()
}, [refetchCommentFiles, refetchComments, refetchTicketCommentsTime, refetchUserTicketCommentReadTime])
const actionsFor = useCallback(comment => {
const isAuthor = comment.user.id === auth.user.id
const isAdmin = get(auth, ['user', 'isAdmin'])
return {
updateAction: isAdmin || isAuthor ? updateComment : null,
deleteAction: isAdmin || isAuthor ? deleteComment : null,
}
}, [auth, deleteComment, updateComment])
useEffect(() => {
const handler = setInterval(refetchCommentsWithFiles, COMMENT_RE_FETCH_INTERVAL)
return () => {
clearInterval(handler)
}
})
const isEmergency = get(ticket, 'isEmergency')
const isPaid = get(ticket, 'isPaid')
const isWarranty = get(ticket, 'isWarranty')
const statusReopenedCounter = get(ticket, 'statusReopenedCounter')
const handleTicketStatusChanged = () => {
refetchTicket()
ticketChangesResult.refetch()
}
const ticketStatusType = get(ticket, ['status', 'type'])
const disabledEditButton = useMemo(() => ticketStatusType === CLOSED_STATUS_TYPE, [ticketStatusType])
const statusUpdatedAt = get(ticket, 'statusUpdatedAt')
const isResidentTicket = useMemo(() => get(ticket, ['createdBy', 'type']) === RESIDENT, [ticket])
const canReadByResident = useMemo(() => get(ticket, 'canReadByResident'), [ticket])
const canCreateComments = useMemo(() => get(auth, ['user', 'isAdmin']) || get(employee, ['role', 'canManageTicketComments']),
[auth, employee])
const getTimeSinceCreation = useCallback(() => {
const diffInMinutes = dayjs().diff(dayjs(statusUpdatedAt), 'minutes')
const daysHavePassed = dayjs.duration(diffInMinutes, 'minutes').format('D')
const hoursHavePassed = dayjs.duration(diffInMinutes, 'minutes').format('H')
const minutesHavePassed = dayjs.duration(diffInMinutes, 'minutes').format('m')
const timeSinceCreation = compact([
Number(daysHavePassed) > 0 && DaysShortMessage.replace('${days}', daysHavePassed),
Number(hoursHavePassed) > 0 && HoursShortMessage.replace('${hours}', hoursHavePassed),
Number(minutesHavePassed) > 0 && MinutesShortMessage.replace('${minutes}', minutesHavePassed),
])
if (isEmpty(timeSinceCreation)) {
return LessThanMinuteMessage
}
return timeSinceCreation.join(' ')
}, [DaysShortMessage, HoursShortMessage, LessThanMinuteMessage, MinutesShortMessage, statusUpdatedAt])
if (!ticket) {
return (
<LoadingOrErrorPage title={TicketTitleMessage} loading={loading} error={error ? ServerErrorMessage : null}/>
)
}
return (
<>
<Head>
<title>{TicketTitleMessage}</title>
</Head>
<PageWrapper>
<PageContent>
<Row gutter={[0, 40]}>
<Col lg={16} xs={24}>
<Row gutter={[0, 40]}>
<Col span={24}>
<Row gutter={[0, 40]}>
<Col lg={18} xs={24}>
<Row gutter={[0, 20]}>
<Col span={24}>
<Typography.Title style={{ margin: 0 }} level={1}>{TicketTitleMessage}</Typography.Title>
</Col>
<Col span={24}>
<Row>
<Col span={24}>
<Typography.Text style={TICKET_CREATE_INFO_TEXT_STYLE}>
<Typography.Text style={TICKET_CREATE_INFO_TEXT_STYLE} type='secondary'>{TicketCreationDate}, {TicketAuthorMessage} </Typography.Text>
<UserNameField user={get(ticket, ['createdBy'])}>
{({ name, postfix }) => (
<Typography.Text style={TICKET_CREATE_INFO_TEXT_STYLE}>
{name}
{postfix && <Typography.Text type='secondary' ellipsis> {postfix}</Typography.Text>}
</Typography.Text>
)}
</UserNameField>
</Typography.Text>
</Col>
<Col span={24}>
<Typography.Text type='secondary' style={TICKET_CREATE_INFO_TEXT_STYLE}>
{SourceMessage} — {get(ticket, ['source', 'name'], '').toLowerCase()}
</Typography.Text>
</Col>
<Col span={24}>
{
!isResidentTicket && !canReadByResident && (
<Typography.Text type='secondary' style={TICKET_CREATE_INFO_TEXT_STYLE}>
<FormattedMessage
id={'pages.condo.ticket.title.CanReadByResident'}
values={{
canReadByResident: (
<Typography.Text type={'danger'}>
{ResidentCannotReadTicketMessage}
</Typography.Text>
),
}}
/>
</Typography.Text>
)
}
</Col>
</Row>
</Col>
</Row>
</Col>
<Col lg={6} xs={24}>
<Row justify={isSmall ? 'center' : 'end'} gutter={[0, 20]}>
<Col span={24}>
<TicketStatusSelect
organization={organization}
employee={employee}
ticket={ticket}
onUpdate={handleTicketStatusChanged}
loading={loading}
data-cy={'ticket__status-select'}
/>
</Col>
{
statusUpdatedAt && (
<Col>
<Typography.Paragraph style={TICKET_UPDATE_INFO_TEXT_STYLE}>
{ChangedMessage}: {dayjs(statusUpdatedAt).format('DD.MM.YY, HH:mm')}
</Typography.Paragraph>
<Typography.Paragraph style={TICKET_UPDATE_INFO_TEXT_STYLE} type={'secondary'}>
{TimeHasPassedMessage.replace('${time}', getTimeSinceCreation())}
</Typography.Paragraph>
</Col>
)
}
</Row>
</Col>
</Row>
<Space direction={'horizontal'} style={{ marginTop: '1.6em ' }}>
{isEmergency && <TicketTag color={TICKET_TYPE_TAG_COLORS.emergency}>{EmergencyMessage.toLowerCase()}</TicketTag>}
{isPaid && <TicketTag color={TICKET_TYPE_TAG_COLORS.paid}>{PaidMessage.toLowerCase()}</TicketTag>}
{isWarranty && <TicketTag color={TICKET_TYPE_TAG_COLORS.warranty}>{WarrantyMessage.toLowerCase()}</TicketTag>}
{
statusReopenedCounter > 0 && (
<TicketTag color={TICKET_TYPE_TAG_COLORS.returned}>
{ReturnedMessage.toLowerCase()} {statusReopenedCounter > 1 && `(${statusReopenedCounter})`}
</TicketTag>
)
}
</Space>
</Col>
<TicketContent ticket={ticket}/>
<ActionBar>
<Link href={`/ticket/${ticket.id}/update`}>
<Button
disabled={disabledEditButton}
color={'green'}
type={'sberDefaultGradient'}
secondary
icon={<EditFilled />}
data-cy={'ticket__update-link'}
>
{UpdateMessage}
</Button>
</Link>
{
!isSmall && (
<Button
type={'sberDefaultGradient'}
icon={<FilePdfFilled />}
href={`/ticket/${ticket.id}/pdf`}
target={'_blank'}
secondary
>
{PrintMessage}
</Button>
)
}
{
canShareTickets
? <ShareTicketModal
organization={organization}
date={get(ticket, 'createdAt')}
number={get(ticket, 'number')}
details={get(ticket, 'details')}
id={id}
locale={get(organization, 'country')}
/>
: null
}
</ActionBar>
<TicketChanges
loading={get(ticketChangesResult, 'loading')}
items={get(ticketChangesResult, 'objs')}
total={get(ticketChangesResult, 'count')}
/>
</Row>
</Col>
<Col lg={7} xs={24} offset={isSmall ? 0 : 1}>
<Affix offsetTop={40}>
<Comments
ticketCommentsTime={ticketCommentsTime}
userTicketCommentReadTime={userTicketCommentReadTime}
createUserTicketCommentReadTime={createUserTicketCommentReadTime}
updateUserTicketCommentReadTime={updateUserTicketCommentReadTime}
loadingUserTicketCommentReadTime={loadingUserTicketCommentReadTime}
FileModel={TicketCommentFile}
fileModelRelationField={'ticketComment'}
ticket={ticket}
// @ts-ignore
createAction={createCommentAction}
updateAction={updateComment}
refetchComments={refetchCommentsWithFiles}
comments={commentsWithFiles}
canCreateComments={canCreateComments}
actionsFor={actionsFor}
/>
</Affix>
</Col>
</Row>
</PageContent>
</PageWrapper>
</>
)
}
Example #11
Source File: index.tsx From datart with Apache License 2.0 | 4 votes |
EditorPage: FC = () => {
const [form] = Form.useForm();
const [container, setContainer] = useState<HTMLDivElement | null>(null);
const dispatch = useDispatch();
const [jobType, setJobType] = useState(JobTypes.Email);
const [fileType, setFileType] = useState<FileTypes[]>(
DEFAULT_VALUES.type || [],
);
const [periodUnit, setPeriodUnit] = useState<TimeModes>(TimeModes.Minute);
const [periodInput, setPeriodInput] = useState(false);
const { params } = useRouteMatch<{ scheduleId: string; orgId: string }>();
const editingSchedule = useSelector(selectEditingSchedule);
const loading = useSelector(selectScheduleDetailLoading);
const saveLoding = useSelector(selectSaveLoading);
const unarchiveLoading = useSelector(selectUnarchiveLoading);
const deleteLoding = useSelector(selectDeleteLoading);
const { toDetails } = useToScheduleDetails();
const isArchived = editingSchedule?.status === 0;
const t = useI18NPrefix('main.pages.schedulePage.sidebar.editorPage.index');
const { actions } = useScheduleSlice();
const { scheduleId, orgId } = params;
const isAdd = useMemo(() => {
return scheduleId === 'add';
}, [scheduleId]);
const active = useMemo(() => {
return editingSchedule?.active;
}, [editingSchedule]);
const refreshScheduleList = useCallback(() => {
dispatch(getSchedules(orgId));
}, [dispatch, orgId]);
const onFinish = useCallback(() => {
form.validateFields().then((values: FormValues) => {
if (!(values?.folderContent && values?.folderContent?.length > 0)) {
message.error(t('tickToSendContent'));
return;
}
const params = toScheduleSubmitParams(values, orgId);
if (isAdd) {
dispatch(
addSchedule({
params,
resolve: (id: string) => {
message.success(t('addSuccess'));
toDetails(orgId, id);
refreshScheduleList();
},
}),
);
} else {
dispatch(
editSchedule({
scheduleId: editingSchedule?.id as string,
params: { ...params, id: editingSchedule?.id as string },
resolve: () => {
message.success(t('saveSuccess'));
refreshScheduleList();
},
}),
);
}
});
}, [
form,
isAdd,
orgId,
dispatch,
editingSchedule,
refreshScheduleList,
toDetails,
t,
]);
const onResetForm = useCallback(() => {
form.resetFields();
setJobType(DEFAULT_VALUES.jobType as JobTypes);
setPeriodUnit(TimeModes.Minute);
setPeriodInput(false);
setFileType([FileTypes.Image]);
dispatch(actions.clearEditingSchedule);
}, [form, dispatch, actions?.clearEditingSchedule]);
useEffect(() => {
dispatch(getFolders(orgId));
if (scheduleId === 'add') {
onResetForm();
} else if (scheduleId) {
dispatch(getScheduleDetails(scheduleId));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scheduleId, dispatch]);
const onJobTypeChange = useCallback(
(v: JobTypes) => {
setJobType(v);
setFileType([FileTypes.Image]);
form.setFieldsValue({
type: [FileTypes.Image],
imageWidth: DEFAULT_VALUES.imageWidth,
});
},
[form],
);
const onFileTypeChange = useCallback(
(v: FileTypes[]) => {
setFileType(v);
form.setFieldsValue({
imageWidth:
v && v.includes(FileTypes.Image)
? DEFAULT_VALUES.imageWidth
: undefined,
});
},
[form],
);
const onPeriodUnitChange = useCallback(
(v: TimeModes) => {
setPeriodUnit(v);
switch (v) {
case TimeModes.Minute:
form.setFieldsValue({ minute: 10 });
break;
}
},
[form],
);
const onPeriodInputChange = useCallback(
(v: boolean) => {
if (v) {
form.setFieldsValue({
cronExpression: getCronExpressionByPartition(form.getFieldsValue()),
});
} else {
const timeValues = getTimeValues(form.getFieldValue('cronExpression'));
form.setFieldsValue(timeValues);
setPeriodUnit(timeValues?.periodUnit);
}
setPeriodInput(v);
},
[form],
);
const unarchive = useCallback(() => {
dispatch(
unarchiveSchedule({
id: editingSchedule!.id,
resolve: () => {
message.success(t('restoredSuccess'));
toDetails(orgId);
},
}),
);
}, [dispatch, toDetails, orgId, editingSchedule, t]);
const del = useCallback(
archive => () => {
dispatch(
deleteSchedule({
id: editingSchedule!.id,
archive,
resolve: () => {
message.success(
`${t('success')}${archive ? t('moveToTrash') : t('delete')}`,
);
toDetails(orgId);
},
}),
);
},
[dispatch, toDetails, orgId, editingSchedule, t],
);
useEffect(() => {
if (editingSchedule) {
const _type = editingSchedule?.type as JobTypes,
echoValues = toEchoFormValues(editingSchedule);
form.setFieldsValue(echoValues);
setFileType(echoValues?.type as FileTypes[]);
setJobType(_type);
setPeriodUnit(echoValues?.periodUnit as TimeModes);
setPeriodInput(!!echoValues?.setCronExpressionManually);
}
return () => {
setJobType(DEFAULT_VALUES.jobType as JobTypes);
dispatch(actions.clearEditingSchedule);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editingSchedule]);
return (
<Container ref={setContainer}>
<Affix offsetTop={0} target={() => container}>
<DetailPageHeader
title={isAdd ? t('newTimedTask') : editingSchedule?.name}
actions={
isArchived ? (
<>
<Popconfirm title={t('sureToRestore')} onConfirm={unarchive}>
<Button loading={unarchiveLoading}>{t('restore')}</Button>
</Popconfirm>
<Popconfirm title={t('sureToDelete')} onConfirm={del(false)}>
<Button loading={deleteLoding} danger>
{t('delete')}
</Button>
</Popconfirm>
</>
) : (
<>
<Tooltip
placement="bottom"
title={active ? t('allowModificationAfterStopping') : ''}
>
<Button
loading={saveLoding}
type="primary"
onClick={form.submit}
disabled={active}
>
{t('save')}
</Button>
</Tooltip>
{!isAdd && (
<Tooltip
placement="bottom"
title={active ? t('allowMoveAfterStopping') : ''}
>
<Popconfirm
title={t('sureMoveRecycleBin')}
onConfirm={del(true)}
>
<Button loading={deleteLoding} disabled={active} danger>
{t('moveToTrash')}
</Button>
</Popconfirm>
</Tooltip>
)}
</>
)
}
/>
</Affix>
<EditorWrapper>
<Spin spinning={loading}>
<Form
wrapperCol={{ span: 19 }}
labelCol={{ span: 5 }}
initialValues={DEFAULT_VALUES}
form={form}
onFinish={onFinish}
scrollToFirstError
>
<FormAreaWrapper>
{!isAdd && editingSchedule?.id ? (
<ScheduleErrorLog scheduleId={editingSchedule?.id} />
) : null}
<FormCard title={t('basicSettings')}>
<FormWrapper>
<BasicBaseForm
isAdd={isAdd}
orgId={orgId}
onJobTypeChange={onJobTypeChange}
initialName={editingSchedule?.name}
periodUnit={periodUnit}
onPeriodUnitChange={onPeriodUnitChange}
periodInput={periodInput}
onPeriodInputChange={onPeriodInputChange}
/>
</FormWrapper>
</FormCard>
{jobType === JobTypes.Email ? (
<FormCard title={t('emailSetting')}>
<FormWrapper>
<EmailSettingForm
fileType={fileType}
onFileTypeChange={onFileTypeChange}
/>
</FormWrapper>
</FormCard>
) : (
<FormCard title={t('enterpriseWeChatSettings')}>
<FormWrapper>
<WeChartSetttingForm />
</FormWrapper>
</FormCard>
)}
<FormCard title={t('sendContentSettings')}>
<FormWrapper>
<SendContentForm />
</FormWrapper>
</FormCard>
</FormAreaWrapper>
</Form>
</Spin>
</EditorWrapper>
</Container>
);
}